Revert "PHP Rebuild"

This reverts commit 2ef3654841.
This commit is contained in:
Stefan Lazic committed 2026-09-05 09:32:05 +01:00
1 parent a59f998f2b
commit 2aa8405006
690 files changed
+12045 -33943

No files matched your search

@@ -0,0 +1,177 @@
"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<TimeOffInput, "startDate" | "endDate" | "reason">,
acknowledgeConflicts?: boolean,
) => Promise<TimeOffActionResult>;
update: (
timeOffId: string,
input: Pick<TimeOffInput, "startDate" | "endDate" | "reason">,
acknowledgeConflicts?: boolean,
) => Promise<TimeOffActionResult>;
remove: (timeOffId: string) => Promise<TimeOffActionResult>;
}
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<string | null>(null);
const [conflicts, setConflicts] = useState<TimeOffConflictItem[] | null>(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 (
<BottomSheet open={open} onClose={handleClose} title={existing ? "Edit Time Off" : "New Time Off"}>
<div className="space-y-4 pb-2">
{conflicts && conflicts.length > 0 ? (
<div className="space-y-3">
<p role="alert" className="text-danger text-sm font-medium">
This overlaps {conflicts.length} existing appointment
{conflicts.length > 1 ? "s" : ""}. These won&apos;t be cancelled automatically —
you&apos;ll need to reschedule or cancel them yourself.
</p>
<ul className="space-y-2">
{conflicts.map((c) => (
<li key={c.id} className="border-line rounded-lg border bg-white p-3 text-xs">
<Link href={`${appointmentHrefBase}/${c.id}`} className="font-bold underline">
{c.customerName}
</Link>
<p className="text-muted">{c.serviceNames}</p>
</li>
))}
</ul>
<div className="flex gap-3">
<Button variant="secondary" className="flex-1" onClick={handleClose}>
Cancel
</Button>
<Button
variant="destructive"
className="flex-1"
isLoading={isPending}
onClick={() => submit(true)}
>
Save Anyway
</Button>
</div>
</div>
) : (
<>
<div className="grid grid-cols-2 gap-4">
<Input label="Start date" type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
<Input label="End date" type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
</div>
<Input
label="Reason (optional)"
placeholder="e.g. Vacation"
value={reason}
onChange={(e) => setReason(e.target.value)}
/>
{error && (
<p role="alert" className="text-danger text-sm font-medium">
{error}
</p>
)}
<Button className="w-full" isLoading={isPending} onClick={() => submit(false)}>
{existing ? "Save Changes" : "Add Time Off"}
</Button>
{existing && (
<Button variant="ghost" className="w-full text-danger" isLoading={isDeleting} onClick={handleDelete}>
Remove Time Off
</Button>
)}
</>
)}
</div>
</BottomSheet>
);
}