mohan #5

Merged
mohanmd merged 46 commits from mohan into master 2026-06-15 09:13:23 +00:00
11 changed files with 155 additions and 99 deletions
Showing only changes of commit 07cfb16708 - Show all commits

View File

@ -485,6 +485,7 @@ public function announcements()
'type' => $ann->type ?? 'Charity',
'posted_by' => $postedBy,
'organization' => $organization,
'posted_by_email' => $ann->sponsor ? $ann->sponsor->email : ($ann->employer ? $ann->employer->email : null),
'status' => $ann->status ?? 'pending',
'remarks' => $ann->remarks,
'created_at' => $ann->created_at ? $ann->created_at->format('M d, Y h:i A') : 'Just now',

View File

@ -40,7 +40,7 @@ export default function AdminLayout({ children, title = 'Dashboard' }) {
{ name: 'Campaigns & Alerts', href: '/admin/notifications', icon: BellRing },
{ name: 'Reports & Analytics', href: '/admin/analytics', icon: BarChart3 },
{ name: 'System Audit Logs', href: '/admin/audit-logs', icon: History },
{ name: 'Charity Events', href: '/admin/announcements', icon: Heart },
{ name: 'Charity Events', href: '/admin/events', icon: Heart },
{ name: 'Skills', href: '/admin/master-data/skills', icon: Settings },
{ name: 'Reason Master', href: '/admin/master-data/reasons', icon: List },
];

View File

@ -68,7 +68,7 @@ export default function EmployerLayout({ children, title, fullPage = false }) {
{ name: 'Shortlist', translationKey: 'shortlist', href: '/employer/shortlist', icon: Bookmark },
{ name: 'Candidates', translationKey: 'candidates', href: '/employer/candidates', icon: UserCheck },
{ name: 'Messages', translationKey: 'messages', href: '/employer/messages', icon: MessageSquare, badge: unread_messages_count },
{ name: 'Charity Events', translationKey: 'charity_events', href: '/employer/announcements', icon: Heart },
{ name: 'Charity Events', translationKey: 'charity_events', href: '/employer/events', icon: Heart },
{ name: 'Subscription', translationKey: 'subscription', href: '/employer/subscription', icon: CreditCard },
{ name: 'Payment History', translationKey: 'payment_history', href: '/employer/payments', icon: FileText },
{ name: 'My Profile', translationKey: 'my_profile', href: '/employer/profile', icon: User },

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Head, router } from '@inertiajs/react';
import { Head, router, Link } from '@inertiajs/react';
import AdminLayout from '../../../Layouts/AdminLayout';
import {
Heart, Plus, Trash2, Send, CheckCircle2, Check, X,
@ -68,7 +68,7 @@ export default function Announcements({ initialAnnouncements }) {
return;
}
router.post(route('admin.announcements.store'), {
router.post(route('admin.events.store'), {
title: newAnnouncement.title,
content: newAnnouncement.content,
provided_items: newAnnouncement.provided_items,
@ -107,7 +107,7 @@ export default function Announcements({ initialAnnouncements }) {
};
const handleApprove = (id) => {
router.post(route('admin.announcements.approve', id), {}, {
router.post(route('admin.events.approve', id), {}, {
onSuccess: () => {
showToast('Charity Event approved successfully');
setTimeout(() => window.location.reload(), 500);
@ -124,7 +124,7 @@ export default function Announcements({ initialAnnouncements }) {
e.preventDefault();
if (!rejectionRemarks.trim()) return;
router.post(route('admin.announcements.reject', rejectingAnnouncementId), {
router.post(route('admin.events.reject', rejectingAnnouncementId), {
remarks: rejectionRemarks
}, {
onSuccess: () => {
@ -138,7 +138,7 @@ export default function Announcements({ initialAnnouncements }) {
const handleDelete = (id) => {
if (confirm('Are you sure you want to delete this charity event?')) {
router.delete(route('admin.announcements.delete', id), {
router.delete(route('admin.events.delete', id), {
onSuccess: () => {
showToast('Charity Event deleted');
setTimeout(() => window.location.reload(), 500);
@ -186,7 +186,7 @@ export default function Announcements({ initialAnnouncements }) {
</div>
)}
<div className="space-y-6 select-none max-w-6xl mx-auto pb-12">
<div className="space-y-6 select-none max-w-7xl mx-auto pb-12">
{/* Upper Header section */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
@ -602,7 +602,7 @@ export default function Announcements({ initialAnnouncements }) {
<p className="font-bold text-sm">No charity events match your criteria.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{filteredAnnouncements.map((ann) => {
const details = ann.charityDetails;
const isExpanded = !!expandedCards[ann.id];
@ -616,46 +616,55 @@ export default function Announcements({ initialAnnouncements }) {
return (
<div
key={ann.id}
className={`bg-white rounded-xl border border-slate-200 border-l-4 ${statusColorClass} shadow-sm hover:shadow-md transition-all flex flex-col justify-between`}
className="group bg-white rounded-2xl border border-slate-200/80 hover:border-slate-300 shadow-sm hover:shadow-md transition-all duration-300 flex flex-col justify-between overflow-hidden relative"
>
{/* Top colored accent line */}
<div className={`h-1.5 w-full ${
ann.status === 'approved' ? 'bg-gradient-to-r from-emerald-400 to-teal-500' :
ann.status === 'rejected' ? 'bg-gradient-to-r from-rose-400 to-red-500' :
'bg-gradient-to-r from-amber-400 to-orange-500'
}`} />
{/* Top Card Area */}
<div className="p-4 space-y-3">
<div className="flex items-start justify-between gap-2">
<div className="space-y-1 flex-1 min-w-0">
<div className="flex items-center space-x-2 flex-wrap gap-y-1">
<h3 className="font-bold text-sm text-slate-800 tracking-tight flex items-center gap-1 min-w-0">
<Sparkles className="w-3.5 h-3.5 text-rose-500 fill-rose-50 shrink-0" />
<span className="truncate block" title={ann.title}>{ann.title}</span>
</h3>
<span className={`px-2 py-0.5 rounded text-[8px] font-extrabold uppercase tracking-wider shrink-0 ${
ann.status === 'approved' ? 'bg-emerald-50 text-emerald-700 border border-solid border-emerald-100' :
ann.status === 'rejected' ? 'bg-rose-50 text-rose-700 border border-solid border-rose-100' :
'bg-amber-50 text-amber-700 border border-solid border-amber-100'
<div className="p-5 flex-1 flex flex-col justify-between space-y-4">
<div className="space-y-3">
{/* Header Title + Actions */}
<div className="flex items-start justify-between gap-3">
<div className="space-y-1.5 flex-1 min-w-0">
{/* Status Badge */}
<div className="flex items-center space-x-2">
<span className={`px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider shrink-0 border border-solid ${
ann.status === 'approved' ? 'bg-emerald-50 text-emerald-700 border-emerald-100' :
ann.status === 'rejected' ? 'bg-rose-50 text-rose-700 border-rose-100' :
'bg-amber-50 text-amber-700 border-amber-100'
}`}>
{ann.status}
</span>
{ann.type === 'charity' && (
<span className="bg-teal-50 text-teal-700 border border-solid border-teal-100 px-2 py-0.5 rounded-full text-[9px] font-black uppercase tracking-wider flex items-center gap-0.5">
<Heart className="w-2 h-2 fill-teal-600 text-teal-600" />
<span>Charity</span>
</span>
)}
</div>
</div>
<div className="flex items-center text-[10px] text-slate-400 font-bold whitespace-nowrap overflow-hidden text-ellipsis">
<Building className="w-3 h-3 mr-1 text-slate-400 shrink-0" />
<span className="truncate max-w-[120px]" title={ann.organization}>{ann.organization}</span>
<span className="mx-1"></span>
<User className="w-3 h-3 mr-1 text-slate-400 shrink-0" />
<span className="truncate max-w-[100px]" title={ann.posted_by}>{ann.posted_by}</span>
</div>
{/* Event Title */}
<h3 className="font-extrabold text-sm text-slate-800 leading-snug tracking-tight group-hover:text-[#0F6E56] transition-colors duration-200" title={ann.title}>
{ann.title}
</h3>
</div>
{/* Action Control Buttons */}
<div className="flex items-center space-x-1 shrink-0">
{ann.status === 'pending' && (
<div className="flex items-center space-x-1 bg-slate-50 p-0.5 rounded-lg border border-slate-200">
<div className="flex items-center space-x-0.5 bg-slate-50 p-0.5 rounded-lg border border-slate-200">
<button
type="button"
onClick={() => handleApprove(ann.id)}
className="p-1 text-emerald-600 hover:bg-emerald-50 rounded transition-colors cursor-pointer border-none bg-transparent"
title="Approve"
>
<Check className="w-4 h-4" />
<Check className="w-3.5 h-3.5" />
</button>
<button
type="button"
@ -663,24 +672,48 @@ export default function Announcements({ initialAnnouncements }) {
className="p-1 text-rose-600 hover:bg-rose-50 rounded transition-colors cursor-pointer border-none bg-transparent"
title="Reject"
>
<X className="w-4 h-4" />
<X className="w-3.5 h-3.5" />
</button>
</div>
)}
<button
type="button"
onClick={() => handleDelete(ann.id)}
className="p-1 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer border-none bg-transparent"
className="p-1.5 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors cursor-pointer border-none bg-transparent"
title="Delete"
>
<Trash2 className="w-4 h-4" />
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
{/* Collapsible Content */}
{/* Posted Organization & Author */}
{ann.posted_by_email ? (
<Link
href={`/admin/employers?search=${encodeURIComponent(ann.posted_by_email)}`}
className="flex items-center text-[10px] text-slate-500 font-bold bg-slate-50/60 hover:bg-slate-100/80 hover:border-slate-300 py-1 px-2.5 rounded-lg border border-slate-100 transition-all w-fit cursor-pointer no-underline"
title="Click to view Employer Profile"
>
<Building className="w-3 h-3 mr-1 text-slate-400 shrink-0" />
<span className="truncate max-w-[120px] text-slate-700" title={ann.organization}>{ann.organization}</span>
<span className="mx-1.5 text-slate-300"></span>
<User className="w-3 h-3 mr-1 text-slate-400 shrink-0" />
<span className="truncate max-w-[100px] text-slate-600" title={ann.posted_by}>{ann.posted_by}</span>
</Link>
) : (
<div className="flex items-center text-[10px] text-slate-500 font-bold bg-slate-50/60 py-1 px-2.5 rounded-lg border border-slate-100 w-fit">
<Building className="w-3 h-3 mr-1 text-slate-400 shrink-0" />
<span className="truncate max-w-[120px] text-slate-700" title={ann.organization}>{ann.organization}</span>
<span className="mx-1.5 text-slate-300"></span>
<User className="w-3 h-3 mr-1 text-slate-400 shrink-0" />
<span className="truncate max-w-[100px] text-slate-600" title={ann.posted_by}>{ann.posted_by}</span>
</div>
)}
</div>
{/* Description Content */}
<div className="space-y-2 text-xs">
<p className={`text-slate-600 leading-relaxed ${isExpanded ? '' : 'line-clamp-2'}`}>
<p className={`text-slate-600 leading-relaxed font-medium ${isExpanded ? '' : 'line-clamp-3'}`}>
{ann.content}
</p>
@ -688,56 +721,78 @@ export default function Announcements({ initialAnnouncements }) {
<button
type="button"
onClick={() => toggleExpand(ann.id)}
className="text-[#0F6E56] hover:underline font-bold flex items-center space-x-0.5 border-none bg-transparent cursor-pointer p-0 text-[10px]"
className="text-[#0F6E56] hover:text-[#0b523f] hover:underline font-bold flex items-center space-x-0.5 border-none bg-transparent cursor-pointer p-0 text-[10px] transition-colors"
>
<span>{isExpanded ? 'Show Less' : 'Read Full Description'}</span>
{isExpanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
</button>
)}
{ann.status === 'rejected' && ann.remarks && (
<div className="bg-rose-50 border border-rose-100 rounded-xl p-2.5 text-rose-800 text-[11px] font-semibold mt-1.5 leading-relaxed">
<strong>Remarks:</strong> {ann.remarks}
<div className="bg-rose-50/80 border border-rose-100 rounded-xl p-2.5 text-rose-800 text-[10px] font-semibold mt-1.5 leading-relaxed">
<span className="font-extrabold text-rose-900 block mb-0.5">Rejection Remarks:</span>
{ann.remarks}
</div>
)}
</div>
{/* Details metadata row (compact icons) */}
{/* Metadata Details container (Soft boxes/pills stacked vertically) */}
{details && (
<div className="pt-2.5 border-t border-slate-100 grid grid-cols-3 gap-2 text-[10px] text-slate-500 font-bold">
<div className="flex items-center space-x-1.5 min-w-0">
<Gift className="w-3.5 h-3.5 text-rose-500 fill-rose-50 shrink-0" />
<span className="truncate text-slate-700" title={details.provided_items}>
<div className="pt-3 border-t border-slate-100/80 space-y-2 text-[10px] font-bold">
{/* Provided Items */}
<div className="flex items-center space-x-2.5 bg-rose-50/40 border border-rose-100/50 p-2 rounded-xl">
<div className="w-7 h-7 rounded-lg bg-rose-50 flex items-center justify-center text-rose-500 shrink-0 border border-rose-100">
<Gift className="w-3.5 h-3.5 fill-rose-500" />
</div>
<div className="min-w-0 leading-tight">
<div className="text-[8px] text-rose-400 font-extrabold uppercase tracking-wider">ITEMS PROVIDED</div>
<div className="truncate text-slate-700 font-bold text-xs" title={details.provided_items}>
{details.provided_items}
</span>
</div>
<div className="flex items-center space-x-1.5 min-w-0">
<Calendar className="w-3.5 h-3.5 text-amber-500 shrink-0" />
<span className="truncate text-slate-700" title={`${details.event_date} ${details.event_time}`}>
{details.event_date}
</span>
</div>
<div className="flex items-center space-x-1.5 min-w-0">
<MapPin className="w-3.5 h-3.5 text-blue-500 shrink-0" />
<span className="truncate text-slate-700" title={details.location_details}>
</div>
{/* Date & Time */}
<div className="flex items-center space-x-2.5 bg-amber-50/40 border border-amber-100/50 p-2 rounded-xl">
<div className="w-7 h-7 rounded-lg bg-amber-50 flex items-center justify-center text-amber-500 shrink-0 border border-amber-100">
<Calendar className="w-3.5 h-3.5 text-amber-600" />
</div>
<div className="min-w-0 leading-tight">
<div className="text-[8px] text-amber-500 font-extrabold uppercase tracking-wider">EVENT SCHEDULE</div>
<div className="truncate text-slate-700 font-bold text-xs" title={`${details.event_date} ${details.event_time}`}>
{details.event_date} ({details.event_time})
</div>
</div>
</div>
{/* Location */}
<div className="flex items-center space-x-2.5 bg-blue-50/40 border border-blue-100/50 p-2 rounded-xl">
<div className="w-7 h-7 rounded-lg bg-blue-50 flex items-center justify-center text-blue-500 shrink-0 border border-blue-100">
<MapPin className="w-3.5 h-3.5 text-blue-600" />
</div>
<div className="min-w-0 leading-tight">
<div className="text-[8px] text-blue-400 font-extrabold uppercase tracking-wider">LOCATION</div>
<div className="truncate text-slate-700 font-bold text-xs" title={details.location_details}>
{details.location_details}
</span>
</div>
</div>
</div>
</div>
)}
</div>
{/* Bottom Card Footer */}
<div className="px-4 py-2 bg-slate-50 rounded-b-xl border-t border-slate-100 flex items-center justify-between text-[9px] text-slate-400 font-bold">
<div className="px-5 py-3 bg-slate-50/80 rounded-b-2xl border-t border-slate-100 flex items-center justify-between text-[9px] text-slate-400 font-bold">
<span>Published: {ann.created_at}</span>
{details?.location_pin && (
<a
href={details.location_pin}
target="_blank"
rel="noreferrer"
className="text-[#0F6E56] hover:underline flex items-center space-x-0.5"
className="text-[#0F6E56] hover:text-[#0b523f] hover:underline flex items-center space-x-0.5 bg-white border border-slate-200/80 hover:border-slate-300 py-1 px-2.5 rounded-lg shadow-sm font-extrabold transition-all hover:scale-[1.02] active:scale-[0.98]"
>
<span>View on Map</span>
<MapPin className="w-2.5 h-2.5" />
<MapPin className="w-2.5 h-2.5 text-[#0F6E56]" />
</a>
)}
</div>

View File

@ -100,7 +100,7 @@ export default function Announcements({ initialAnnouncements }) {
const handleCreate = (e) => {
e.preventDefault();
post('/employer/announcements/create', {
post('/employer/events/create', {
preserveScroll: true,
onSuccess: () => {
reset();

View File

@ -193,11 +193,11 @@
return redirect()->back();
})->name('admin.reasons.delete');
Route::get('/announcements', [\App\Http\Controllers\Admin\AdminExtraController::class, 'announcements'])->name('admin.announcements');
Route::post('/announcements/create', [\App\Http\Controllers\Admin\AdminExtraController::class, 'storeAnnouncement'])->name('admin.announcements.store');
Route::post('/announcements/{id}/approve', [\App\Http\Controllers\Admin\AdminExtraController::class, 'approveAnnouncement'])->name('admin.announcements.approve');
Route::post('/announcements/{id}/reject', [\App\Http\Controllers\Admin\AdminExtraController::class, 'rejectAnnouncement'])->name('admin.announcements.reject');
Route::delete('/announcements/{id}', [\App\Http\Controllers\Admin\AdminExtraController::class, 'deleteAnnouncement'])->name('admin.announcements.delete');
Route::get('/events', [\App\Http\Controllers\Admin\AdminExtraController::class, 'announcements'])->name('admin.events');
Route::post('/events/create', [\App\Http\Controllers\Admin\AdminExtraController::class, 'storeAnnouncement'])->name('admin.events.store');
Route::post('/events/{id}/approve', [\App\Http\Controllers\Admin\AdminExtraController::class, 'approveAnnouncement'])->name('admin.events.approve');
Route::post('/events/{id}/reject', [\App\Http\Controllers\Admin\AdminExtraController::class, 'rejectAnnouncement'])->name('admin.events.reject');
Route::delete('/events/{id}', [\App\Http\Controllers\Admin\AdminExtraController::class, 'deleteAnnouncement'])->name('admin.events.delete');
// New Modules Added
Route::get('/safety', [\App\Http\Controllers\Admin\AdminExtraController::class, 'safety'])->name('admin.safety');
@ -329,8 +329,8 @@
Route::get('/candidates', [\App\Http\Controllers\Employer\CandidateController::class, 'index'])->name('employer.candidates');
Route::post('/candidates/{id}/status', [\App\Http\Controllers\Employer\CandidateController::class, 'updateStatus'])->name('employer.candidates.status');
Route::get('/announcements', [\App\Http\Controllers\Employer\AnnouncementController::class, 'index'])->name('employer.announcements');
Route::post('/announcements/create', [\App\Http\Controllers\Employer\AnnouncementController::class, 'store'])->name('employer.announcements.store');
Route::get('/events', [\App\Http\Controllers\Employer\AnnouncementController::class, 'index'])->name('employer.events');
Route::post('/events/create', [\App\Http\Controllers\Employer\AnnouncementController::class, 'store'])->name('employer.events.store');
Route::get('/profile', [\App\Http\Controllers\Employer\ProfileController::class, 'index'])->name('employer.profile');
Route::post('/profile/update', [\App\Http\Controllers\Employer\ProfileController::class, 'update'])->name('employer.profile.update');

View File

@ -288,7 +288,7 @@ public function test_admin_can_reject_announcement_with_remarks()
$response = $this->actingAs($admin)
->withSession(['user' => $admin])
->post('/admin/announcements/' . $announcement->id . '/reject', [
->post('/admin/events/' . $announcement->id . '/reject', [
'remarks' => 'Inappropriate content or location details.',
]);