"use client" import * as React from "react" import { useTransition } from "react" import { useTranslations } from "next-intl" import { Loader2, Save, Bell, Mail, MessageSquare, Megaphone, GraduationCap, BookOpen, CalendarCheck, Moon, Send } from "lucide-react" import { toast } from "sonner" import { sendTestNotificationAction } from "@/modules/settings/actions-notifications" import { Button } from "@/shared/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Input } from "@/shared/components/ui/input" import { Switch } from "@/shared/components/ui/switch" import { Label } from "@/shared/components/ui/label" import { Separator } from "@/shared/components/ui/separator" import { cn } from "@/shared/lib/utils" import { useSettingsService } from "@/modules/settings/components/settings-service-context" import type { NotificationPreferences } from "@/modules/notifications/types" interface NotificationPreferencesFormProps { preferences: NotificationPreferences } interface ChannelItem { key: keyof Pick< NotificationPreferences, "emailEnabled" | "smsEnabled" | "pushEnabled" > labelKey: string descKey: string icon: React.ComponentType<{ className?: string }> } interface CategoryItem { key: keyof Pick< NotificationPreferences, | "homeworkNotifications" | "gradeNotifications" | "announcementNotifications" | "messageNotifications" | "attendanceNotifications" > labelKey: string descKey: string icon: React.ComponentType<{ className?: string }> } const CHANNELS: ChannelItem[] = [ { key: "pushEnabled", labelKey: "channels.push", descKey: "channels.pushDesc", icon: Bell }, { key: "emailEnabled", labelKey: "channels.email", descKey: "channels.emailDesc", icon: Mail }, { key: "smsEnabled", labelKey: "channels.sms", descKey: "channels.smsDesc", icon: MessageSquare }, ] const CATEGORIES: CategoryItem[] = [ { key: "messageNotifications", labelKey: "categories.messages", descKey: "categories.messagesDesc", icon: MessageSquare }, { key: "announcementNotifications", labelKey: "categories.announcements", descKey: "categories.announcementsDesc", icon: Megaphone }, { key: "homeworkNotifications", labelKey: "categories.homework", descKey: "categories.homeworkDesc", icon: BookOpen }, { key: "gradeNotifications", labelKey: "categories.grades", descKey: "categories.gradesDesc", icon: GraduationCap }, { key: "attendanceNotifications", labelKey: "categories.attendance", descKey: "categories.attendanceDesc", icon: CalendarCheck }, ] export function NotificationPreferencesForm({ preferences }: NotificationPreferencesFormProps) { const t = useTranslations("settings.notifications") const { notifications } = useSettingsService() const [isPending, startTransition] = useTransition() const [testingChannel, setTestingChannel] = React.useState(null) const [channels, setChannels] = React.useState({ emailEnabled: preferences.emailEnabled, smsEnabled: preferences.smsEnabled, pushEnabled: preferences.pushEnabled, }) const [categories, setCategories] = React.useState({ homeworkNotifications: preferences.homeworkNotifications, gradeNotifications: preferences.gradeNotifications, announcementNotifications: preferences.announcementNotifications, messageNotifications: preferences.messageNotifications, attendanceNotifications: preferences.attendanceNotifications, }) const [quietHours, setQuietHours] = React.useState({ quietHoursEnabled: preferences.quietHoursEnabled, quietHoursStart: preferences.quietHoursStart ?? "", quietHoursEnd: preferences.quietHoursEnd ?? "", }) // 记录初始状态,用于 dirty 检测 const initialSnapshot = React.useMemo( () => JSON.stringify({ channels: { emailEnabled: preferences.emailEnabled, smsEnabled: preferences.smsEnabled, pushEnabled: preferences.pushEnabled, }, categories: { homeworkNotifications: preferences.homeworkNotifications, gradeNotifications: preferences.gradeNotifications, announcementNotifications: preferences.announcementNotifications, messageNotifications: preferences.messageNotifications, attendanceNotifications: preferences.attendanceNotifications, }, quietHours: { quietHoursEnabled: preferences.quietHoursEnabled, quietHoursStart: preferences.quietHoursStart ?? "", quietHoursEnd: preferences.quietHoursEnd ?? "", }, }), [preferences], ) const isDirty = React.useMemo(() => { const currentSnapshot = JSON.stringify({ channels, categories, quietHours }) return currentSnapshot !== initialSnapshot }, [channels, categories, quietHours, initialSnapshot]) const toggleChannel = (key: keyof typeof channels) => { setChannels((prev) => ({ ...prev, [key]: !prev[key] })) } const toggleCategory = (key: keyof typeof categories) => { setCategories((prev) => ({ ...prev, [key]: !prev[key] })) } const toggleQuietHours = () => { setQuietHours((prev) => ({ ...prev, quietHoursEnabled: !prev.quietHoursEnabled })) } function onSubmit() { if (!isDirty) return startTransition(async () => { try { const result = await notifications.updatePreferences({ ...channels, ...categories, quietHoursEnabled: quietHours.quietHoursEnabled, quietHoursStart: quietHours.quietHoursStart || null, quietHoursEnd: quietHours.quietHoursEnd || null, }) if (result.success) { toast.success(t("success")) } else { toast.error(result.message || t("failure")) } } catch { toast.error(t("failure")) } }) } async function handleTestNotification(channel: "push" | "email" | "sms"): Promise { setTestingChannel(channel) try { const result = await sendTestNotificationAction({ channel }) if (result.success) { toast.success(t("testSuccess")) } else { toast.error(result.message || t("testFailure")) } } catch { toast.error(t("testFailure")) } finally { setTestingChannel(null) } } return ( {t("title")} {t("description")} {/* Delivery channels */}

{t("channels.title")}

{t("channels.subtitle")}

{CHANNELS.map((item) => { const Icon = item.icon const checked = channels[item.key] const channelName = item.key === "emailEnabled" ? "email" : item.key === "smsEnabled" ? "sms" : "push" return (

{t(item.descKey)}

{checked ? ( ) : null} toggleChannel(item.key)} aria-label={t(item.labelKey)} />
) })}
{/* Notification categories */}

{t("categories.title")}

{t("categories.subtitle")}

{CATEGORIES.map((item) => { const Icon = item.icon const checked = categories[item.key] return (

{t(item.descKey)}

toggleCategory(item.key)} aria-label={t(item.labelKey)} />
) })}
{/* Quiet hours */}

{t("quietHours.title")}

{t("quietHours.subtitle")}

{t("quietHours.enableDesc")}

setQuietHours((prev) => ({ ...prev, quietHoursStart: e.target.value }))} disabled={!quietHours.quietHoursEnabled} />
setQuietHours((prev) => ({ ...prev, quietHoursEnd: e.target.value }))} disabled={!quietHours.quietHoursEnabled} />
) }