"use client"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useState, useTransition } from "react"; import { Button } from "@/components/ui/button"; import { BottomSheet } from "@/components/ui/bottom-sheet"; import { Input } from "@/components/ui/input"; import type { TimeOffActionResult, TimeOffConflictItem } from "../actions/time-off"; import { createTimeOffAction, deleteTimeOffAction, updateTimeOffAction } from "../actions/time-off"; import type { TimeOffInput } from "@/services/staff/staff.service"; export interface EditableTimeOff { id: string; startDate: string; endDate: string; reason: string | null; } /** Same DI shape as AppointmentStatusActions' `actions` prop — the owner-on-behalf-of-staff editor injects its own Server Actions bound to a chosen staffId instead of forking this component. */ export interface TimeOffActionSet { create: ( input: Pick, acknowledgeConflicts?: boolean, ) => Promise; update: ( timeOffId: string, input: Pick, acknowledgeConflicts?: boolean, ) => Promise; remove: (timeOffId: string) => Promise; } const DEFAULT_ACTIONS: TimeOffActionSet = { create: createTimeOffAction, update: updateTimeOffAction, remove: deleteTimeOffAction, }; function todayIso(): string { return new Date().toISOString().slice(0, 10); } export function TimeOffFormSheet({ open, onClose, existing, actions = DEFAULT_ACTIONS, appointmentHrefBase = "/staff/appointments", }: { open: boolean; onClose: () => void; existing?: EditableTimeOff | null; actions?: TimeOffActionSet; appointmentHrefBase?: string; }) { const router = useRouter(); const [startDate, setStartDate] = useState(existing?.startDate ?? todayIso()); const [endDate, setEndDate] = useState(existing?.endDate ?? existing?.startDate ?? todayIso()); const [reason, setReason] = useState(existing?.reason ?? ""); const [error, setError] = useState(null); const [conflicts, setConflicts] = useState(null); const [isPending, startTransition] = useTransition(); const [isDeleting, startDeleteTransition] = useTransition(); const reset = () => { setStartDate(existing?.startDate ?? todayIso()); setEndDate(existing?.endDate ?? existing?.startDate ?? todayIso()); setReason(existing?.reason ?? ""); setError(null); setConflicts(null); }; const handleClose = () => { onClose(); reset(); }; const submit = (acknowledgeConflicts: boolean) => { if (startDate > endDate) { setError("Start date must be on or before the end date."); return; } setError(null); startTransition(async () => { const input = { startDate, endDate, reason: reason.trim() || undefined }; const result: TimeOffActionResult = existing ? await actions.update(existing.id, input, acknowledgeConflicts) : await actions.create(input, acknowledgeConflicts); if (!result.success) { setError(result.error ?? "Something went wrong."); setConflicts(result.conflicts ?? null); return; } handleClose(); router.refresh(); }); }; const handleDelete = () => { if (!existing) return; startDeleteTransition(async () => { const result = await actions.remove(existing.id); if (!result.success) { setError(result.error ?? "Couldn't remove this time off."); return; } handleClose(); router.refresh(); }); }; return (
{conflicts && conflicts.length > 0 ? (

This overlaps {conflicts.length} existing appointment {conflicts.length > 1 ? "s" : ""}. These won't be cancelled automatically — you'll need to reschedule or cancel them yourself.

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

    {c.serviceNames}

  • ))}
) : ( <>
setStartDate(e.target.value)} /> setEndDate(e.target.value)} />
setReason(e.target.value)} /> {error && (

{error}

)} {existing && ( )} )}
); }