import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; import { useUpdateLanguage, useUpdateClockStyle, useUpdateClockHour12, getGetMeQueryKey, type AuthUser, type ClockStyle as ClockStyleType, type NotificationSoundId, } from "@workspace/api-client-react"; import { Settings as SettingsIcon, BellOff, Volume2, VolumeX, Check, Eye, EyeOff, Play, } from "lucide-react"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; import { Clock, CLOCK_STYLES, resolveClockStyle, resolveClockHour12, useNow, useHomeClockVisibility, useTopbarClockVisibility, } from "@/components/clock"; import { ALL_SOUND_IDS, notificationPlayer, } from "@/lib/notification-sounds"; import { useAuth } from "@/contexts/AuthContext"; import { useUpdatePrefs } from "@/components/notification-settings"; import i18n from "@/i18n"; type Slot = "order" | "meeting" | "note"; const SLOT_KEYS: Record< Slot, { enabledKey: keyof Pick< AuthUser, "notifyOrdersEnabled" | "notifyMeetingsEnabled" | "notifyNotesEnabled" >; soundKey: keyof Pick< AuthUser, "notificationSoundOrder" | "notificationSoundMeeting" | "notificationSoundNote" >; vibrateKey: keyof Pick< AuthUser, "vibrationEnabledOrder" | "vibrationEnabledMeeting" | "vibrationEnabledNote" >; enabledLabel: string; vibrateLabel: string; } > = { order: { enabledKey: "notifyOrdersEnabled", soundKey: "notificationSoundOrder", vibrateKey: "vibrationEnabledOrder", enabledLabel: "notifSettings.ordersEnabled", vibrateLabel: "notifSettings.vibrationOrder", }, meeting: { enabledKey: "notifyMeetingsEnabled", soundKey: "notificationSoundMeeting", vibrateKey: "vibrationEnabledMeeting", enabledLabel: "notifSettings.meetingsEnabled", vibrateLabel: "notifSettings.vibrationMeeting", }, note: { enabledKey: "notifyNotesEnabled", soundKey: "notificationSoundNote", vibrateKey: "vibrationEnabledNote", enabledLabel: "notifSettings.notesEnabled", vibrateLabel: "notifSettings.vibrationNote", }, }; function ToggleRow({ active, onClick, label, leftIcon, }: { active: boolean; onClick: () => void; label: string; leftIcon?: React.ReactNode; }) { return ( ); } function SectionTitle({ children }: { children: React.ReactNode }) { return (
{children}
); } function GroupCard({ children }: { children: React.ReactNode }) { return (
{children}
); } function LanguageGroup() { const { t, i18n: i18nInstance } = useTranslation(); const queryClient = useQueryClient(); const updateLanguage = useUpdateLanguage(); const current = i18nInstance.language === "ar" ? "ar" : "en"; const choose = (next: "ar" | "en") => { if (next === current) return; i18n.changeLanguage(next); updateLanguage.mutate( { data: { language: next } }, { onSuccess: () => { queryClient.invalidateQueries({ queryKey: getGetMeQueryKey() }); }, }, ); }; const options: Array<{ value: "ar" | "en"; label: string }> = [ { value: "ar", label: t("settingsPanel.lang.ar") }, { value: "en", label: t("settingsPanel.lang.en") }, ]; return ( {t("settingsPanel.section.language")}
{options.map((opt) => { const isActive = opt.value === current; return ( ); })}
); } function SoundList({ current, onPick, }: { current: NotificationSoundId; onPick: (id: NotificationSoundId) => void; }) { const { t } = useTranslation(); return (
{ALL_SOUND_IDS.map((id) => { const isActive = id === current; return (
); })}
); } function NotificationsGroup() { const { t } = useTranslation(); const { user } = useAuth(); const apply = useUpdatePrefs(); const [slot, setSlot] = useState("order"); if (!user) return null; const cfg = SLOT_KEYS[slot]; return ( {t("settingsPanel.section.notifications")}
{(["order", "meeting", "note"] as const).map((s) => { const isActive = s === slot; return ( ); })}
apply({ [cfg.enabledKey]: !user[cfg.enabledKey] } as Record) } label={t(cfg.enabledLabel)} /> apply({ [cfg.vibrateKey]: !user[cfg.vibrateKey] } as Record) } label={t(cfg.vibrateLabel)} />
apply({ [cfg.soundKey]: id } as Record)} /> ); } function SoundGroup() { const { t } = useTranslation(); const { user } = useAuth(); const apply = useUpdatePrefs(); if (!user) return null; const muted = user.notificationsMuted; return ( {t("settingsPanel.section.sound")} apply({ notificationsMuted: !muted })} label={t("settingsPanel.sound.master")} leftIcon={ muted ? ( ) : ( ) } /> ); } function ClockGroup() { const { t, i18n: i18nInstance } = useTranslation(); const lang = i18nInstance.language; const queryClient = useQueryClient(); const now = useNow(); const { user } = useAuth(); const updateClockStyle = useUpdateClockStyle(); const updateClockHour12 = useUpdateClockHour12(); const [homeClockVisible, setHomeClockVisible] = useHomeClockVisibility(); const [topbarClockVisible, setTopbarClockVisible] = useTopbarClockVisibility(); if (!user) return null; const active = resolveClockStyle(user.clockStyle); const activeHour12 = resolveClockHour12(user.clockHour12); const chooseStyle = (style: ClockStyleType) => { const previous = queryClient.getQueryData(getGetMeQueryKey()); if (previous) { queryClient.setQueryData(getGetMeQueryKey(), { ...previous, clockStyle: style }); } updateClockStyle.mutate( { data: { clockStyle: style } }, { onSuccess: (data) => queryClient.setQueryData(getGetMeQueryKey(), data), onError: () => { if (previous) queryClient.setQueryData(getGetMeQueryKey(), previous); else queryClient.invalidateQueries({ queryKey: getGetMeQueryKey() }); }, }, ); }; const chooseHour12 = (next: boolean) => { if (next === activeHour12) return; const previous = queryClient.getQueryData(getGetMeQueryKey()); if (previous) { queryClient.setQueryData(getGetMeQueryKey(), { ...previous, clockHour12: next }); } updateClockHour12.mutate( { data: { clockHour12: next } }, { onSuccess: (data) => queryClient.setQueryData(getGetMeQueryKey(), data), onError: () => { if (previous) queryClient.setQueryData(getGetMeQueryKey(), previous); else queryClient.invalidateQueries({ queryKey: getGetMeQueryKey() }); }, }, ); }; return ( {t("settingsPanel.section.clock")} setHomeClockVisible(!homeClockVisible)} label={t("home.clockStyle.showWidget")} leftIcon={ homeClockVisible ? ( ) : ( ) } /> setTopbarClockVisible(!topbarClockVisible)} label={t("home.clockStyle.showTopbar")} leftIcon={ topbarClockVisible ? ( ) : ( ) } />
{t("home.clockStyle.hourFormat.label")}
{([false, true] as const).map((value) => { const isActive = value === activeHour12; const labelKey = value ? "h12" : "h24"; return ( ); })}
{t("home.clockStyle.label")}
{CLOCK_STYLES.map((style) => { const isActive = style === active; return ( ); })}
); } /** * Unified per-user Settings panel (Task #527). Consolidates the four * preferences that previously each had their own topbar button: * - Language (was: globe button) * - Notifications: per-category sound + vibration (was: bell #2 popover) * - Sound: master mute toggle (was: volume button) * - Clock: visibility, 12/24h, style (was: clock-style picker button) * * Renders as a right-side Sheet on every viewport — full-width on * phones, capped width on desktop. Keeps the same persistence paths * as the original controls (DB columns via the existing hooks + * localStorage for clock visibility), so no migration is required. */ export function SettingsPanel() { const { t } = useTranslation(); const [open, setOpen] = useState(false); const { user } = useAuth(); if (!user) return null; const muted = user.notificationsMuted; return ( {t("settingsPanel.title")}
); }