2026-10-22 10:54:59 +00:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useMemo, useState } from "react";
|
Cash float card, Office role + Slips tab, job card PDF, manual close/delete
- Cash Floats: ledger card (opening balance, top-ups, slips), manual entries, slip photos
- Office access level; clock app Slips tab (photograph a slip, popup for supplier/cost
code/comments/amount); only Office sees it; per-role route table in lib/clock-access
- Job cards: Machine Service PDF (with Oil block), JC numbers, manual Close job, Delete
- Hide Expenses > Vehicle Costs and Finance (Bastion) from the menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:09:57 +00:00
|
|
|
import { useRouter } from "next/navigation";
|
|
|
|
|
import { Download, FileText } from "lucide-react";
|
|
|
|
|
import { buildJobCardPdf } from "@/lib/job-card-pdf";
|
|
|
|
|
import { downloadPdf } from "@/lib/qr-pdf";
|
2026-10-22 10:54:59 +00:00
|
|
|
import { inBounds, presetBounds, saDay, type LogPreset } from "@/lib/oil-log";
|
|
|
|
|
import { formatMinutes, JOB_TYPES, jobTypeLabel } from "@/lib/job-cards";
|
|
|
|
|
|
|
|
|
|
export type JobRow = {
|
|
|
|
|
id: string; startedAt: string; endedAt: string; open: boolean; asset: string; assetName: string;
|
2026-10-22 11:44:01 +00:00
|
|
|
mechanic: string; type: string; minutes: number; odometer: number | null; note: string; oil: string;
|
Cash float card, Office role + Slips tab, job card PDF, manual close/delete
- Cash Floats: ledger card (opening balance, top-ups, slips), manual entries, slip photos
- Office access level; clock app Slips tab (photograph a slip, popup for supplier/cost
code/comments/amount); only Office sees it; per-role route table in lib/clock-access
- Job cards: Machine Service PDF (with Oil block), JC numbers, manual Close job, Delete
- Hide Expenses > Vehicle Costs and Finance (Bastion) from the menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:09:57 +00:00
|
|
|
oilLines: string[]; jcNo: number | null; site: string;
|
2026-10-22 10:54:59 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const PRESETS: { id: LogPreset; label: string }[] = [
|
|
|
|
|
{ id: "today", label: "Today" }, { id: "week", label: "This week" }, { id: "month", label: "This month" },
|
|
|
|
|
{ id: "all", label: "Everything" }, { id: "range", label: "Date range" },
|
|
|
|
|
];
|
|
|
|
|
const fmt = (n: number) => n.toLocaleString("en-ZA", { maximumFractionDigits: 1 });
|
|
|
|
|
const when = (s: string) => (s ? new Date(s.replace(" ", "T")).toLocaleString("en-ZA", {
|
|
|
|
|
timeZone: "Africa/Johannesburg", day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", hourCycle: "h23" }) : "—");
|
|
|
|
|
|
Cash float card, Office role + Slips tab, job card PDF, manual close/delete
- Cash Floats: ledger card (opening balance, top-ups, slips), manual entries, slip photos
- Office access level; clock app Slips tab (photograph a slip, popup for supplier/cost
code/comments/amount); only Office sees it; per-role route table in lib/clock-access
- Job cards: Machine Service PDF (with Oil block), JC numbers, manual Close job, Delete
- Hide Expenses > Vehicle Costs and Finance (Bastion) from the menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:09:57 +00:00
|
|
|
function pdfOf(r: JobRow) {
|
|
|
|
|
return buildJobCardPdf({
|
|
|
|
|
jcNo: r.jcNo, plantNo: r.asset, plantName: r.assetName, site: r.site, mechanic: r.mechanic,
|
|
|
|
|
machineHours: r.odometer != null ? String(r.odometer) : "", startedAt: r.startedAt, endedAt: r.endedAt,
|
|
|
|
|
type: r.type, minutes: r.minutes, note: r.note, oil: r.oilLines,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 10:54:59 +00:00
|
|
|
export function JobCardsTable({ rows }: { rows: JobRow[] }) {
|
Cash float card, Office role + Slips tab, job card PDF, manual close/delete
- Cash Floats: ledger card (opening balance, top-ups, slips), manual entries, slip photos
- Office access level; clock app Slips tab (photograph a slip, popup for supplier/cost
code/comments/amount); only Office sees it; per-role route table in lib/clock-access
- Job cards: Machine Service PDF (with Oil block), JC numbers, manual Close job, Delete
- Hide Expenses > Vehicle Costs and Finance (Bastion) from the menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:09:57 +00:00
|
|
|
const router = useRouter();
|
|
|
|
|
const [closing, setClosing] = useState<JobRow | null>(null);
|
|
|
|
|
const [deleting, setDeleting] = useState<JobRow | null>(null);
|
|
|
|
|
const [cType, setCType] = useState("");
|
|
|
|
|
const [cNote, setCNote] = useState("");
|
|
|
|
|
const [cOdo, setCOdo] = useState("");
|
|
|
|
|
const [cEnd, setCEnd] = useState("");
|
|
|
|
|
const [working, setWorking] = useState(false);
|
|
|
|
|
const [dialogError, setDialogError] = useState("");
|
|
|
|
|
const [pdfBusy, setPdfBusy] = useState("");
|
|
|
|
|
|
|
|
|
|
async function download(r: JobRow | null) {
|
|
|
|
|
setPdfBusy(r?.id ?? "blank");
|
|
|
|
|
try {
|
|
|
|
|
const bytes = r ? await pdfOf(r) : await buildJobCardPdf({});
|
|
|
|
|
downloadPdf(bytes, r ? `job-card-${String(r.jcNo ?? 0).padStart(4, "0")}-${r.asset || "vehicle"}.pdf` : "job-card-blank.pdf");
|
|
|
|
|
} catch { alert("Could not build the PDF. Please try again."); }
|
|
|
|
|
finally { setPdfBusy(""); }
|
|
|
|
|
}
|
|
|
|
|
function openClose(r: JobRow) { setClosing(r); setCType(""); setCNote(""); setCOdo(""); setCEnd(""); setDialogError(""); }
|
|
|
|
|
async function submitClose(e: React.FormEvent) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (!closing || !cType) return;
|
|
|
|
|
setWorking(true); setDialogError("");
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/job-cards/${closing.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ job_type: cType, note: cNote, odometer: cOdo, ended_at: cEnd ? new Date(cEnd).toISOString() : undefined }) });
|
|
|
|
|
const j = await res.json().catch(() => ({}));
|
|
|
|
|
if (!res.ok || !j.ok) throw new Error(j.error || "Could not close the job");
|
|
|
|
|
setClosing(null); router.refresh();
|
|
|
|
|
} catch (err: any) { setDialogError(err.message); } finally { setWorking(false); }
|
|
|
|
|
}
|
|
|
|
|
async function confirmDelete() {
|
|
|
|
|
if (!deleting) return;
|
|
|
|
|
setWorking(true); setDialogError("");
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/job-cards/${deleting.id}`, { method: "DELETE" });
|
|
|
|
|
const j = await res.json().catch(() => ({}));
|
|
|
|
|
if (!res.ok || !j.ok) throw new Error(j.error || "Could not delete the job");
|
|
|
|
|
setDeleting(null); router.refresh();
|
|
|
|
|
} catch (err: any) { setDialogError(err.message); } finally { setWorking(false); }
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-22 10:54:59 +00:00
|
|
|
const [preset, setPreset] = useState<LogPreset>("month");
|
|
|
|
|
const [from, setFrom] = useState("");
|
|
|
|
|
const [to, setTo] = useState("");
|
|
|
|
|
const [type, setType] = useState("");
|
|
|
|
|
const [asset, setAsset] = useState("");
|
|
|
|
|
const [mechanic, setMechanic] = useState("");
|
|
|
|
|
|
|
|
|
|
const assets = useMemo(() => [...new Set(rows.map((r) => r.asset))].filter(Boolean).sort(), [rows]);
|
|
|
|
|
const mechanics = useMemo(() => [...new Set(rows.map((r) => r.mechanic))].filter(Boolean).sort(), [rows]);
|
|
|
|
|
const shown = useMemo(() => {
|
|
|
|
|
const bounds = presetBounds(preset, saDay(new Date().toISOString()), from, to);
|
|
|
|
|
return rows.filter((r) => inBounds(saDay(r.startedAt), bounds) && (!type || r.type === type)
|
|
|
|
|
&& (!asset || r.asset === asset) && (!mechanic || r.mechanic === mechanic));
|
|
|
|
|
}, [rows, preset, from, to, type, asset, mechanic]);
|
|
|
|
|
|
|
|
|
|
const done = shown.filter((r) => !r.open);
|
|
|
|
|
const totalMinutes = done.reduce((s, r) => s + r.minutes, 0);
|
|
|
|
|
const field = "min-h-[44px] rounded-lg border border-gray-200 dark:border-border bg-white dark:bg-surface px-3 text-sm";
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="bg-white dark:bg-surface border border-gray-100 dark:border-border rounded-2xl p-4 sm:p-5 shadow-sm">
|
|
|
|
|
<div className="flex flex-col lg:flex-row lg:items-center gap-3 mb-3">
|
|
|
|
|
<div className="hidden lg:flex gap-1.5" role="group" aria-label="Period">
|
|
|
|
|
{PRESETS.map((p) => (
|
|
|
|
|
<button key={p.id} onClick={() => setPreset(p.id)} aria-pressed={preset === p.id}
|
|
|
|
|
className={`min-h-[44px] px-4 rounded-lg text-xs font-medium border ${preset === p.id ? "bg-[#30638E] text-white border-[#30638E]" : "border-gray-200 dark:border-border text-gray-600 dark:text-gray-300"}`}>
|
|
|
|
|
{p.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<select className={`${field} lg:hidden`} value={preset} onChange={(e) => setPreset(e.target.value as LogPreset)} aria-label="Period">
|
|
|
|
|
{PRESETS.map((p) => <option key={p.id} value={p.id}>{p.label}</option>)}
|
|
|
|
|
</select>
|
|
|
|
|
{preset === "range" && (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<input type="date" className={field} value={from} max={to || undefined} onChange={(e) => setFrom(e.target.value)} aria-label="From" />
|
|
|
|
|
<span className="text-xs text-gray-500">to</span>
|
|
|
|
|
<input type="date" className={field} value={to} min={from || undefined} onChange={(e) => setTo(e.target.value)} aria-label="To" />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div className="flex flex-wrap gap-2 lg:ml-auto">
|
|
|
|
|
<select className={field} value={type} onChange={(e) => setType(e.target.value)} aria-label="Kind of work">
|
|
|
|
|
<option value="">All work</option>{JOB_TYPES.map((t) => <option key={t.id} value={t.id}>{t.label}</option>)}
|
|
|
|
|
</select>
|
|
|
|
|
<select className={field} value={asset} onChange={(e) => setAsset(e.target.value)} aria-label="Vehicle">
|
|
|
|
|
<option value="">All vehicles</option>{assets.map((a) => <option key={a} value={a}>{a}</option>)}
|
|
|
|
|
</select>
|
|
|
|
|
<select className={field} value={mechanic} onChange={(e) => setMechanic(e.target.value)} aria-label="Mechanic">
|
|
|
|
|
<option value="">All mechanics</option>{mechanics.map((m) => <option key={m} value={m}>{m}</option>)}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-wrap gap-2 mb-3 text-xs font-medium">
|
|
|
|
|
<span className="px-3 py-1.5 rounded-lg bg-[#30638E]/10 text-[#30638E]">{shown.length} job{shown.length !== 1 && "s"}</span>
|
|
|
|
|
<span className="px-3 py-1.5 rounded-lg bg-[#00798C]/10 text-[#00798C]">{formatMinutes(totalMinutes)} on finished jobs</span>
|
Cash float card, Office role + Slips tab, job card PDF, manual close/delete
- Cash Floats: ledger card (opening balance, top-ups, slips), manual entries, slip photos
- Office access level; clock app Slips tab (photograph a slip, popup for supplier/cost
code/comments/amount); only Office sees it; per-role route table in lib/clock-access
- Job cards: Machine Service PDF (with Oil block), JC numbers, manual Close job, Delete
- Hide Expenses > Vehicle Costs and Finance (Bastion) from the menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:09:57 +00:00
|
|
|
<button onClick={() => download(null)} disabled={pdfBusy === "blank"} className="ml-auto min-h-[44px] px-3 rounded-lg border border-gray-200 dark:border-border inline-flex items-center gap-2 text-xs font-medium"><FileText className="w-4 h-4" />{pdfBusy === "blank" ? "Building…" : "Blank job card PDF"}</button>
|
2026-10-22 10:54:59 +00:00
|
|
|
{shown.some((r) => r.open) && <span className="px-3 py-1.5 rounded-lg bg-[#EDAE49]/20 text-[#8a5d00]">{shown.filter((r) => r.open).length} in progress</span>}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{shown.length === 0 ? (
|
|
|
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 py-4">
|
|
|
|
|
{rows.length === 0 ? "No job cards yet. Mechanics start one by scanning a vehicle's QR in the Plant Clock app." : "No jobs match these filters."}
|
|
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="overflow-x-auto [-webkit-overflow-scrolling:touch]">
|
Cash float card, Office role + Slips tab, job card PDF, manual close/delete
- Cash Floats: ledger card (opening balance, top-ups, slips), manual entries, slip photos
- Office access level; clock app Slips tab (photograph a slip, popup for supplier/cost
code/comments/amount); only Office sees it; per-role route table in lib/clock-access
- Job cards: Machine Service PDF (with Oil block), JC numbers, manual Close job, Delete
- Hide Expenses > Vehicle Costs and Finance (Bastion) from the menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:09:57 +00:00
|
|
|
<table className="w-full text-sm min-w-[1240px]">
|
2026-10-22 10:54:59 +00:00
|
|
|
<thead>
|
|
|
|
|
<tr className="text-left text-xs text-gray-500 dark:text-gray-400">
|
|
|
|
|
<th className="sticky left-0 bg-white dark:bg-surface py-2 pr-3 font-medium">Vehicle</th>
|
|
|
|
|
<th className="py-2 pr-3 font-medium">Mechanic</th>
|
|
|
|
|
<th className="py-2 pr-3 font-medium">Started</th>
|
|
|
|
|
<th className="py-2 pr-3 font-medium">Finished</th>
|
|
|
|
|
<th className="py-2 pr-3 font-medium text-right">Time</th>
|
|
|
|
|
<th className="py-2 pr-3 font-medium">Work</th>
|
|
|
|
|
<th className="py-2 pr-3 font-medium text-right">Odometer</th>
|
2026-10-22 11:44:01 +00:00
|
|
|
<th className="py-2 pr-3 font-medium">Oil used</th>
|
Cash float card, Office role + Slips tab, job card PDF, manual close/delete
- Cash Floats: ledger card (opening balance, top-ups, slips), manual entries, slip photos
- Office access level; clock app Slips tab (photograph a slip, popup for supplier/cost
code/comments/amount); only Office sees it; per-role route table in lib/clock-access
- Job cards: Machine Service PDF (with Oil block), JC numbers, manual Close job, Delete
- Hide Expenses > Vehicle Costs and Finance (Bastion) from the menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:09:57 +00:00
|
|
|
<th className="py-2 pr-3 font-medium">Note</th>
|
|
|
|
|
<th className="py-2 font-medium">Actions</th>
|
2026-10-22 10:54:59 +00:00
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
{shown.map((r) => (
|
|
|
|
|
<tr key={r.id} className="border-t border-gray-100 dark:border-border align-top">
|
|
|
|
|
<td className="sticky left-0 bg-white dark:bg-surface py-2 pr-3 whitespace-nowrap"><span className="font-mono font-semibold">{r.asset || "—"}</span><span className="block text-xs text-gray-500">{r.assetName}</span></td>
|
|
|
|
|
<td className="py-2 pr-3">{r.mechanic || "—"}</td>
|
|
|
|
|
<td className="py-2 pr-3 whitespace-nowrap tabular-nums">{when(r.startedAt)}</td>
|
|
|
|
|
<td className="py-2 pr-3 whitespace-nowrap tabular-nums">{r.open ? <span className="px-2 py-0.5 rounded bg-[#EDAE49]/20 text-[#8a5d00] text-xs font-medium">In progress</span> : when(r.endedAt)}</td>
|
|
|
|
|
<td className="py-2 pr-3 text-right tabular-nums whitespace-nowrap">{r.open ? "—" : formatMinutes(r.minutes)}</td>
|
|
|
|
|
<td className="py-2 pr-3 whitespace-nowrap">{jobTypeLabel(r.type) || "—"}</td>
|
|
|
|
|
<td className="py-2 pr-3 text-right tabular-nums">{r.odometer != null ? fmt(r.odometer) : "—"}</td>
|
2026-10-22 11:44:01 +00:00
|
|
|
<td className="py-2 pr-3 min-w-[140px]">{r.oil || "—"}</td>
|
Cash float card, Office role + Slips tab, job card PDF, manual close/delete
- Cash Floats: ledger card (opening balance, top-ups, slips), manual entries, slip photos
- Office access level; clock app Slips tab (photograph a slip, popup for supplier/cost
code/comments/amount); only Office sees it; per-role route table in lib/clock-access
- Job cards: Machine Service PDF (with Oil block), JC numbers, manual Close job, Delete
- Hide Expenses > Vehicle Costs and Finance (Bastion) from the menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:09:57 +00:00
|
|
|
<td className="py-2 pr-3 min-w-[240px] whitespace-pre-wrap break-words">{r.note || "—"}</td>
|
|
|
|
|
<td className="py-2">
|
|
|
|
|
<div className="flex items-center gap-1 whitespace-nowrap">
|
|
|
|
|
<button onClick={() => download(r)} disabled={pdfBusy === r.id} aria-label={`Download job card PDF for ${r.asset}`} className="min-h-[44px] px-3 rounded-lg border border-gray-200 dark:border-border text-xs font-medium inline-flex items-center gap-1.5"><Download className="w-3.5 h-3.5" />PDF</button>
|
|
|
|
|
{r.open && <button onClick={() => openClose(r)} className="min-h-[44px] px-3 rounded-lg bg-[#30638E] text-white text-xs font-medium">Close job</button>}
|
|
|
|
|
<button onClick={() => { setDialogError(""); setDeleting(r); }} className="min-h-[44px] px-3 rounded-lg text-xs text-[#D1495B] hover:bg-[#D1495B]/10">Delete</button>
|
|
|
|
|
</div>
|
|
|
|
|
</td>
|
2026-10-22 10:54:59 +00:00
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
Cash float card, Office role + Slips tab, job card PDF, manual close/delete
- Cash Floats: ledger card (opening balance, top-ups, slips), manual entries, slip photos
- Office access level; clock app Slips tab (photograph a slip, popup for supplier/cost
code/comments/amount); only Office sees it; per-role route table in lib/clock-access
- Job cards: Machine Service PDF (with Oil block), JC numbers, manual Close job, Delete
- Hide Expenses > Vehicle Costs and Finance (Bastion) from the menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 13:09:57 +00:00
|
|
|
{closing && (
|
|
|
|
|
<div className="fixed inset-0 z-50 grid place-items-center bg-black/50 p-4" onClick={() => !working && setClosing(null)}>
|
|
|
|
|
<form onClick={(e) => e.stopPropagation()} onSubmit={submitClose} role="dialog" aria-modal="true" aria-label="Close job"
|
|
|
|
|
className="w-full max-w-md max-h-[90svh] overflow-y-auto rounded-2xl bg-white dark:bg-surface p-5 shadow-xl">
|
|
|
|
|
<h3 className="text-base font-semibold text-[#003D5B] dark:text-white">Close job on {closing.asset}</h3>
|
|
|
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">Started by {closing.mechanic || "—"} · {when(closing.startedAt)}. Use this when the mechanic did not scan the second time.</p>
|
|
|
|
|
<p className="text-sm font-medium mt-4">Kind of work</p>
|
|
|
|
|
<div className="grid grid-cols-3 gap-2 mt-1" role="radiogroup" aria-label="Kind of work">
|
|
|
|
|
{JOB_TYPES.map((t) => (
|
|
|
|
|
<button key={t.id} type="button" role="radio" aria-checked={cType === t.id} onClick={() => setCType(t.id)}
|
|
|
|
|
className={`min-h-[48px] rounded-lg text-xs font-medium border ${cType === t.id ? "bg-[#30638E] text-white border-[#30638E]" : "border-gray-200 dark:border-border"}`}>{t.label}</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
<label htmlFor="c-note" className="block text-sm font-medium mt-4">What was done? <span className="font-normal text-gray-500">(optional)</span></label>
|
|
|
|
|
<textarea id="c-note" value={cNote} onChange={(e) => setCNote(e.target.value)} maxLength={1000} className="mt-1 w-full min-h-[90px] rounded-lg border border-gray-200 dark:border-border bg-white dark:bg-surface p-2 text-sm" />
|
|
|
|
|
<label htmlFor="c-odo" className="block text-sm font-medium mt-3">Odometer / hour meter <span className="font-normal text-gray-500">(optional)</span></label>
|
|
|
|
|
<input id="c-odo" type="number" inputMode="decimal" min={0} step="any" value={cOdo} onChange={(e) => setCOdo(e.target.value)} className="mt-1 w-full min-h-[44px] rounded-lg border border-gray-200 dark:border-border bg-white dark:bg-surface px-3 text-sm" />
|
|
|
|
|
<label htmlFor="c-end" className="block text-sm font-medium mt-3">Finished at <span className="font-normal text-gray-500">(blank = now)</span></label>
|
|
|
|
|
<input id="c-end" type="datetime-local" value={cEnd} onChange={(e) => setCEnd(e.target.value)} className="mt-1 w-full min-h-[44px] rounded-lg border border-gray-200 dark:border-border bg-white dark:bg-surface px-3 text-sm" />
|
|
|
|
|
{dialogError && <p role="alert" className="text-xs text-[#D1495B] mt-3">{dialogError}</p>}
|
|
|
|
|
<div className="flex justify-end gap-2 mt-5">
|
|
|
|
|
<button type="button" disabled={working} onClick={() => setClosing(null)} className="min-h-[44px] px-4 rounded-lg text-sm border border-gray-200 dark:border-border">Cancel</button>
|
|
|
|
|
<button type="submit" disabled={working || !cType} className="min-h-[44px] px-5 rounded-lg text-sm font-medium text-white bg-[#30638E] disabled:opacity-60">{working ? "Closing…" : "Close job"}</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{deleting && (
|
|
|
|
|
<div className="fixed inset-0 z-50 grid place-items-center bg-black/50 p-4" onClick={() => !working && setDeleting(null)}>
|
|
|
|
|
<div onClick={(e) => e.stopPropagation()} role="alertdialog" aria-modal="true" aria-label="Delete job card" className="w-full max-w-sm rounded-2xl bg-white dark:bg-surface p-5 shadow-xl">
|
|
|
|
|
<h3 className="text-base font-semibold text-[#003D5B] dark:text-white">Delete this job card?</h3>
|
|
|
|
|
<p className="text-sm text-gray-600 dark:text-gray-300 mt-2">{deleting.asset} · {deleting.mechanic || "—"} · {when(deleting.startedAt)}{deleting.open ? " (in progress)" : ""}.</p>
|
|
|
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">Any oil booked onto it stays booked out of stock; it just stops belonging to this card. This cannot be undone.</p>
|
|
|
|
|
{dialogError && <p role="alert" className="text-xs text-[#D1495B] mt-3">{dialogError}</p>}
|
|
|
|
|
<div className="flex justify-end gap-2 mt-5">
|
|
|
|
|
<button disabled={working} onClick={() => setDeleting(null)} className="min-h-[44px] px-4 rounded-lg text-sm border border-gray-200 dark:border-border">Cancel</button>
|
|
|
|
|
<button disabled={working} onClick={confirmDelete} className="min-h-[44px] px-5 rounded-lg text-sm font-medium text-white bg-[#D1495B] disabled:opacity-60">{working ? "Deleting…" : "Delete"}</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-10-22 10:54:59 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|