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 (
{leftIcon}
{label}
);
}
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 (
choose(opt.value)}
data-testid={`settings-lang-${opt.value}`}
className={`text-sm font-medium px-2 py-1.5 rounded-md transition-colors ${
isActive
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
{opt.label}
);
})}
);
}
function SoundList({
current,
onPick,
}: {
current: NotificationSoundId;
onPick: (id: NotificationSoundId) => void;
}) {
const { t } = useTranslation();
return (
{ALL_SOUND_IDS.map((id) => {
const isActive = id === current;
return (
onPick(id)}
className="flex-1 flex items-center gap-2 text-start min-w-0"
>
{isActive ? (
) : (
)}
{t(`notifSettings.sounds.${id}`)}
notificationPlayer.testPlay(id)}
aria-label={t("notifSettings.preview")}
className="p-1 rounded hover:bg-foreground/10 text-muted-foreground"
>
);
})}
);
}
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 (
setSlot(s)}
aria-pressed={isActive}
className={`text-xs font-medium px-2 py-1.5 rounded-md transition-colors ${
isActive
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
{t(`notifSettings.slot.${s}`)}
);
})}
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 (
chooseHour12(value)}
disabled={updateClockHour12.isPending}
aria-pressed={isActive}
className={`text-sm font-medium px-2 py-1.5 rounded-md transition-colors ${
isActive
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
{t(`home.clockStyle.hourFormat.${labelKey}`)}
);
})}
{t("home.clockStyle.label")}
{CLOCK_STYLES.map((style) => {
const isActive = style === active;
return (
chooseStyle(style)}
disabled={updateClockStyle.isPending}
className={`flex items-center justify-between gap-3 px-2 py-2 rounded-lg text-start hover:bg-foreground/5 transition-colors ${
isActive ? "bg-primary/10" : ""
}`}
>
{t(`home.clockStyle.options.${style}`)}
{isActive && }
);
})}
);
}
/**
* 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 (
{muted && (
)}
{t("settingsPanel.title")}
);
}