![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/backups/lavocat.quebec/backup-20250730-021618/src/pages/user/ |
import { useEffect, useState } from 'react';
import type { NextPage } from 'next';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import LayoutWithSidebar from '../../components/LayoutWithSidebar';
import { useRequireRole, USER_ROLES } from '../../lib/auth-utils';
import { format } from 'date-fns';
import RegistrationForm from '@/components/RegistrationForm';
import { motion, AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import { useTheme } from '../../context/ThemeContext';
import RegistrationDetailModal from '../../components/RegistrationDetailModal';
import DocumentViewer from '@/components/DocumentViewer';
import dynamic from 'next/dynamic';
import toast from 'react-hot-toast';
import { useWebSocket } from '../../context/StableWebSocketContext';
import BarreauBadge from '../../components/BarreauBadge';
const PrivateChat = dynamic(() => import('@/components/Chat/PrivateChat'), { ssr: false });
interface Registration {
id: string;
firstName: string;
lastName: string;
email: string;
phone: string;
detaineeInfo: {
name: string;
facility: string;
inmateId: string;
} | null;
status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'MISSING_DOCUMENTS' | 'DOCUMENTS_UNDER_REVIEW' | 'ADDITIONAL_INFO_NEEDED' | 'VERIFICATION_IN_PROGRESS' | 'LAWYER_VERIFICATION' | 'FACILITY_VERIFICATION' | 'DOCUMENTS_EXPIRED' | 'DOCUMENTS_INCOMPLETE' | 'INFORMATION_MISMATCH' | 'PENDING_PAYMENT' | 'PAYMENT_RECEIVED' | 'PENDING_LAWYER_APPROVAL' | 'PENDING_FACILITY_APPROVAL' | 'ON_HOLD' | 'ESCALATED' | 'FINAL_REVIEW' | 'COMPLETED';
createdAt: string;
updatedAt: string;
userId?: string;
createdBy?: string;
creator?: {
id: string;
email: string;
name: string;
};
user?: {
id: string;
email: string;
name: string;
};
documents?: {
id: string;
name: string;
url: string;
type: string;
}[];
reasonForJoining?: string;
urgentNeeds?: string;
}
// Quebec detention facilities list
const QUEBEC_FACILITIES = {
'ABITIBI-TÉMISCAMINGUE': [
{ id: 'amos', name: "Établissement de détention d'Amos" }
],
'BAS-SAINT-LAURENT': [
{ id: 'rimouski', name: "Établissement de détention de Rimouski" }
],
'CAPITALE-NATIONALE': [
{ id: 'quebec-femme', name: 'Établissement de détention de Québec – secteur féminin' },
{ id: 'quebec-homme', name: 'Établissement de détention de Québec – secteur masculin' }
],
'CÔTE-NORD': [
{ id: 'baie-comeau', name: 'Établissement de détention de Baie-Comeau' },
{ id: 'sept-iles', name: 'Établissement de détention de Sept-Îles' }
],
'ESTRIE': [
{ id: 'sherbrooke', name: 'Établissement de détention de Sherbrooke' }
],
'GASPÉSIE-ÎLES-DE-LA-MADELEINE': [
{ id: 'new-carlisle', name: 'Établissement de détention de New Carlisle' },
{ id: 'perce', name: 'Établissement de détention de Percé' },
{ id: 'havre-aubert', name: 'Établissement de détention de Havre-Aubert' }
],
'OUTAOUAIS': [
{ id: 'hull', name: 'Établissement de détention de Hull' }
],
'LAURENTIDES': [
{ id: 'saint-jerome', name: 'Établissement de détention de Saint-Jérôme' }
],
'LAVAL': [
{ id: 'leclerc', name: 'Établissement Leclerc de Laval' }
],
'MAURICIE ET CENTRE-DU-QUÉBEC': [
{ id: 'trois-rivieres', name: 'Établissement de détention de Trois-Rivières' }
],
'MONTEREGIE': [
{ id: 'sorel-tracy', name: 'Établissement de détention de Sorel-Tracy' }
],
'MONTRÉAL': [
{ id: 'bordeaux', name: 'Établissement de détention de Montréal (Bordeaux)' },
{ id: 'riviere-des-prairies', name: 'Établissement de détention de Rivière-des-Prairies' }
],
'SAGUENAY–LAC-SAINT-JEAN': [
{ id: 'roberval', name: 'Établissement de détention de Roberval' }
]
};
function getFacilityName(facilityId: string) {
for (const region of Object.values(QUEBEC_FACILITIES)) {
const facility = region.find(f => f.id === facilityId);
if (facility) return facility.name;
}
return facilityId;
}
// Add status helpers at the top
const getStatusColor = (status: string) => {
switch (status) {
case 'APPROVED':
case 'COMPLETED':
case 'PAYMENT_RECEIVED':
return 'bg-green-100 text-green-800';
case 'REJECTED':
case 'DOCUMENTS_EXPIRED':
case 'INFORMATION_MISMATCH':
case 'ESCALATED':
return 'bg-red-100 text-red-800';
case 'PENDING':
case 'ADDITIONAL_INFO_NEEDED':
case 'PENDING_PAYMENT':
return 'bg-yellow-100 text-yellow-800';
case 'MISSING_DOCUMENTS':
case 'DOCUMENTS_INCOMPLETE':
return 'bg-orange-100 text-orange-800';
case 'DOCUMENTS_UNDER_REVIEW':
case 'PENDING_LAWYER_APPROVAL':
case 'PENDING_FACILITY_APPROVAL':
return 'bg-blue-100 text-blue-800';
case 'VERIFICATION_IN_PROGRESS':
case 'FINAL_REVIEW':
return 'bg-purple-100 text-purple-800';
case 'ON_HOLD':
return 'bg-gray-100 text-gray-800';
case 'WebAd':
return 'bg-pink-100 text-pink-800';
default:
return 'bg-yellow-100 text-yellow-800';
}
};
const getStatusLabel = (status: string) => {
const statusMap: { [key: string]: string } = {
PENDING: 'Pending',
APPROVED: 'Approved',
REJECTED: 'Rejected',
MISSING_DOCUMENTS: 'Missing Documents',
DOCUMENTS_UNDER_REVIEW: 'Documents Under Review',
ADDITIONAL_INFO_NEEDED: 'Additional Info Needed',
VERIFICATION_IN_PROGRESS: 'Verification in Progress',
LAWYER_VERIFICATION: 'Lawyer Verification',
FACILITY_VERIFICATION: 'Facility Verification',
DOCUMENTS_EXPIRED: 'Documents Expired',
DOCUMENTS_INCOMPLETE: 'Documents Incomplete',
INFORMATION_MISMATCH: 'Information Mismatch',
PENDING_PAYMENT: 'Pending Payment',
PAYMENT_RECEIVED: 'Payment Received',
PENDING_LAWYER_APPROVAL: 'Pending Lawyer Approval',
PENDING_FACILITY_APPROVAL: 'Pending Facility Approval',
ON_HOLD: 'On Hold',
ESCALATED: 'Escalated',
FINAL_REVIEW: 'Final Review',
COMPLETED: 'Completed'
};
return statusMap[status] || status;
};
const getStatusEncouragement = (status: string) => {
const encouragementMap: { [key: string]: string } = {
PENDING: 'Your application is being reviewed. We will contact you soon.',
APPROVED: 'Congratulations! Your application has been approved.',
REJECTED: 'We\'re sorry, but your application has been rejected. Please contact us for more information.',
MISSING_DOCUMENTS: 'Please upload the required documents to proceed.',
DOCUMENTS_UNDER_REVIEW: 'Your documents are being reviewed. We will contact you if additional information is needed.',
ADDITIONAL_INFO_NEEDED: 'Please provide the additional information requested.',
VERIFICATION_IN_PROGRESS: 'Your information is being verified. This may take a few days.',
LAWYER_VERIFICATION: 'Your application is being reviewed by our legal team.',
FACILITY_VERIFICATION: 'We are verifying the facility information.',
DOCUMENTS_EXPIRED: 'Some of your documents have expired. Please upload updated versions.',
DOCUMENTS_INCOMPLETE: 'Please complete your document uploads.',
INFORMATION_MISMATCH: 'There is a discrepancy in the information provided. Please review and update.',
PENDING_PAYMENT: 'Please complete the payment process to continue.',
PAYMENT_RECEIVED: 'Payment received. Your application is being processed.',
PENDING_LAWYER_APPROVAL: 'Your application is awaiting lawyer approval.',
PENDING_FACILITY_APPROVAL: 'Your application is awaiting facility approval.',
ON_HOLD: 'Your application is temporarily on hold. We will contact you soon.',
ESCALATED: 'Your application has been escalated for review.',
FINAL_REVIEW: 'Your application is in final review.',
COMPLETED: 'Your application has been completed successfully.'
};
return encouragementMap[status] || '';
};
// Enhanced status icon component with animations for ALL statuses
const StatusIcon = ({ status }: { status: string }) => {
const iconMap: { [key: string]: JSX.Element } = {
// Primary statuses with enhanced animations
APPROVED: (
<div className="relative">
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div className="absolute -inset-1 bg-green-400 rounded-full animate-ping opacity-20"></div>
</div>
),
COMPLETED: (
<div className="relative">
<div className="w-8 h-8 bg-emerald-600 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div className="absolute -inset-1 bg-emerald-500 rounded-full animate-ping opacity-20"></div>
</div>
),
REJECTED: (
<div className="w-8 h-8 bg-red-500 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
),
PENDING: (
<div className="relative">
<div className="animate-spin w-8 h-8 border-2 border-amber-500 border-t-transparent rounded-full"></div>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-3 h-3 bg-amber-500 rounded-full animate-pulse"></div>
</div>
</div>
),
// Document-related statuses
MISSING_DOCUMENTS: <span className="text-orange-500 text-2xl">📄❗</span>,
DOCUMENTS_UNDER_REVIEW: <span className="text-blue-500 text-2xl">📄🔍</span>,
DOCUMENTS_EXPIRED: <span className="text-red-500 text-2xl">📄⏰</span>,
DOCUMENTS_INCOMPLETE: <span className="text-orange-600 text-2xl">📄⚠️</span>,
// Review and verification statuses
ADDITIONAL_INFO_NEEDED: <span className="text-yellow-600 text-2xl">❓📝</span>,
VERIFICATION_IN_PROGRESS: <span className="text-purple-500 text-2xl">🔍✨</span>,
LAWYER_VERIFICATION: <span className="text-indigo-600 text-2xl">⚖️👨💼</span>,
FACILITY_VERIFICATION: <span className="text-blue-600 text-2xl">🏢🔍</span>,
INFORMATION_MISMATCH: <span className="text-red-600 text-2xl">❌📊</span>,
// Payment statuses
PENDING_PAYMENT: <span className="text-yellow-500 text-2xl">💳⏳</span>,
PAYMENT_RECEIVED: <span className="text-green-600 text-2xl">💳✅</span>,
// Approval statuses
PENDING_LAWYER_APPROVAL: <span className="text-indigo-500 text-2xl">⚖️⏳</span>,
PENDING_FACILITY_APPROVAL: <span className="text-blue-400 text-2xl">🏢⏳</span>,
// Process statuses
ON_HOLD: <span className="text-gray-500 text-2xl">⏸️📋</span>,
ESCALATED: <span className="text-red-700 text-2xl">🚨📈</span>,
FINAL_REVIEW: <span className="text-purple-600 text-2xl">🏁🔍</span>,
};
return iconMap[status] || <span className="text-gray-400 text-2xl">📝</span>;
};
// Helper function to determine application type and display info
const getApplicationType = (registration: Registration, currentUserId?: string, currentUserEmail?: string) => {
if (!currentUserId) return { type: 'unknown', label: 'Application', icon: '📝', description: '' };
const isOwner = registration.userId === currentUserId;
const isCreator = registration.createdBy === currentUserId;
const isEmailMatch = registration.email === currentUserEmail && registration.userId === null;
if (isOwner && isCreator) {
return {
type: 'own',
label: 'My Application',
icon: '👤',
description: '',
bgColor: 'bg-blue-50 border-blue-200',
textColor: 'text-blue-800'
};
} else if (isCreator && !isOwner) {
return {
type: 'created-for-other',
label: `Application for ${registration.firstName} ${registration.lastName}`,
icon: '👥',
description: `You created this application for ${registration.email}`,
bgColor: 'bg-purple-50 border-purple-200',
textColor: 'text-purple-800'
};
} else if (isOwner && !isCreator) {
return {
type: 'created-by-other',
label: 'My Application',
icon: '🤝',
description: registration.creator ? `Originally created by ${registration.creator.name || registration.creator.email}` : 'Created by someone else for you',
bgColor: 'bg-green-50 border-green-200',
textColor: 'text-green-800'
};
} else if (isEmailMatch) {
return {
type: 'email-match',
label: 'My Application',
icon: '📧',
description: 'Linked by email match',
bgColor: 'bg-yellow-50 border-yellow-200',
textColor: 'text-yellow-800'
};
}
return {
type: 'unknown',
label: 'Application',
icon: '❓',
description: '',
bgColor: 'bg-gray-50 border-gray-200',
textColor: 'text-gray-800'
};
};
const UserDashboard: NextPage = () => {
const { data: session, status } = useSession();
const router = useRouter();
const { ws, connected, directMessageNotifications, getTotalUnreadDirectMessages } = useWebSocket();
// Role-based access control - allow USER, CLIENT, ADMIN, SUPERADMIN to access user dashboard
const { isAuthorized } = useRequireRole([
USER_ROLES.USER,
USER_ROLES.CLIENT,
USER_ROLES.ADMIN,
USER_ROLES.SUPERADMIN
], '/');
const [registrations, setRegistrations] = useState<Registration[]>([]);
// Debug registrations state changes
useEffect(() => {
console.log('User Dashboard - Registrations state updated:', {
count: registrations.length,
registrations: registrations.map(r => ({ id: r.id, name: `${r.firstName} ${r.lastName}`, status: r.status }))
});
}, [registrations]);
const [selectedRegistration, setSelectedRegistration] = useState<Registration | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [stats, setStats] = useState({
total: 0,
pending: 0,
approved: 0,
rejected: 0
});
const [showNewApplication, setShowNewApplication] = useState(false);
const [deletingId, setDeletingId] = useState<string | null>(null);
const { theme, setTheme } = useTheme();
const [themes, setThemes] = useState<any[]>([]);
const [themesLoading, setThemesLoading] = useState(false);
const [themesError, setThemesError] = useState('');
const [showModal, setShowModal] = useState(false);
const [selectedDocument, setSelectedDocument] = useState<{
url: string;
type: string;
name: string;
} | null>(null);
const [showPrivateChat, setShowPrivateChat] = useState<{ open: boolean; registrationId: string | null }>({ open: false, registrationId: null });
// Mobile responsive states
const [isMobile, setIsMobile] = useState(false);
const [showMobileFilters, setShowMobileFilters] = useState(false);
// Mobile detection
useEffect(() => {
const checkMobile = () => {
const mobile = window.innerWidth < 768;
setIsMobile(mobile);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
// WebSocket status update and message listener
useEffect(() => {
if (!ws || !connected || !session?.user?.id) return;
const handleMessage = (event: MessageEvent) => {
try {
const message = JSON.parse(event.data);
if (message.type === 'STATUS_UPDATE' && message.data.userId === session.user.id) {
const { statusLabel, registrationName, newStatus, registrationId } = message.data;
// Update the registration in state
setRegistrations(prev => prev.map(reg =>
reg.id === registrationId
? { ...reg, status: newStatus, updatedAt: new Date().toISOString() }
: reg
));
// Update stats
fetchRegistrations();
// Show toast notification
toast.success(`📄 Application Status Updated: ${statusLabel}`, {
duration: 5000,
position: 'top-right',
icon: '🔔',
});
// Show browser notification if permission granted
if (Notification.permission === 'granted') {
new Notification('Application Status Updated', {
body: `Your application status has been updated to: ${statusLabel}`,
icon: '/icons/apple-touch-icon-180x180.png'
});
}
console.log(`[UserDashboard] ✅ Received status update: ${statusLabel} for registration ${registrationId}`);
}
// Handle private message notifications when chat is closed
if (message.type === 'PRIVATE_MESSAGE' && message.data.sender.id !== session.user.id) {
const isPrivateChatOpen = showPrivateChat.open && showPrivateChat.registrationId === message.data.registrationId;
if (!isPrivateChatOpen) {
// Show toast notification for new private message
toast((t) => (
<div className="flex items-center space-x-3">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-bold">
{message.data.sender.name?.charAt(0) || 'A'}
</span>
</div>
</div>
<div className="flex-1">
<p className="font-medium text-gray-900">
💬 New message from {message.data.sender.name || 'Admin'}
</p>
<p className="text-sm text-gray-600 truncate max-w-48">
{message.data.content}
</p>
</div>
<button
onClick={() => {
toast.dismiss(t.id);
setShowPrivateChat({ open: true, registrationId: message.data.registrationId });
}}
className="flex-shrink-0 bg-blue-500 text-white px-3 py-1 rounded text-sm hover:bg-blue-600"
>
Reply
</button>
</div>
), {
duration: 6000,
position: 'top-right',
style: {
maxWidth: '400px',
},
});
// Show browser notification
if (Notification.permission === 'granted') {
new Notification(`New message from ${message.data.sender.name || 'Admin'}`, {
body: message.data.content,
icon: '/icons/apple-touch-icon-180x180.png'
});
}
console.log(`[UserDashboard] ✅ Received private message from ${message.data.sender.name}`);
}
}
} catch (error) {
console.error('[UserDashboard] ❌ Error parsing WebSocket message:', error);
}
};
ws.addEventListener('message', handleMessage);
return () => ws.removeEventListener('message', handleMessage);
}, [ws, connected, session?.user?.id, showPrivateChat]);
if (status === 'loading') {
return (
<LayoutWithSidebar>
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
<p className="mt-4 text-gray-600">Loading session...</p>
</div>
</div>
</LayoutWithSidebar>
);
}
// Request notification permission on mount
useEffect(() => {
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission().then(permission => {
console.log('[UserDashboard] Notification permission:', permission);
if (permission === 'granted') {
toast.success('🔔 Notifications enabled! You\'ll receive real-time updates.', {
duration: 4000,
position: 'top-right',
});
}
});
}
}, []);
useEffect(() => {
console.log('User Dashboard - Session status:', status);
console.log('User Dashboard - Session:', session);
console.log('User Dashboard - Session user ID:', session?.user?.id);
console.log('User Dashboard - Is authorized:', isAuthorized);
if (status === 'unauthenticated') {
console.log('User Dashboard - No session, redirecting to login');
router.push('/auth/login');
return;
}
if (status === 'authenticated' && session?.user?.id && isAuthorized) {
console.log('User Dashboard - Session authenticated and authorized, fetching registrations');
fetchRegistrations();
}
}, [session, status, router, isAuthorized]);
const fetchRegistrations = async () => {
try {
console.log('User Dashboard - Starting to fetch registrations');
console.log('User Dashboard - Session user ID before fetch:', session?.user?.id);
setIsLoading(true);
setError(null);
const response = await fetch('/api/user/registrations', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin', // Ensure cookies are sent
});
console.log('User Dashboard - Response status:', response.status);
console.log('User Dashboard - Response headers:', response.headers);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
console.log('User Dashboard - Error response:', errorData);
if (response.status === 401) {
console.log('User Dashboard - Unauthorized, redirecting to login');
router.push('/auth/login');
return;
}
throw new Error(errorData.message || `Failed to fetch registrations (${response.status})`);
}
const data = await response.json();
console.log('User Dashboard - Received data:', data);
console.log('User Dashboard - Data type:', typeof data);
console.log('User Dashboard - Is array:', Array.isArray(data));
console.log('User Dashboard - Data length:', Array.isArray(data) ? data.length : 'N/A');
// Ensure data is an array
if (!Array.isArray(data)) {
console.error('Expected array but got:', data);
setError('Invalid data format received');
setRegistrations([]);
return;
}
console.log('User Dashboard - Setting registrations:', data);
setRegistrations(data);
// Calculate stats
const newStats = {
total: data.length,
pending: data.filter((r: Registration) => r.status === 'PENDING').length,
approved: data.filter((r: Registration) => r.status === 'APPROVED').length,
rejected: data.filter((r: Registration) => r.status === 'REJECTED').length
};
setStats(newStats);
} catch (err) {
console.error('Error fetching registrations:', err);
if (err instanceof Error && err.message.includes('401')) {
router.push('/auth/login');
} else {
setError(`Error loading applications: ${err instanceof Error ? err.message : 'Unknown error'}`);
}
setRegistrations([]);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
console.log('User Dashboard - Session status:', status);
console.log('User Dashboard - Session user:', session?.user);
console.log('User Dashboard - Session user ID:', session?.user?.id);
console.log('User Dashboard - Session user role:', session?.user?.role);
if (status === 'authenticated' && session?.user?.id) {
console.log('User Dashboard - Fetching themes for user:', session.user.id);
setThemesLoading(true);
setThemesError('');
fetch('/api/user/themes', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin', // Ensure cookies are sent
})
.then(res => {
console.log('User Dashboard - Themes response status:', res.status);
if (!res.ok) {
if (res.status === 401) {
console.log('User Dashboard - Themes unauthorized, redirecting to login');
router.push('/auth/login');
return;
}
throw new Error(`Failed to fetch themes (${res.status})`);
}
return res.json();
})
.then(data => {
console.log('User Dashboard - Themes data:', data);
if (Array.isArray(data)) {
setThemes(
data.map((theme: { colors: string | object }) => ({
...theme,
colors: typeof theme.colors === 'string' ? JSON.parse(theme.colors) : theme.colors
}))
);
} else {
setThemes([]);
}
})
.catch(error => {
console.error('Error fetching themes:', error);
if (!error.message.includes('401')) {
setThemesError(`Error loading themes: ${error.message}`);
}
setThemes([]);
})
.finally(() => setThemesLoading(false));
}
}, [session?.user?.id, status, router]);
const handleDeleteRegistration = async (id: string, e: React.MouseEvent) => {
e.stopPropagation(); // Prevent row click
if (!confirm('Are you sure you want to delete this application?')) {
return;
}
setDeletingId(id);
try {
const response = await fetch(`/api/user/registrations/${id}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Error deleting application');
}
// Remove the deleted registration from the state
setRegistrations(prev => prev.filter(reg => reg.id !== id));
// Update stats
const updatedStats = {
total: registrations.length - 1,
pending: registrations.filter(r => r.status === 'PENDING' && r.id !== id).length,
approved: registrations.filter(r => r.status === 'APPROVED' && r.id !== id).length,
rejected: registrations.filter(r => r.status === 'REJECTED' && r.id !== id).length
};
setStats(updatedStats);
toast.success('✅ Application deleted successfully', {
duration: 3000,
position: 'top-right',
});
} catch (err) {
console.error('Error deleting application:', err);
toast.error('❌ Failed to delete application', {
duration: 4000,
position: 'top-right',
});
} finally {
setDeletingId(null);
}
};
const handleApplyTheme = (colors: any) => {
setTheme(colors);
};
const handleDeleteTheme = async (id: string) => {
setDeletingId(id);
try {
await fetch('/api/user/themes', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id }),
});
setThemes(themes.filter(t => t.id !== id));
} catch {
setThemesError('Failed to delete theme');
} finally {
setDeletingId(null);
}
};
// Handler for successful registration
const handleRegistrationSuccess = () => {
setShowNewApplication(false);
fetchRegistrations();
toast.success('🎉 Application submitted successfully! You will receive updates via email and notifications.', {
duration: 5000,
position: 'top-right',
});
};
const handleViewRegistration = (id: string) => {
const reg = registrations.find(r => r.id === id);
if (reg) setSelectedRegistration(reg);
};
if (isLoading) {
return (
<LayoutWithSidebar>
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
</LayoutWithSidebar>
);
}
return (
<LayoutWithSidebar>
<div className="container mx-auto px-4 py-8">
<div className="mb-8">
<div className={`${isMobile ? 'space-y-4' : 'flex justify-between items-center'}`}>
<div>
<div className="flex items-center gap-2 mb-2">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Welcome back, {session?.user?.name}!
</h1>
<BarreauBadge
verificationStatus={session?.user?.verificationStatus}
isVerified={session?.user?.isVerified}
size="sm"
showText={true}
/>
</div>
<h1 className={`font-bold text-gray-900 dark:text-white ${isMobile ? 'text-2xl' : 'text-3xl'}`}>My Applications</h1>
<p className="mt-2 text-gray-600 dark:text-gray-400">Manage your class action applications here.</p>
</div>
<button
onClick={() => setShowNewApplication(true)}
className={`btn-primary ${isMobile ? 'w-full' : ''}`}
>
<span className="flex items-center justify-center">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
New Application
</span>
</button>
</div>
</div>
{/* Stats */}
<div className={`grid gap-4 mb-8 ${isMobile ? 'grid-cols-2' : 'grid-cols-1 md:grid-cols-4'}`}>
<div className={`bg-white dark:bg-gray-800 rounded-lg shadow ${isMobile ? 'p-4' : 'p-6'}`}>
<div className="flex items-center">
<div className="p-2 bg-blue-100 dark:bg-blue-900 rounded-full mr-3">
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div>
<h3 className={`font-semibold text-gray-900 dark:text-white ${isMobile ? 'text-sm' : 'text-lg'}`}>Total</h3>
<p className={`font-bold text-primary ${isMobile ? 'text-2xl' : 'text-3xl'}`}>{stats.total}</p>
</div>
</div>
</div>
<div className={`bg-white dark:bg-gray-800 rounded-lg shadow ${isMobile ? 'p-4' : 'p-6'}`}>
<div className="flex items-center">
<div className="p-2 bg-yellow-100 dark:bg-yellow-900 rounded-full mr-3">
<svg className="w-5 h-5 text-yellow-600 dark:text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h3 className={`font-semibold text-gray-900 dark:text-white ${isMobile ? 'text-sm' : 'text-lg'}`}>Pending</h3>
<p className={`font-bold text-yellow-600 ${isMobile ? 'text-2xl' : 'text-3xl'}`}>{stats.pending}</p>
</div>
</div>
</div>
<div className={`bg-white dark:bg-gray-800 rounded-lg shadow ${isMobile ? 'p-4' : 'p-6'}`}>
<div className="flex items-center">
<div className="p-2 bg-green-100 dark:bg-green-900 rounded-full mr-3">
<svg className="w-5 h-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h3 className={`font-semibold text-gray-900 dark:text-white ${isMobile ? 'text-sm' : 'text-lg'}`}>Approved</h3>
<p className={`font-bold text-green-600 ${isMobile ? 'text-2xl' : 'text-3xl'}`}>{stats.approved}</p>
</div>
</div>
</div>
<div className={`bg-white dark:bg-gray-800 rounded-lg shadow ${isMobile ? 'p-4' : 'p-6'}`}>
<div className="flex items-center">
<div className="p-2 bg-red-100 dark:bg-red-900 rounded-full mr-3">
<svg className="w-5 h-5 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h3 className={`font-semibold text-gray-900 dark:text-white ${isMobile ? 'text-sm' : 'text-lg'}`}>Rejected</h3>
<p className={`font-bold text-red-600 ${isMobile ? 'text-2xl' : 'text-3xl'}`}>{stats.rejected}</p>
</div>
</div>
</div>
</div>
{/* Recent Registrations - Card/Grid Layout */}
<div className="flex items-center justify-between mb-4">
<h2 className={`font-semibold ${isMobile ? 'text-lg' : 'text-xl'}`}>Recent Registrations</h2>
{registrations.length > 0 && (
<span className="text-sm text-gray-500 dark:text-gray-400">
{registrations.length} {registrations.length === 1 ? 'application' : 'applications'}
</span>
)}
</div>
{registrations.length === 0 ? (
<div className="text-center py-12">
<div className="bg-gray-100 dark:bg-gray-800 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No Applications Yet</h3>
<p className="text-gray-500 dark:text-gray-400 mb-4">Get started by creating your first application.</p>
<button
onClick={() => setShowNewApplication(true)}
className="btn-primary"
>
Create Application
</button>
</div>
) : (
<div className={`grid gap-6 ${isMobile ? 'grid-cols-1' : 'grid-cols-1 lg:grid-cols-2'}`}>
{registrations.map(registration => {
const appType = getApplicationType(registration, session?.user?.id, session?.user?.email);
return (
<div key={registration.id} className={`bg-white rounded-2xl shadow-xl flex flex-col border-2 ${isMobile ? 'p-4 min-h-[320px]' : 'p-8 min-h-[380px]'} ${appType.bgColor || 'border-primary/10'}`}>
{/* Application Type Header */}
<div className={`mb-3 px-3 py-2 rounded-lg ${appType.bgColor} border`}>
<div className="flex items-center">
<span className="text-lg mr-2">{appType.icon}</span>
<span className={`font-semibold ${appType.textColor}`}>{appType.label}</span>
</div>
{appType.description && (
<p className={`text-sm mt-1 ${appType.textColor} opacity-80`}>{appType.description}</p>
)}
</div>
{/* Enhanced Status Display for ALL statuses */}
<div className="mb-4">
{/* Special enhanced display for primary statuses */}
{registration.status === 'PENDING' ? (
<div className="bg-gradient-to-r from-amber-100 to-orange-100 border-2 border-amber-300 rounded-xl p-3 shadow-sm">
<div className="flex items-center space-x-3">
<StatusIcon status={registration.status} />
<div>
<div className="font-bold text-amber-800 text-sm">⏳ Under Review</div>
<div className="text-amber-700 text-xs">Application being processed</div>
</div>
</div>
<div className="mt-2 bg-amber-200 rounded-full h-1.5 overflow-hidden">
<div className="bg-gradient-to-r from-amber-400 to-orange-500 h-full rounded-full animate-pulse" style={{ width: '45%' }}></div>
</div>
</div>
) : registration.status === 'APPROVED' || registration.status === 'COMPLETED' ? (
<div className="bg-gradient-to-r from-green-100 to-emerald-100 border-2 border-green-300 rounded-xl p-3 shadow-sm">
<div className="flex items-center space-x-3">
<StatusIcon status={registration.status} />
<div>
<div className="font-bold text-green-800 text-sm">
{registration.status === 'COMPLETED' ? '🎉 Completed' : '✅ Approved'}
</div>
<div className="text-green-700 text-xs">
{registration.status === 'COMPLETED' ? 'Process finished successfully!' : 'Welcome to the class action!'}
</div>
</div>
</div>
<div className="mt-2 bg-green-200 rounded-full h-1.5 overflow-hidden">
<div className="bg-gradient-to-r from-green-400 to-emerald-500 h-full rounded-full" style={{ width: '100%' }}></div>
</div>
</div>
) : registration.status === 'REJECTED' ? (
<div className="bg-gradient-to-r from-red-100 to-pink-100 border-2 border-red-300 rounded-xl p-3 shadow-sm">
<div className="flex items-center space-x-3">
<StatusIcon status={registration.status} />
<div>
<div className="font-bold text-red-800 text-sm">❌ Declined</div>
<div className="text-red-700 text-xs">Please check feedback</div>
</div>
</div>
</div>
) : (
/* Standard display for all other statuses */
<div className={`rounded-xl p-3 shadow-sm border-2 ${
registration.status.includes('DOCUMENTS') || registration.status === 'MISSING_DOCUMENTS' ? 'bg-orange-50 border-orange-200' :
registration.status.includes('VERIFICATION') || registration.status.includes('REVIEW') ? 'bg-purple-50 border-purple-200' :
registration.status.includes('PAYMENT') ? 'bg-yellow-50 border-yellow-200' :
registration.status.includes('APPROVAL') ? 'bg-blue-50 border-blue-200' :
registration.status === 'ON_HOLD' ? 'bg-gray-50 border-gray-200' :
registration.status === 'ESCALATED' ? 'bg-red-50 border-red-200' :
'bg-gray-50 border-gray-200'
}`}>
<div className="flex items-center space-x-3">
<StatusIcon status={registration.status} />
<div className="flex-1">
<div className={`font-bold text-sm ${
registration.status.includes('DOCUMENTS') || registration.status === 'MISSING_DOCUMENTS' ? 'text-orange-800' :
registration.status.includes('VERIFICATION') || registration.status.includes('REVIEW') ? 'text-purple-800' :
registration.status.includes('PAYMENT') ? 'text-yellow-800' :
registration.status.includes('APPROVAL') ? 'text-blue-800' :
registration.status === 'ON_HOLD' ? 'text-gray-800' :
registration.status === 'ESCALATED' ? 'text-red-800' :
'text-gray-800'
}`}>
{getStatusLabel(registration.status)}
</div>
<div className={`text-xs ${
registration.status.includes('DOCUMENTS') || registration.status === 'MISSING_DOCUMENTS' ? 'text-orange-700' :
registration.status.includes('VERIFICATION') || registration.status.includes('REVIEW') ? 'text-purple-700' :
registration.status.includes('PAYMENT') ? 'text-yellow-700' :
registration.status.includes('APPROVAL') ? 'text-blue-700' :
registration.status === 'ON_HOLD' ? 'text-gray-700' :
registration.status === 'ESCALATED' ? 'text-red-700' :
'text-gray-700'
}`}>
{getStatusEncouragement(registration.status)}
</div>
</div>
</div>
</div>
)}
</div>
{/* Main Info */}
<div className="mb-2">
<div className={`font-extrabold ${isMobile ? 'text-xl' : 'text-2xl'}`}>{registration.firstName} {registration.lastName}</div>
<div className="text-sm text-gray-500">{registration.email}</div>
</div>
<div className="mb-2">
<span className="font-semibold">Detainee:</span> {registration.detaineeInfo?.name || '-'}
</div>
<div className="text-sm text-gray-500 mb-2">
{getFacilityName(registration.detaineeInfo?.facility || '') || '-'}
</div>
{/* Extra Info */}
<div className="mb-4">
<div className="text-xs text-gray-400">{format(new Date(registration.createdAt), 'MMM d, yyyy')}</div>
{registration.reasonForJoining && (
<div className="mt-2 text-gray-700 italic">"{registration.reasonForJoining}"</div>
)}
{registration.urgentNeeds && (
<div className="mt-1 text-pink-600 font-medium">{registration.urgentNeeds}</div>
)}
</div>
{/* Documents */}
{registration.documents && registration.documents.length > 0 && (
<div className="mb-4">
<h4 className="text-sm font-semibold mb-2">Documents</h4>
<div className="space-y-2">
{registration.documents.map((doc) => (
<a
key={doc.id}
href={`/user/registrations/${registration.id}`}
className="flex items-center text-blue-600 hover:text-blue-800 underline"
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
{doc.name}
</a>
))}
</div>
</div>
)}
{/* Encouragement */}
<div className="mb-4 text-primary font-semibold">
{getStatusEncouragement(registration.status)}
</div>
{/* Actions */}
<div className={`mt-auto pt-4 ${isMobile ? 'space-y-2' : 'grid grid-cols-3 gap-2'}`}>
<button
onClick={() => router.push(`/user/applications/${registration.id}`)}
className={`font-semibold text-sm px-4 py-2.5 rounded-lg flex items-center justify-center transition-all duration-200 shadow-md hover:shadow-lg transform hover:scale-105 text-white ${isMobile ? 'w-full' : ''}`}
style={{
background: `linear-gradient(135deg, #8b5cf6, #7c3aed)`,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = `linear-gradient(135deg, #7c3aed, #8b5cf6)`;
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = `linear-gradient(135deg, #8b5cf6, #7c3aed)`;
}}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Edit
</button>
<button
onClick={() => setShowPrivateChat({ open: true, registrationId: registration.id })}
className={`font-semibold text-sm px-4 py-2.5 rounded-lg flex items-center justify-center transition-all duration-200 shadow-md hover:shadow-lg transform hover:scale-105 text-white ${isMobile ? 'w-full' : ''}`}
style={{
background: `linear-gradient(135deg, #3b82f6, #2563eb)`,
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = `linear-gradient(135deg, #2563eb, #3b82f6)`;
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = `linear-gradient(135deg, #3b82f6, #2563eb)`;
}}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
Chat
</button>
<button
onClick={e => handleDeleteRegistration(registration.id, e)}
className={`bg-gradient-to-r from-red-500 to-red-600 hover:from-red-600 hover:to-red-700 text-white font-semibold text-sm px-4 py-2.5 rounded-lg flex items-center justify-center transition-all duration-200 shadow-md hover:shadow-lg transform hover:scale-105 ${isMobile ? 'w-full' : ''}`}
>
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Delete
</button>
</div>
</div>
);
})}
</div>
)}
{/* Document Viewer Modal */}
<AnimatePresence>
{selectedDocument && (
<></>
)}
</AnimatePresence>
{/* New Application Form */}
<AnimatePresence>
{showNewApplication && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4"
>
<motion.div
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
className={`rounded-xl shadow-xl w-full overflow-y-auto bg-white ${isMobile ? 'max-w-full h-full max-h-screen' : 'max-w-7xl max-h-[90vh]'}`}
>
<div className={`${isMobile ? 'p-4' : 'p-6'}`}>
<div className="flex justify-between items-center mb-6">
<h2 className={`font-bold ${isMobile ? 'text-xl' : 'text-2xl'}`}>Application Form</h2>
<button
onClick={() => setShowNewApplication(false)}
className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg"
>
{isMobile ? (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
'Close'
)}
</button>
</div>
<RegistrationForm
isFrench={router.locale === 'fr'}
initialLocale={router.locale || 'en'}
onSuccess={handleRegistrationSuccess}
isAdmin={false}
/>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{showPrivateChat.open && showPrivateChat.registrationId && (
<PrivateChat
registrationId={showPrivateChat.registrationId}
onClose={() => setShowPrivateChat({ open: false, registrationId: null })}
/>
)}
</AnimatePresence>
</div>
</LayoutWithSidebar>
);
};
export default UserDashboard;