"use client"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState, useTransition } from "react"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Weekday } from "@/generated/prisma/enums"; import { saveWorkingHoursAction } from "../actions/working-hours"; import type { WorkingHoursActionResult, WorkingHoursConflictItem } from "../actions/working-hours"; import type { WorkingHoursDayInput } from "@/services/staff/staff.service"; const DAY_ORDER: Weekday[] = [ Weekday.MONDAY, Weekday.TUESDAY, Weekday.WEDNESDAY, Weekday.THURSDAY, Weekday.FRIDAY, Weekday.SATURDAY, Weekday.SUNDAY, ]; const DAY_LABEL: Record = { MONDAY: "Monday", TUESDAY: "Tuesday", WEDNESDAY: "Wednesday", THURSDAY: "Thursday", FRIDAY: "Friday", SATURDAY: "Saturday", SUNDAY: "Sunday", }; interface DayState { dayOfWeek: Weekday; isOff: boolean; startTime: string; // "HH:MM" endTime: string; } function toTimeString(minutes: number | null): string { if (minutes === null) return "09:00"; const h = Math.floor(minutes / 60); const m = minutes % 60; return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`; } function toMinutes(time: string): number { const [h, m] = time.split(":").map(Number); return (h ?? 0) * 60 + (m ?? 0); } export interface InitialWorkingHoursRow { dayOfWeek: Weekday; isOff: boolean; startMinutes: number | null; endMinutes: number | null; } export function WorkingHoursForm({ initialRows, businessHoursLabel, onSave = saveWorkingHoursAction, appointmentHrefBase = "/staff/appointments", }: { initialRows: InitialWorkingHoursRow[]; businessHoursLabel: Record; /** Owner-on-behalf-of-staff editing (src/app/(owner)/owner/team/[id]/hours) injects its own Server Action here instead of the self-service default — same pattern as AppointmentStatusActions' `actions` prop. */ onSave?: ( days: WorkingHoursDayInput[], acknowledgeConflicts?: boolean, ) => Promise; /** Conflict links point at /owner/appointments/[id] when the owner is editing on a staff member's behalf. */ appointmentHrefBase?: string; }) { const router = useRouter(); const [days, setDays] = useState(() => DAY_ORDER.map((dayOfWeek) => { const existing = initialRows.find((r) => r.dayOfWeek === dayOfWeek); return { dayOfWeek, isOff: existing?.isOff ?? true, startTime: toTimeString(existing?.startMinutes ?? null), endTime: toTimeString(existing?.endMinutes ?? 19 * 60), }; }), ); const [error, setError] = useState(null); const [saved, setSaved] = useState(false); const [conflicts, setConflicts] = useState([]); const [isPending, startTransition] = useTransition(); const updateDay = (dayOfWeek: Weekday, patch: Partial) => { setSaved(false); setConflicts([]); setDays((prev) => prev.map((d) => (d.dayOfWeek === dayOfWeek ? { ...d, ...patch } : d))); }; const copyMondayToAll = () => { const monday = days.find((d) => d.dayOfWeek === Weekday.MONDAY); if (!monday) return; setSaved(false); setConflicts([]); setDays((prev) => prev.map((d) => (d.dayOfWeek === Weekday.MONDAY ? d : { ...d, isOff: monday.isOff, startTime: monday.startTime, endTime: monday.endTime }))); }; const submit = (acknowledgeConflicts: boolean) => { setError(null); startTransition(async () => { const result = await onSave( days.map((d) => ({ dayOfWeek: d.dayOfWeek, isOff: d.isOff, ...(d.isOff ? {} : { startMinutes: toMinutes(d.startTime), endMinutes: toMinutes(d.endTime) }), })), acknowledgeConflicts, ); if (!result.success) { setError(result.error ?? "Couldn't save working hours."); setConflicts(result.conflicts ?? []); return; } setSaved(true); setConflicts([]); router.refresh(); }); }; if (conflicts.length > 0) { return (

These hours would leave {conflicts.length} upcoming appointment{conflicts.length > 1 ? "s" : ""} outside working hours. They won't be cancelled automatically — review them, then confirm if you still want to save.

    {conflicts.map((c) => (
  • {c.customerName}

    {c.serviceNames}

  • ))}
); } return (
{days.map((day) => (
updateDay(day.dayOfWeek, { isOff: !checked })} aria-label={`${DAY_LABEL[day.dayOfWeek]} working`} />

{DAY_LABEL[day.dayOfWeek]}

Business hours: {businessHoursLabel[day.dayOfWeek]}

{day.isOff ? ( Off ) : (
updateDay(day.dayOfWeek, { startTime: e.target.value })} className="border-line rounded-lg border px-2 py-1.5 text-xs" aria-label={`${DAY_LABEL[day.dayOfWeek]} start time`} /> to updateDay(day.dayOfWeek, { endTime: e.target.value })} className="border-line rounded-lg border px-2 py-1.5 text-xs" aria-label={`${DAY_LABEL[day.dayOfWeek]} end time`} />
)}
))}
{error && (

{error}

)} {saved && !error && (

Saved.

)}
); }