Fixed an issue where media manager only shows files uploaded in specific sections. Fixed an issue with icon picker names. Removed jumbo rounding and a few other style tweaks.
89 lines
3.4 KiB
TypeScript
89 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { useRouter } from "next/navigation";
|
|
import { useState, useTransition } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { NotificationChannel } from "@/generated/prisma/enums";
|
|
|
|
const CHANNEL_ORDER: NotificationChannel[] = [NotificationChannel.EMAIL, NotificationChannel.PUSH];
|
|
const CHANNEL_LABEL: Record<NotificationChannel, string> = { EMAIL: "Email", PUSH: "Push", IN_APP: "In-app" };
|
|
|
|
export interface PreferenceCellState {
|
|
channel: NotificationChannel;
|
|
isEnabled: boolean;
|
|
}
|
|
|
|
/**
|
|
* Every other notification a barber gets (new appointment, reschedule,
|
|
* cancellation, schedule change) is required — always sent, no toggle —
|
|
* so this form only ever shows the one type that's genuinely optional:
|
|
* appointment reminders. See NotificationService/types.ts,
|
|
* PREFERENCE_CONTROLLED_TYPES, and docs/phase-7-summary.md — showing a
|
|
* toggle for a required type would just be a no-op switch that does
|
|
* nothing when flipped, which the Phase 7 brief explicitly calls out to
|
|
* avoid.
|
|
*/
|
|
export interface NotificationPreferencesFormProps {
|
|
initial: PreferenceCellState[];
|
|
/** Recipient-specific — see features/staff/actions/profile.ts (staff) and features/notifications/actions/preferences.ts (customer). */
|
|
action: (cells: PreferenceCellState[]) => Promise<{ success: boolean; error?: string }>;
|
|
}
|
|
|
|
export function NotificationPreferencesForm({ initial, action }: NotificationPreferencesFormProps) {
|
|
const router = useRouter();
|
|
const [cells, setCells] = useState(initial);
|
|
const [saved, setSaved] = useState(false);
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
const isEnabled = (channel: NotificationChannel) => cells.find((c) => c.channel === channel)?.isEnabled ?? true;
|
|
|
|
const toggle = (channel: NotificationChannel) => {
|
|
setSaved(false);
|
|
setCells((prev) => prev.map((c) => (c.channel === channel ? { ...c, isEnabled: !c.isEnabled } : c)));
|
|
};
|
|
|
|
const handleSave = () => {
|
|
startTransition(async () => {
|
|
await action(cells);
|
|
setSaved(true);
|
|
router.refresh();
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<p className="text-muted text-sm">
|
|
Booking confirmations, changes, and cancellations always notify you — those aren't
|
|
optional. Choose how you'd like to hear about upcoming appointment reminders.
|
|
</p>
|
|
|
|
<div className="rounded-md border-line divide-line divide-y overflow-hidden border bg-white">
|
|
{CHANNEL_ORDER.map((channel) => (
|
|
<div key={channel} className="flex items-center justify-between px-4 py-3">
|
|
<span className="text-sm font-medium">Reminders via {CHANNEL_LABEL[channel]}</span>
|
|
<button
|
|
type="button"
|
|
role="switch"
|
|
aria-checked={isEnabled(channel)}
|
|
aria-label={`Appointment reminders via ${CHANNEL_LABEL[channel]}`}
|
|
onClick={() => toggle(channel)}
|
|
className={`h-5 w-5 rounded-md border transition-colors ${
|
|
isEnabled(channel) ? "border-accent bg-accent" : "border-line bg-white"
|
|
}`}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{saved && (
|
|
<p role="status" className="text-accent-foreground text-sm font-medium">
|
|
Saved.
|
|
</p>
|
|
)}
|
|
<Button className="w-full" isLoading={isPending} onClick={handleSave}>
|
|
Save
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|