Files
lab/src/app/(dashboard)/jobs/_components/jobs-table.tsx
T
kovakmedya d7d2ac557b feat(jobs): due-date awareness — DueBadge + dashboard 'Geciken İşler' widget
dueDate was sitting on every job but the UI never warned anyone about
it. Added a small badge primitive and surfaced it everywhere a job is
listed plus a dedicated dashboard card.

  - lib/appwrite/due-date.ts: dueState() buckets a job into overdue /
    today / soon (1-3 days) / future / none, with delivered + cancelled
    jobs always resolving to none. dueLabel() returns the Turkish text.
  - components/due-badge.tsx: renders nothing for future/none, a
    secondary badge for today/soon, a destructive badge for overdue.
    Drop-in (job, className).
  - JobsTable (inbound + outbound): new 'Termin' column shows the date
    and the DueBadge stacked. Sorting still on createdAt for now —
    explicit ordering by dueDate will come with the filter task.
  - Job detail header: badge stack now has the DueBadge before the
    status pill so a glance at the page shows whether the case is
    behind schedule.
  - Dashboard: getDashboardData fetches up to 10 overdue jobs in
    parallel (lessThan('dueDate', now), excluding delivered/cancelled).
    Added a destructive-tinted 'Geciken İşler' card above the rest of
    the widgets when the list is non-empty, with quick links into each
    job's detail page.
2026-05-22 16:02:13 +03:00

109 lines
3.2 KiB
TypeScript

import Link from "next/link";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { DueBadge } from "@/components/due-badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
JOB_STATUS_LABELS,
PROSTHETIC_TYPE_LABELS,
} from "@/lib/appwrite/job-types";
import type { JobWithCounterpart } from "@/lib/appwrite/job-queries";
import type { JobStatus } from "@/lib/appwrite/schema";
const dateFormatter = new Intl.DateTimeFormat("tr-TR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
function statusVariant(status: JobStatus): "default" | "secondary" | "outline" | "destructive" {
switch (status) {
case "delivered":
return "default";
case "sent":
return "secondary";
case "in_progress":
return "secondary";
case "cancelled":
return "destructive";
default:
return "outline";
}
}
export function JobsTable({
rows,
counterpartLabel,
emptyMessage,
}: {
rows: JobWithCounterpart[];
counterpartLabel: string;
emptyMessage: string;
}) {
if (rows.length === 0) {
return (
<p className="text-muted-foreground py-6 text-center text-sm">{emptyMessage}</p>
);
}
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>{counterpartLabel}</TableHead>
<TableHead>Hasta Kodu</TableHead>
<TableHead>Üye</TableHead>
<TableHead>Renk</TableHead>
<TableHead>Tür</TableHead>
<TableHead>Durum</TableHead>
<TableHead>Termin</TableHead>
<TableHead>Tarih</TableHead>
<TableHead className="text-right">İşlem</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((j) => (
<TableRow key={j.$id}>
<TableCell className="font-medium">{j.counterpart?.companyName ?? "—"}</TableCell>
<TableCell className="font-mono text-xs">{j.patientCode}</TableCell>
<TableCell className="tabular-nums">{j.memberCount}</TableCell>
<TableCell className="text-muted-foreground">{j.color || "—"}</TableCell>
<TableCell className="text-muted-foreground">
{PROSTHETIC_TYPE_LABELS[j.prostheticType] ?? j.prostheticType}
</TableCell>
<TableCell>
<Badge variant={statusVariant(j.status)}>{JOB_STATUS_LABELS[j.status]}</Badge>
</TableCell>
<TableCell className="text-muted-foreground text-xs">
{j.dueDate ? (
<div className="flex flex-col gap-1">
<span>{dateFormatter.format(new Date(j.dueDate))}</span>
<DueBadge job={j} />
</div>
) : (
"—"
)}
</TableCell>
<TableCell className="text-muted-foreground text-xs">
{dateFormatter.format(new Date(j.$createdAt))}
</TableCell>
<TableCell className="text-right">
<Button asChild size="sm" variant="outline">
<Link href={`/jobs/${j.$id}`}>Detay</Link>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}