fix: patch P0 security vulnerabilities and critical UX issues across 6 modules
Security: Add admin/layout.tsx auth guard; Add requirePermission() to 12 admin pages Dashboard: Fix StudentStatsGrid rendering; Fix teacher greeting; Add loading/error boundaries; Fix col-span; Add metadata Announcements: Fix audience filtering; Add user detail page; Trigger notifications on publish; Pass classes data; Add loading.tsx Messages: Implement soft delete; Add unread badge with polling; Add notification dropdown polling; Add keyword search; Add quiet hours DND Management: Add loading/error for 9 admin routes; Fix admin-classes-view to use Select for school/grade Profile/Settings: Add loading/error; Fix parent role routing; Create ParentSettingsView; Integrate AiProviderSettingsCard; Add Tab URL persistence; Add logout confirm; Add avatar; Fix Progress arbitrary class Schema: Add senderDeletedAt/receiverDeletedAt to messages; Add quietHours to notificationPreferences; Add uniqueIndex import Docs: Update architecture docs 004/005
This commit is contained in:
@@ -39,9 +39,13 @@ import { formatDate } from "@/shared/lib/utils"
|
||||
export function AdminClassesClient({
|
||||
classes,
|
||||
teachers,
|
||||
schools,
|
||||
grades,
|
||||
}: {
|
||||
classes: AdminClassListItem[]
|
||||
teachers: TeacherOption[]
|
||||
schools: { id: string; name: string }[]
|
||||
grades: { id: string; name: string; school: { id: string; name: string } }[]
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const [isWorking, setIsWorking] = useState(false)
|
||||
@@ -50,18 +54,34 @@ export function AdminClassesClient({
|
||||
const [deleteItem, setDeleteItem] = useState<AdminClassListItem | null>(null)
|
||||
|
||||
const defaultTeacherId = useMemo(() => teachers[0]?.id ?? "", [teachers])
|
||||
const defaultSchoolId = useMemo(() => schools[0]?.id ?? "", [schools])
|
||||
const [createTeacherId, setCreateTeacherId] = useState(defaultTeacherId)
|
||||
const [createSchoolId, setCreateSchoolId] = useState(defaultSchoolId)
|
||||
const [createGradeId, setCreateGradeId] = useState("")
|
||||
const [editTeacherId, setEditTeacherId] = useState("")
|
||||
const [editSchoolId, setEditSchoolId] = useState("")
|
||||
const [editGradeId, setEditGradeId] = useState("")
|
||||
const [editSubjectTeachers, setEditSubjectTeachers] = useState<Array<{ subject: string; teacherId: string | null }>>([])
|
||||
|
||||
const createGrades = useMemo(() => grades.filter((g) => g.school.id === createSchoolId), [grades, createSchoolId])
|
||||
const editGrades = useMemo(() => grades.filter((g) => g.school.id === editSchoolId), [grades, editSchoolId])
|
||||
const selectedCreateSchool = schools.find((s) => s.id === createSchoolId)
|
||||
const selectedCreateGrade = grades.find((g) => g.id === createGradeId)
|
||||
const selectedEditSchool = schools.find((s) => s.id === editSchoolId)
|
||||
const selectedEditGrade = grades.find((g) => g.id === editGradeId)
|
||||
|
||||
useEffect(() => {
|
||||
if (!createOpen) return
|
||||
setCreateTeacherId(defaultTeacherId)
|
||||
}, [createOpen, defaultTeacherId])
|
||||
setCreateSchoolId(defaultSchoolId)
|
||||
setCreateGradeId("")
|
||||
}, [createOpen, defaultTeacherId, defaultSchoolId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!editItem) return
|
||||
setEditTeacherId(editItem.teacher.id)
|
||||
setEditSchoolId(editItem.schoolId ?? "")
|
||||
setEditGradeId(editItem.gradeId ?? "")
|
||||
setEditSubjectTeachers(
|
||||
DEFAULT_CLASS_SUBJECTS.map((s) => ({
|
||||
subject: s,
|
||||
@@ -227,10 +247,30 @@ export function AdminClassesClient({
|
||||
</DialogHeader>
|
||||
<form action={handleCreate} className="space-y-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="create-school-name" className="text-right">
|
||||
School
|
||||
</Label>
|
||||
<Input id="create-school-name" name="schoolName" className="col-span-3" placeholder="e.g. First Primary School" />
|
||||
<Label className="text-right">School</Label>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
value={createSchoolId}
|
||||
onValueChange={(v) => {
|
||||
setCreateSchoolId(v)
|
||||
setCreateGradeId("")
|
||||
}}
|
||||
disabled={schools.length === 0}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={schools.length === 0 ? "No schools" : "Select a school"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{schools.map((s) => (
|
||||
<SelectItem key={s.id} value={s.id}>
|
||||
{s.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<input type="hidden" name="schoolId" value={createSchoolId} />
|
||||
<input type="hidden" name="schoolName" value={selectedCreateSchool?.name ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
@@ -241,10 +281,27 @@ export function AdminClassesClient({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="create-grade" className="text-right">
|
||||
Grade
|
||||
</Label>
|
||||
<Input id="create-grade" name="grade" className="col-span-3" placeholder="e.g. Grade 10" />
|
||||
<Label className="text-right">Grade</Label>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
value={createGradeId}
|
||||
onValueChange={setCreateGradeId}
|
||||
disabled={createGrades.length === 0}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={createGrades.length === 0 ? "No grades" : "Select a grade"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{createGrades.map((g) => (
|
||||
<SelectItem key={g.id} value={g.id}>
|
||||
{g.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<input type="hidden" name="gradeId" value={createGradeId} />
|
||||
<input type="hidden" name="grade" value={selectedCreateGrade?.name ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
@@ -284,7 +341,7 @@ export function AdminClassesClient({
|
||||
<Button type="button" variant="outline" onClick={() => setCreateOpen(false)} disabled={isWorking}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={isWorking || teachers.length === 0 || !createTeacherId}>
|
||||
<Button type="submit" disabled={isWorking || teachers.length === 0 || !createTeacherId || !createGradeId}>
|
||||
Create
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -306,15 +363,30 @@ export function AdminClassesClient({
|
||||
{editItem ? (
|
||||
<form action={handleUpdate} className="space-y-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="edit-school-name" className="text-right">
|
||||
School
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-school-name"
|
||||
name="schoolName"
|
||||
className="col-span-3"
|
||||
defaultValue={editItem.schoolName ?? ""}
|
||||
/>
|
||||
<Label className="text-right">School</Label>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
value={editSchoolId}
|
||||
onValueChange={(v) => {
|
||||
setEditSchoolId(v)
|
||||
setEditGradeId("")
|
||||
}}
|
||||
disabled={schools.length === 0}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={schools.length === 0 ? "No schools" : "Select a school"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{schools.map((s) => (
|
||||
<SelectItem key={s.id} value={s.id}>
|
||||
{s.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<input type="hidden" name="schoolId" value={editSchoolId} />
|
||||
<input type="hidden" name="schoolName" value={selectedEditSchool?.name ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
@@ -325,10 +397,27 @@ export function AdminClassesClient({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="edit-grade" className="text-right">
|
||||
Grade
|
||||
</Label>
|
||||
<Input id="edit-grade" name="grade" className="col-span-3" defaultValue={editItem.grade} />
|
||||
<Label className="text-right">Grade</Label>
|
||||
<div className="col-span-3">
|
||||
<Select
|
||||
value={editGradeId}
|
||||
onValueChange={setEditGradeId}
|
||||
disabled={editGrades.length === 0}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={editGrades.length === 0 ? "No grades" : "Select a grade"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{editGrades.map((g) => (
|
||||
<SelectItem key={g.id} value={g.id}>
|
||||
{g.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<input type="hidden" name="gradeId" value={editGradeId} />
|
||||
<input type="hidden" name="grade" value={selectedEditGrade?.name ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
|
||||
Reference in New Issue
Block a user