feat: add transfer UI for Assignment
Per-row employee <Select> + Transfer button alongside the existing Return
button, shown for active (returned_at null) rows. PATCHes
/api/assignments/[id] with {employeeId}, which the route already dispatches
to transferAssignment() vs returnAssignment() based on body shape.
This commit is contained in:
@@ -88,6 +88,14 @@ export default function AssignmentsPage() {
|
||||
const [returnErrors, setReturnErrors] = useState<Record<string, string>>({});
|
||||
const [returningId, setReturningId] = useState<string | null>(null);
|
||||
|
||||
// Per-row transfer mini-form state, keyed by assignment id — same shape as
|
||||
// maintenance-records/page.tsx's close mini-form state (separate maps
|
||||
// rather than one nested-object map, so each field stays a plain string
|
||||
// setter).
|
||||
const [transferEmployeeId, setTransferEmployeeId] = useState<Record<string, string>>({});
|
||||
const [transferErrors, setTransferErrors] = useState<Record<string, string>>({});
|
||||
const [transferringId, setTransferringId] = useState<string | null>(null);
|
||||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setSubmitError(null);
|
||||
@@ -183,6 +191,61 @@ export default function AssignmentsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTransfer(id: string) {
|
||||
setTransferringId(id);
|
||||
setTransferErrors((previous) => {
|
||||
const next = { ...previous };
|
||||
delete next[id];
|
||||
return next;
|
||||
});
|
||||
|
||||
const employeeId = transferEmployeeId[id] ?? "";
|
||||
|
||||
try {
|
||||
// PATCH /api/assignments/[id]'s body is `{employeeId}` for a transfer
|
||||
// — see src/app/api/assignments/[id]/route.ts's dispatch (a body
|
||||
// carrying employeeId is a transfer, everything else is a return).
|
||||
// employeeId is sent even if unselected (empty string), which the
|
||||
// server rejects with a clean 400 message surfaced below, same as any
|
||||
// other validation failure.
|
||||
const response = await fetch(`/api/assignments/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ employeeId }),
|
||||
});
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await response.json();
|
||||
} catch {
|
||||
body = null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const message =
|
||||
body !== null &&
|
||||
typeof body === "object" &&
|
||||
"error" in body &&
|
||||
typeof (body as { error: unknown }).error === "string"
|
||||
? (body as { error: string }).error
|
||||
: "Request failed";
|
||||
setTransferErrors((previous) => ({ ...previous, [id]: message }));
|
||||
return;
|
||||
}
|
||||
|
||||
setTransferEmployeeId((previous) => {
|
||||
const next = { ...previous };
|
||||
delete next[id];
|
||||
return next;
|
||||
});
|
||||
refetch();
|
||||
} catch {
|
||||
setTransferErrors((previous) => ({ ...previous, [id]: "Request failed" }));
|
||||
} finally {
|
||||
setTransferringId(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 p-6">
|
||||
<h1 className="text-xl font-semibold">Assignments</h1>
|
||||
@@ -220,6 +283,7 @@ export default function AssignmentsPage() {
|
||||
<TableCell>{assignment.created_at}</TableCell>
|
||||
<TableCell>
|
||||
{assignment.returned_at === null ? (
|
||||
<div className="flex w-56 flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
@@ -236,6 +300,47 @@ export default function AssignmentsPage() {
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Select
|
||||
value={transferEmployeeId[assignment.id] ?? ""}
|
||||
onValueChange={(value) =>
|
||||
setTransferEmployeeId((previous) => ({
|
||||
...previous,
|
||||
[assignment.id]: value,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Transfer to..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{employees.map((employee) => (
|
||||
<SelectItem key={employee.id} value={employee.id}>
|
||||
{employee.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={
|
||||
transferringId === assignment.id ||
|
||||
!transferEmployeeId[assignment.id]
|
||||
}
|
||||
onClick={() => handleTransfer(assignment.id)}
|
||||
>
|
||||
{transferringId === assignment.id ? "Transferring..." : "Transfer"}
|
||||
</Button>
|
||||
{transferErrors[assignment.id] ? (
|
||||
<p className="text-xs text-destructive">
|
||||
{transferErrors[assignment.id]}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user