Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 155 additions & 74 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ThemeProvider } from './context/ThemeContext';
import { AuthProvider } from './context/AuthContext';
import { SocketProvider } from './context/SocketContext';
Expand All @@ -9,6 +10,17 @@ import { IdleTimeoutModal } from './components/IdleTimeoutModal';
import { ProtectedRoute } from './components/ProtectedRoute';
import { SkeletonCard, SkeletonText } from './components/ui/Skeleton';

// Initialize TanStack Query Client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 1,
staleTime: 30000,
},
},
});

// Route-level code splitting
const Login = lazy(() => import('./pages/Login'));
const Signup = lazy(() => import('./pages/Signup'));
Expand All @@ -20,6 +32,15 @@ const MyComplaints = lazy(() => import('./pages/MyComplaints'));
const AssistantDashboard = lazy(() => import('./pages/AssistantDashboard'));
const Profile = lazy(() => import('./pages/Profile'));

// Admin Module routes
const AdminDashboard = lazy(() => import('./pages/admin/AdminDashboard'));
const AdminUsers = lazy(() => import('./pages/admin/AdminUsers'));
const AdminDepartments = lazy(() => import('./pages/admin/AdminDepartments'));
const AdminLabs = lazy(() => import('./pages/admin/AdminLabs'));
const AdminComputers = lazy(() => import('./pages/admin/AdminComputers'));
const AdminSettings = lazy(() => import('./pages/admin/AdminSettings'));
const AdminAudit = lazy(() => import('./pages/admin/AdminAudit'));

// Dev-only lazy loaded design system preview
const DesignSystemPreview = import.meta.env.DEV
? lazy(() => import('./pages/DesignSystemPreview'))
Expand All @@ -40,80 +61,140 @@ const PageLoaderFallback: React.FC = () => (

export const App: React.FC = () => {
return (
<Router future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<ThemeProvider>
<AuthProvider>
<SocketProvider>
<ToastProvider>
<AppShell>
<IdleTimeoutModal />

<Suspense fallback={<PageLoaderFallback />}>
<Routes>
{/* Public Authentication */}
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/complete-profile" element={<CompleteProfile />} />
<Route path="/pending-approval" element={<PendingApproval />} />

{/* Dev-only Design System Preview (excluded in production) */}
{import.meta.env.DEV && DesignSystemPreview && (
<Route path="/design" element={<DesignSystemPreview />} />
)}

{/* Root Redirection */}
<Route path="/" element={<Navigate to="/report" replace />} />

{/* Protected Student / Universal Routes */}
<Route
path="/report"
element={
<ProtectedRoute>
<ReportComplaint />
</ProtectedRoute>
}
/>

<Route
path="/my-complaints"
element={
<ProtectedRoute>
<MyComplaints />
</ProtectedRoute>
}
/>

{/* Protected Assistant Routes */}
<Route
path="/assistant"
element={
<ProtectedRoute allowedRoles={['LAB_ASSISTANT', 'ADMIN', 'DEPT_AUTHORITY', 'HOD']}>
<AssistantDashboard />
</ProtectedRoute>
}
/>

{/* Protected Profile Route for all authenticated roles */}
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>

{/* Fallback */}
<Route path="*" element={<Navigate to="/report" replace />} />
</Routes>
</Suspense>
</AppShell>
</ToastProvider>
</SocketProvider>
</AuthProvider>
</ThemeProvider>
</Router>
<QueryClientProvider client={queryClient}>
<Router future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<ThemeProvider>
<AuthProvider>
<SocketProvider>
<ToastProvider>
<AppShell>
<IdleTimeoutModal />

<Suspense fallback={<PageLoaderFallback />}>
<Routes>
{/* Public Authentication */}
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/verify-email" element={<VerifyEmail />} />
<Route path="/complete-profile" element={<CompleteProfile />} />
<Route path="/pending-approval" element={<PendingApproval />} />

{/* Dev-only Design System Preview (excluded in production) */}
{import.meta.env.DEV && DesignSystemPreview && (
<Route path="/design" element={<DesignSystemPreview />} />
)}

{/* Root Redirection */}
<Route path="/" element={<Navigate to="/report" replace />} />

{/* Protected Student / Universal Routes */}
<Route
path="/report"
element={
<ProtectedRoute>
<ReportComplaint />
</ProtectedRoute>
}
/>

<Route
path="/my-complaints"
element={
<ProtectedRoute>
<MyComplaints />
</ProtectedRoute>
}
/>

{/* Protected Assistant Routes */}
<Route
path="/assistant"
element={
<ProtectedRoute allowedRoles={['LAB_ASSISTANT', 'ADMIN', 'DEPT_AUTHORITY', 'HOD']}>
<AssistantDashboard />
</ProtectedRoute>
}
/>

{/* Protected Profile Route for all authenticated roles */}
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>

{/* Protected Admin Module Routes (Strictly ADMIN only) */}
<Route
path="/admin"
element={
<ProtectedRoute allowedRoles={['ADMIN']}>
<AdminDashboard />
</ProtectedRoute>
}
/>
<Route
path="/admin/users"
element={
<ProtectedRoute allowedRoles={['ADMIN']}>
<AdminUsers />
</ProtectedRoute>
}
/>
<Route
path="/admin/departments"
element={
<ProtectedRoute allowedRoles={['ADMIN']}>
<AdminDepartments />
</ProtectedRoute>
}
/>
<Route
path="/admin/labs"
element={
<ProtectedRoute allowedRoles={['ADMIN']}>
<AdminLabs />
</ProtectedRoute>
}
/>
<Route
path="/admin/computers"
element={
<ProtectedRoute allowedRoles={['ADMIN']}>
<AdminComputers />
</ProtectedRoute>
}
/>
<Route
path="/admin/settings"
element={
<ProtectedRoute allowedRoles={['ADMIN']}>
<AdminSettings />
</ProtectedRoute>
}
/>
<Route
path="/admin/audit"
element={
<ProtectedRoute allowedRoles={['ADMIN']}>
<AdminAudit />
</ProtectedRoute>
}
/>

{/* Fallback */}
<Route path="*" element={<Navigate to="/report" replace />} />
</Routes>
</Suspense>
</AppShell>
</ToastProvider>
</SocketProvider>
</AuthProvider>
</ThemeProvider>
</Router>
</QueryClientProvider>
);
};

Expand Down
14 changes: 7 additions & 7 deletions client/src/components/TicketTimelineModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,11 @@ export const TicketTimelineModal: React.FC<TicketTimelineModalProps> = ({
<span className="text-slate-600 dark:text-slate-400 font-medium">Assigned Assistant:</span>
<button
type="button"
onClick={() =>
setSelectedAssistantId(
(ticket.assignedTo as any)?._id || (ticket.assignedTo as any)?.id
)
}
onClick={() => {
if (ticket.assignedTo) {
setSelectedAssistantId(ticket.assignedTo._id);
}
}}
className="font-bold text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1.5 focus:outline-none"
>
<span>{ticket.assignedTo.name}</span>
Expand Down Expand Up @@ -226,10 +226,10 @@ export const TicketTimelineModal: React.FC<TicketTimelineModalProps> = ({
<div className="flex-1 space-y-0.5">
<div className="flex justify-between items-center">
<span className="font-semibold text-slate-900 dark:text-slate-100">
{entry.actor.role === 'LAB_ASSISTANT' && (entry.actor as any).id ? (
{entry.actor.role === 'LAB_ASSISTANT' && entry.actor.id ? (
<button
type="button"
onClick={() => setSelectedAssistantId((entry.actor as any).id)}
onClick={() => setSelectedAssistantId(entry.actor.id)}
className="text-blue-600 dark:text-blue-400 hover:underline inline-flex items-center gap-1 focus:outline-none"
>
<span>{entry.actor.name}</span>
Expand Down
30 changes: 29 additions & 1 deletion client/src/components/layout/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ import {
Sun,
Moon,
LucideIcon,
LayoutDashboard,
Users,
Building2,
FlaskConical,
Monitor,
Settings,
ScrollText,
} from 'lucide-react';

interface NavItem {
Expand Down Expand Up @@ -87,6 +94,20 @@ export const AppShell: React.FC<{ children: React.ReactNode }> = ({ children })
title = 'Fixify | Pending Approval';
} else if (path === '/design') {
title = 'Fixify | Design System Preview';
} else if (path === '/admin') {
title = 'Fixify | Admin - Dashboard';
} else if (path.startsWith('/admin/users')) {
title = 'Fixify | Admin - Users';
} else if (path.startsWith('/admin/departments')) {
title = 'Fixify | Admin - Departments';
} else if (path.startsWith('/admin/labs')) {
title = 'Fixify | Admin - Laboratories';
} else if (path.startsWith('/admin/computers')) {
title = 'Fixify | Admin - Computers';
} else if (path.startsWith('/admin/settings')) {
title = 'Fixify | Admin - Settings';
} else if (path.startsWith('/admin/audit')) {
title = 'Fixify | Admin - Audit Log';
}
document.title = title;
}, [location.pathname]);
Expand Down Expand Up @@ -169,7 +190,14 @@ export const AppShell: React.FC<{ children: React.ReactNode }> = ({ children })

case 'ADMIN':
items.push(
{ label: 'Admin Work Orders', href: '/assistant', icon: Wrench, testId: 'nav-admin' },
{ label: 'Dashboard', href: '/admin', icon: LayoutDashboard, testId: 'nav-admin-dashboard' },
{ label: 'Users', href: '/admin/users', icon: Users, testId: 'nav-admin-users' },
{ label: 'Departments', href: '/admin/departments', icon: Building2, testId: 'nav-admin-departments' },
{ label: 'Laboratories', href: '/admin/labs', icon: FlaskConical, testId: 'nav-admin-labs' },
{ label: 'Computers', href: '/admin/computers', icon: Monitor, testId: 'nav-admin-computers' },
{ label: 'Work Orders', href: '/assistant', icon: Wrench, testId: 'nav-assistant' },
{ label: 'Settings', href: '/admin/settings', icon: Settings, testId: 'nav-admin-settings' },
{ label: 'Audit Log', href: '/admin/audit', icon: ScrollText, testId: 'nav-admin-audit' },
{ label: 'Report Issue', href: '/report', icon: PlusCircle, testId: 'nav-report' },
{ label: 'My Complaints', href: '/my-complaints', icon: Clock, testId: 'nav-my-complaints' }
);
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/profile/ProfileForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface ProfileFormProps {
onSuccess?: (updated: User) => void;
// For 'complete' mode
redirectTarget?: string;
onCompleteSubmit?: (payload: any) => Promise<void>;
onCompleteSubmit?: (payload: Record<string, unknown>) => Promise<void>;
isLoadingExternal?: boolean;
}

Expand Down Expand Up @@ -163,7 +163,7 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
// 'edit' mode: call PATCH /profile/me
try {
setIsSubmitting(true);
const payload: Record<string, any> = {
const payload: Record<string, unknown> = {
firstName: firstName.trim(),
lastName: lastName.trim(),
phone: phone.trim(),
Expand Down Expand Up @@ -526,7 +526,7 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({
</label>
{user.assignedLabs && Array.isArray(user.assignedLabs) && user.assignedLabs.length > 0 ? (
<div className="flex flex-wrap gap-2">
{user.assignedLabs.map((lab: any) => (
{user.assignedLabs.map((lab) => (
<div
key={typeof lab === 'string' ? lab : lab._id}
className="px-3 py-1.5 rounded-xl bg-card border border-border text-xs font-semibold flex items-center gap-2 text-foreground shadow-xs"
Expand Down
7 changes: 4 additions & 3 deletions client/src/pages/CompleteProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
QrCode,
ShieldCheck,
} from 'lucide-react';
import { User } from '../types';
import { useAuth } from '../context/AuthContext';
import { useTheme } from '../context/ThemeContext';
import { Button } from '../components/ui/Button';
Expand All @@ -26,11 +27,11 @@ export const CompleteProfile: React.FC = () => {
const [isLoading, setIsLoading] = useState(false);
const [serverError, setServerError] = useState<string | null>(null);

const handleCompleteSubmit = async (payload: any) => {
const handleCompleteSubmit = async (payload: Record<string, unknown>) => {
try {
setIsLoading(true);
setServerError(null);
const result = await registerProfile(payload);
const result = await registerProfile(payload as unknown as Parameters<typeof registerProfile>[0]);
await refreshUser();

if (result.isPendingApproval) {
Expand Down Expand Up @@ -147,7 +148,7 @@ export const CompleteProfile: React.FC = () => {
{/* Reusable Institutional Profile Form */}
<ProfileForm
mode="complete"
user={user || ({} as any)}
user={user || ({} as User)}
redirectTarget={redirectTarget}
onCompleteSubmit={handleCompleteSubmit}
isLoadingExternal={isLoading}
Expand Down
Loading
Loading