Skip to content

Commit 10e9271

Browse files
committed
feat: class detail page shell with roster and enrollment history tabs
Adds the refactored class detail page with the new shell layout: breadcrumbs, ClassHeader with stat cards, header actions (edit, take attendance, delete), and the first two tabs. Roster tab: resident list with search/filter, enroll/unenroll modals, change enrollment status, and bulk graduate functionality. Enrollment History tab: historical enrollment records with filtering. Also updates ClassesPage (adds TakeAttendanceModal integration) and program-routes (adds class detail route).
1 parent 5807ed9 commit 10e9271

14 files changed

+2836
-615
lines changed

frontend-v2/src/pages/ClassesPage.tsx

Lines changed: 255 additions & 99 deletions
Large diffs are not rendered by default.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useState } from 'react';
2+
import { CheckCircle } from 'lucide-react';
3+
import { Button } from '@/components/ui/button';
4+
import {
5+
Dialog,
6+
DialogContent,
7+
DialogDescription,
8+
DialogHeader,
9+
DialogTitle
10+
} from '@/components/ui/dialog';
11+
12+
interface BulkGraduateModalProps {
13+
open: boolean;
14+
onClose: () => void;
15+
count: number;
16+
onConfirm: () => Promise<void>;
17+
}
18+
19+
export function BulkGraduateModal({
20+
open,
21+
onClose,
22+
count,
23+
onConfirm
24+
}: BulkGraduateModalProps) {
25+
const [isSubmitting, setIsSubmitting] = useState(false);
26+
27+
const handleConfirm = async () => {
28+
setIsSubmitting(true);
29+
await onConfirm();
30+
setIsSubmitting(false);
31+
onClose();
32+
};
33+
34+
return (
35+
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
36+
<DialogContent className="max-w-md">
37+
<DialogHeader>
38+
<DialogTitle className="text-[#203622]">
39+
Graduate Residents
40+
</DialogTitle>
41+
<DialogDescription>
42+
Are you sure you want to graduate {count}{' '}
43+
{count === 1 ? 'resident' : 'residents'}?
44+
</DialogDescription>
45+
</DialogHeader>
46+
<div className="bg-green-50 border border-green-200 rounded-lg p-4 my-2">
47+
<p className="text-sm text-green-800">
48+
This will update the enrollment status to
49+
&quot;Completed&quot; for all selected residents.
50+
</p>
51+
</div>
52+
<div className="flex justify-end gap-3 pt-2">
53+
<Button
54+
variant="outline"
55+
onClick={onClose}
56+
disabled={isSubmitting}
57+
>
58+
Cancel
59+
</Button>
60+
<Button
61+
onClick={() => {
62+
void handleConfirm();
63+
}}
64+
disabled={isSubmitting}
65+
className="bg-[#556830] hover:bg-[#203622]"
66+
>
67+
<CheckCircle className="size-4 mr-2" />
68+
{isSubmitting
69+
? 'Graduating...'
70+
: `Graduate ${count} ${count === 1 ? 'Resident' : 'Residents'}`}
71+
</Button>
72+
</div>
73+
</DialogContent>
74+
</Dialog>
75+
);
76+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { useState, useEffect } from 'react';
2+
import { Button } from '@/components/ui/button';
3+
import { Label } from '@/components/ui/label';
4+
import {
5+
Dialog,
6+
DialogContent,
7+
DialogDescription,
8+
DialogHeader,
9+
DialogTitle
10+
} from '@/components/ui/dialog';
11+
import {
12+
Select,
13+
SelectContent,
14+
SelectItem,
15+
SelectTrigger,
16+
SelectValue
17+
} from '@/components/ui/select';
18+
import { SelectedClassStatus } from '@/types/attendance';
19+
import API from '@/api/api';
20+
import { toast } from 'sonner';
21+
22+
interface ChangeClassStatusModalProps {
23+
open: boolean;
24+
onClose: () => void;
25+
classId: number;
26+
programId: number;
27+
className: string;
28+
currentStatus: SelectedClassStatus;
29+
capacity: number;
30+
onStatusChanged: () => void;
31+
}
32+
33+
const STATUS_TRANSITIONS: Record<string, SelectedClassStatus[]> = {
34+
[SelectedClassStatus.Scheduled]: [
35+
SelectedClassStatus.Active,
36+
SelectedClassStatus.Cancelled
37+
],
38+
[SelectedClassStatus.Active]: [
39+
SelectedClassStatus.Paused,
40+
SelectedClassStatus.Completed
41+
],
42+
[SelectedClassStatus.Paused]: [
43+
SelectedClassStatus.Active,
44+
SelectedClassStatus.Cancelled
45+
]
46+
};
47+
48+
export function ChangeClassStatusModal({
49+
open,
50+
onClose,
51+
classId,
52+
programId,
53+
className,
54+
currentStatus,
55+
capacity,
56+
onStatusChanged
57+
}: ChangeClassStatusModalProps) {
58+
const [newStatus, setNewStatus] = useState<SelectedClassStatus>(currentStatus);
59+
const [isSubmitting, setIsSubmitting] = useState(false);
60+
61+
const allowedStatuses = STATUS_TRANSITIONS[currentStatus] ?? [];
62+
63+
useEffect(() => {
64+
if (open) {
65+
setNewStatus(currentStatus);
66+
}
67+
}, [open, currentStatus]);
68+
69+
const handleSubmit = async () => {
70+
setIsSubmitting(true);
71+
const resp = await API.patch<
72+
unknown,
73+
{ status: string; capacity: number }
74+
>(`programs/${programId}/classes/${classId}`, {
75+
status: newStatus,
76+
capacity
77+
});
78+
if (resp.success) {
79+
toast.success(`Class status updated to ${newStatus}`);
80+
onClose();
81+
onStatusChanged();
82+
} else {
83+
toast.error(resp.message || 'Failed to update status');
84+
}
85+
setIsSubmitting(false);
86+
};
87+
88+
return (
89+
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
90+
<DialogContent className="max-w-md">
91+
<DialogHeader>
92+
<DialogTitle className="text-[#203622]">
93+
Change Class Status
94+
</DialogTitle>
95+
<DialogDescription>
96+
Update the status for {className}
97+
</DialogDescription>
98+
</DialogHeader>
99+
<div className="space-y-4">
100+
<div>
101+
<Label htmlFor="classStatus">New Status</Label>
102+
<Select
103+
value={newStatus}
104+
onValueChange={(v) =>
105+
setNewStatus(v as SelectedClassStatus)
106+
}
107+
>
108+
<SelectTrigger id="classStatus" className="mt-1">
109+
<SelectValue />
110+
</SelectTrigger>
111+
<SelectContent>
112+
<SelectItem value={currentStatus}>
113+
{currentStatus} (current)
114+
</SelectItem>
115+
{allowedStatuses.map((s) => (
116+
<SelectItem key={s} value={s}>
117+
{s}
118+
</SelectItem>
119+
))}
120+
</SelectContent>
121+
</Select>
122+
</div>
123+
<div className="flex gap-2 justify-end pt-4">
124+
<Button
125+
variant="outline"
126+
onClick={onClose}
127+
disabled={isSubmitting}
128+
>
129+
Cancel
130+
</Button>
131+
<Button
132+
onClick={() => {
133+
void handleSubmit();
134+
}}
135+
disabled={
136+
newStatus === currentStatus || isSubmitting
137+
}
138+
className="bg-[#556830] hover:bg-[#203622]"
139+
>
140+
{isSubmitting ? 'Updating...' : 'Update Status'}
141+
</Button>
142+
</div>
143+
</div>
144+
</DialogContent>
145+
</Dialog>
146+
);
147+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { useState, useEffect } from 'react';
2+
import { Button } from '@/components/ui/button';
3+
import { Label } from '@/components/ui/label';
4+
import { Textarea } from '@/components/ui/textarea';
5+
import {
6+
Dialog,
7+
DialogContent,
8+
DialogDescription,
9+
DialogHeader,
10+
DialogTitle
11+
} from '@/components/ui/dialog';
12+
import {
13+
Select,
14+
SelectContent,
15+
SelectItem,
16+
SelectTrigger,
17+
SelectValue
18+
} from '@/components/ui/select';
19+
import { EnrollmentStatus } from '@/types/attendance';
20+
21+
interface ChangeEnrollmentStatusModalProps {
22+
open: boolean;
23+
onClose: () => void;
24+
residentDisplayId: string;
25+
residentName: string;
26+
currentStatus: EnrollmentStatus;
27+
allowedStatuses: EnrollmentStatus[];
28+
onStatusChange: (newStatus: EnrollmentStatus, reason: string) => void;
29+
}
30+
31+
const NEEDS_REASON_STATUSES = new Set([
32+
EnrollmentStatus.Withdrawn,
33+
EnrollmentStatus.Dropped,
34+
EnrollmentStatus.Segregated,
35+
EnrollmentStatus['Failed To Complete'],
36+
EnrollmentStatus.Transfered,
37+
EnrollmentStatus.Cancelled
38+
]);
39+
40+
export function ChangeEnrollmentStatusModal({
41+
open,
42+
onClose,
43+
residentDisplayId,
44+
residentName,
45+
currentStatus,
46+
allowedStatuses,
47+
onStatusChange
48+
}: ChangeEnrollmentStatusModalProps) {
49+
const [newStatus, setNewStatus] = useState<EnrollmentStatus>(currentStatus);
50+
const [reason, setReason] = useState('');
51+
52+
useEffect(() => {
53+
if (open) {
54+
setNewStatus(currentStatus);
55+
setReason('');
56+
}
57+
}, [open, currentStatus]);
58+
59+
const needsReason = NEEDS_REASON_STATUSES.has(newStatus);
60+
const canSubmit =
61+
newStatus !== currentStatus && (!needsReason || reason.trim().length > 0);
62+
63+
const handleSubmit = () => {
64+
onStatusChange(newStatus, reason);
65+
onClose();
66+
};
67+
68+
return (
69+
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
70+
<DialogContent className="max-w-md">
71+
<DialogHeader>
72+
<DialogTitle>Change Enrollment Status</DialogTitle>
73+
<DialogDescription>
74+
Update enrollment status for {residentDisplayId} -{' '}
75+
{residentName}
76+
</DialogDescription>
77+
</DialogHeader>
78+
<div className="space-y-4">
79+
<div>
80+
<Label htmlFor="status">New Status</Label>
81+
<Select
82+
value={newStatus}
83+
onValueChange={(value) =>
84+
setNewStatus(value as EnrollmentStatus)
85+
}
86+
>
87+
<SelectTrigger id="status" className="mt-1">
88+
<SelectValue />
89+
</SelectTrigger>
90+
<SelectContent>
91+
<SelectItem value={currentStatus}>
92+
{currentStatus}
93+
</SelectItem>
94+
{allowedStatuses.map((status) => (
95+
<SelectItem key={status} value={status}>
96+
{status}
97+
</SelectItem>
98+
))}
99+
</SelectContent>
100+
</Select>
101+
</div>
102+
{needsReason && (
103+
<div>
104+
<Label htmlFor="reason">
105+
Reason for Status Change *
106+
</Label>
107+
<Textarea
108+
id="reason"
109+
placeholder="Explain why this resident's status is being changed..."
110+
value={reason}
111+
onChange={(e) => setReason(e.target.value)}
112+
rows={4}
113+
className="mt-1"
114+
/>
115+
<p className="text-xs text-gray-500 mt-1">
116+
This information will be saved in the enrollment
117+
history
118+
</p>
119+
</div>
120+
)}
121+
<div className="flex gap-2 justify-end pt-4">
122+
<Button variant="outline" onClick={onClose}>
123+
Cancel
124+
</Button>
125+
<Button
126+
onClick={handleSubmit}
127+
disabled={!canSubmit}
128+
className="bg-[#556830] hover:bg-[#203622]"
129+
>
130+
Update Status
131+
</Button>
132+
</div>
133+
</div>
134+
</DialogContent>
135+
</Dialog>
136+
);
137+
}

0 commit comments

Comments
 (0)