Bug fixes (from bugs/ directory): - Fix cross-module DB queries in 9 modules (homework, grades, parent, diagnostic, elective, proctoring, notifications, scheduling, classes) by routing through data-access functions - Fix shared/lib <-> auth circular dependency via new session.ts module - Fix divide-by-zero guard in grades data-access - Fix audit export data truncation (paginated fetch for full datasets) - Fix missing transactions in homework grading and elective lottery - Fix missing revalidatePath in course-plans actions - Fix frontend permission checks using requirePermission instead of requireAuth - Fix dashboard role routing using session.user.roles - Fix student auth pattern (migrate getDemoStudentUser to users module) - Fix ActionState return type handling in components Code quality fixes: - Remove 60+ as type assertions (replace with type guards) - Remove non-null assertions (use optional chaining or explicit checks) - Convert dynamic imports to static imports (grades, diagnostic) - Add React.cache() wrapping for read functions - Parallelize independent queries with Promise.all - Add explicit return types to 30+ arrow functions - Replace any with unknown + type guards - Fix import type for type-only imports - Add Zod validation schemas for classes and diagnostic modules - Extract duplicate code (normalizeRoleName, normalizeBcryptHash, logger IP extraction) - Add console.error to silent catch blocks - Fix permission naming consistency (exam:proctor_read -> exam:proctor:read) Architecture doc sync: - Update 004_architecture_impact_map.md and 005_architecture_data.json - Update management-modules-audit.md for P0-7 cross-module fix Moved deleted proctoring event route to deletes/ folder.
203 lines
7.1 KiB
TypeScript
203 lines
7.1 KiB
TypeScript
"use client"
|
|
|
|
import { useTransition } from "react"
|
|
import { zodResolver } from "@hookform/resolvers/zod"
|
|
import { useForm } from "react-hook-form"
|
|
import { z } from "zod"
|
|
import { Loader2, Save } from "lucide-react"
|
|
import { toast } from "sonner"
|
|
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/shared/components/ui/card"
|
|
import { Input } from "@/shared/components/ui/input"
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/shared/components/ui/select"
|
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/shared/components/ui/form"
|
|
import { UserProfile } from "@/modules/users/data-access"
|
|
import { updateUserProfile } from "@/modules/users/actions"
|
|
|
|
const profileFormSchema = z.object({
|
|
name: z.string().min(2, "Name must be at least 2 characters."),
|
|
email: z.string().email().optional(), // Read only
|
|
role: z.string().optional(), // Read only
|
|
phone: z.string().optional(),
|
|
address: z.string().optional(),
|
|
gender: z.string().optional(),
|
|
age: z.coerce.number().min(0).optional(),
|
|
})
|
|
|
|
type ProfileFormValues = z.infer<typeof profileFormSchema>
|
|
|
|
export function ProfileSettingsForm({ user }: { user: UserProfile }) {
|
|
const [isPending, startTransition] = useTransition()
|
|
|
|
const form = useForm<ProfileFormValues>({
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
resolver: zodResolver(profileFormSchema) as any,
|
|
defaultValues: {
|
|
name: user.name ?? "",
|
|
email: user.email ?? "",
|
|
role: user.role ?? "",
|
|
phone: user.phone ?? "",
|
|
address: user.address ?? "",
|
|
gender: user.gender ?? "",
|
|
age: user.age ?? undefined,
|
|
},
|
|
})
|
|
|
|
function onSubmit(data: ProfileFormValues) {
|
|
startTransition(async () => {
|
|
try {
|
|
const result = await updateUserProfile({
|
|
name: data.name,
|
|
phone: data.phone || undefined,
|
|
address: data.address || undefined,
|
|
gender: data.gender || undefined,
|
|
age: data.age || undefined,
|
|
})
|
|
if (result.success) {
|
|
toast.success("Profile updated successfully")
|
|
} else {
|
|
toast.error(result.message || "Failed to update profile")
|
|
}
|
|
} catch (error) {
|
|
toast.error("Failed to update profile")
|
|
console.error(error)
|
|
}
|
|
})
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Profile Information</CardTitle>
|
|
<CardDescription>Update your personal information.</CardDescription>
|
|
</CardHeader>
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
<FormField
|
|
control={form.control}
|
|
name="name"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Full Name</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="Your name" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="email"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Email</FormLabel>
|
|
<FormControl>
|
|
<Input {...field} disabled />
|
|
</FormControl>
|
|
<FormDescription>Email cannot be changed.</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="phone"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Phone</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="+1 234 567 890" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="gender"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Gender</FormLabel>
|
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select gender" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
<SelectItem value="male">Male</SelectItem>
|
|
<SelectItem value="female">Female</SelectItem>
|
|
<SelectItem value="other">Other</SelectItem>
|
|
<SelectItem value="prefer_not_to_say">Prefer not to say</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="age"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Age</FormLabel>
|
|
<FormControl>
|
|
<Input type="number" placeholder="Age" {...field} value={field.value ?? ""} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="role"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Role</FormLabel>
|
|
<FormControl>
|
|
<Input {...field} disabled className="capitalize" />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="address"
|
|
render={({ field }) => (
|
|
<FormItem className="col-span-1 sm:col-span-2">
|
|
<FormLabel>Address</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="123 Main St, City, Country" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter className="flex justify-end border-t px-6 py-4">
|
|
<Button type="submit" disabled={isPending}>
|
|
{isPending ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Saving...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="mr-2 h-4 w-4" />
|
|
Save Changes
|
|
</>
|
|
)}
|
|
</Button>
|
|
</CardFooter>
|
|
</form>
|
|
</Form>
|
|
</Card>
|
|
)
|
|
}
|