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.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ArrowRight, FlaskConical, Link2, Plus, Stethoscope } from "lucide-react";
|
||||
import { AlertCircle, ArrowRight, FlaskConical, Link2, Plus, Stethoscope } from "lucide-react";
|
||||
|
||||
import { DueBadge } from "@/components/due-badge";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -109,6 +111,34 @@ export default async function DashboardPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{data.overdueJobs.length > 0 && (
|
||||
<Card className="border-destructive/40 bg-destructive/5">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<AlertCircle className="text-destructive size-4" />
|
||||
Geciken İşler ({data.overdueJobs.length})
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Termin tarihi geçmiş ve henüz teslim edilmemiş işler.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="divide-y rounded-md border bg-background">
|
||||
{data.overdueJobs.map((j) => (
|
||||
<li key={j.$id} className="flex items-center gap-3 px-3 py-2 text-sm">
|
||||
<Link href={`/jobs/${j.$id}`} className="flex-1 min-w-0 hover:underline">
|
||||
<span className="font-medium">{j.counterpartName ?? "—"}</span>
|
||||
<span className="text-muted-foreground"> · </span>
|
||||
<span className="font-mono text-xs">{j.patientCode}</span>
|
||||
</Link>
|
||||
<DueBadge job={j} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{isClinic && data.approvedConnectionsCount === 0 && (
|
||||
<Card className="border-primary/20 bg-primary/5">
|
||||
<CardHeader>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { notFound, redirect } from "next/navigation";
|
||||
import { Query } from "node-appwrite";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { DueBadge } from "@/components/due-badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { listJobFiles } from "@/lib/appwrite/job-file-queries";
|
||||
@@ -116,9 +117,12 @@ export default async function JobDetailPage({
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-3">
|
||||
<Badge variant="secondary" className="text-sm">
|
||||
{JOB_STATUS_LABELS[job.status]}
|
||||
</Badge>
|
||||
<div className="flex items-center gap-2">
|
||||
<DueBadge job={job} />
|
||||
<Badge variant="secondary" className="text-sm">
|
||||
{JOB_STATUS_LABELS[job.status]}
|
||||
</Badge>
|
||||
</div>
|
||||
<JobActionsPanel job={job} side={side} kind={ctx.kind} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ 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,
|
||||
@@ -63,6 +64,7 @@ export function JobsTable({
|
||||
<TableHead>Renk</TableHead>
|
||||
<TableHead>Tür</TableHead>
|
||||
<TableHead>Durum</TableHead>
|
||||
<TableHead>Termin</TableHead>
|
||||
<TableHead>Tarih</TableHead>
|
||||
<TableHead className="text-right">İşlem</TableHead>
|
||||
</TableRow>
|
||||
@@ -80,6 +82,16 @@ export function JobsTable({
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user