PHP Rebuild
CI / ci (push) Canceled after 0s

Initial PHP rebuild of the app
This commit is contained in:
Stefan Lazic committed 2026-09-05 08:21:33 +01:00
1 parent 8a2b211069
commit 2ef3654841
690 files changed
+33944 -12046

No files matched your search

@@ -1,213 +0,0 @@
"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<Weekday, string> = {
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<Weekday, string>;
/** 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<WorkingHoursActionResult>;
/** 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<DayState[]>(() =>
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<string | null>(null);
const [saved, setSaved] = useState(false);
const [conflicts, setConflicts] = useState<WorkingHoursConflictItem[]>([]);
const [isPending, startTransition] = useTransition();
const updateDay = (dayOfWeek: Weekday, patch: Partial<DayState>) => {
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 (
<div className="space-y-4">
<p role="alert" className="text-danger text-sm font-medium">
These hours would leave {conflicts.length} upcoming appointment{conflicts.length > 1 ? "s" : ""} outside
working hours. They won&apos;t be cancelled automatically — review them, then confirm if you still want to
save.
</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={() => setConflicts([])}>
Go Back
</Button>
<Button variant="destructive" className="flex-1" isLoading={isPending} onClick={() => submit(true)}>
Save Anyway
</Button>
</div>
</div>
);
}
return (
<div className="space-y-4">
<button type="button" onClick={copyMondayToAll} className="text-accent text-xs font-bold tracking-widest uppercase underline">
Copy Monday to all days
</button>
<div className="divide-line rounded-md border-line divide-y border bg-white">
{days.map((day) => (
<div key={day.dayOfWeek} className="flex items-center justify-between gap-4 px-5 py-4">
<div className="flex items-center gap-3">
<Switch checked={!day.isOff} onCheckedChange={(checked) => updateDay(day.dayOfWeek, { isOff: !checked })} aria-label={`${DAY_LABEL[day.dayOfWeek]} working`} />
<div>
<p className="text-sm font-bold">{DAY_LABEL[day.dayOfWeek]}</p>
<p className="text-muted text-[10px]">Business hours: {businessHoursLabel[day.dayOfWeek]}</p>
</div>
</div>
{day.isOff ? (
<span className="text-muted text-xs">Off</span>
) : (
<div className="flex items-center gap-2">
<input
type="time"
value={day.startTime}
onChange={(e) => 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`}
/>
<span className="text-muted text-xs">to</span>
<input
type="time"
value={day.endTime}
onChange={(e) => 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`}
/>
</div>
)}
</div>
))}
</div>
{error && (
<p role="alert" className="text-danger text-sm font-medium">
{error}
</p>
)}
{saved && !error && (
<p role="status" className="text-accent-foreground text-sm font-medium">
Saved.
</p>
)}
<Button className="w-full" isLoading={isPending} onClick={() => submit(false)}>
Save
</Button>
</div>
);
}