diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx
index 5e376a0..f424d6c 100644
--- a/src/app/(dashboard)/dashboard/page.tsx
+++ b/src/app/(dashboard)/dashboard/page.tsx
@@ -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() {
/>
+ {data.overdueJobs.length > 0 && (
+
+
+
+
+ Geciken İşler ({data.overdueJobs.length})
+
+
+ Termin tarihi geçmiş ve henüz teslim edilmemiş işler.
+
+
+
+
+ {data.overdueJobs.map((j) => (
+ -
+
+ {j.counterpartName ?? "—"}
+ ·
+ {j.patientCode}
+
+
+
+ ))}
+
+
+
+ )}
+
{isClinic && data.approvedConnectionsCount === 0 && (
diff --git a/src/app/(dashboard)/jobs/[jobId]/page.tsx b/src/app/(dashboard)/jobs/[jobId]/page.tsx
index adbedd9..252301e 100644
--- a/src/app/(dashboard)/jobs/[jobId]/page.tsx
+++ b/src/app/(dashboard)/jobs/[jobId]/page.tsx
@@ -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({
-
- {JOB_STATUS_LABELS[job.status]}
-
+
+
+
+ {JOB_STATUS_LABELS[job.status]}
+
+
diff --git a/src/app/(dashboard)/jobs/_components/jobs-table.tsx b/src/app/(dashboard)/jobs/_components/jobs-table.tsx
index 7e87568..40749a2 100644
--- a/src/app/(dashboard)/jobs/_components/jobs-table.tsx
+++ b/src/app/(dashboard)/jobs/_components/jobs-table.tsx
@@ -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({
Renk
Tür
Durum
+ Termin
Tarih
İşlem
@@ -80,6 +82,16 @@ export function JobsTable({
{JOB_STATUS_LABELS[j.status]}
+
+ {j.dueDate ? (
+
+ {dateFormatter.format(new Date(j.dueDate))}
+
+
+ ) : (
+ "—"
+ )}
+
{dateFormatter.format(new Date(j.$createdAt))}
diff --git a/src/components/due-badge.tsx b/src/components/due-badge.tsx
new file mode 100644
index 0000000..d553d90
--- /dev/null
+++ b/src/components/due-badge.tsx
@@ -0,0 +1,27 @@
+import { AlertCircle, Clock } from "lucide-react";
+
+import { Badge } from "@/components/ui/badge";
+import { dueLabel, dueState } from "@/lib/appwrite/due-date";
+import type { Job } from "@/lib/appwrite/schema";
+
+export function DueBadge({
+ job,
+ className,
+}: {
+ job: Pick;
+ className?: string;
+}) {
+ const state = dueState(job);
+ if (state.kind === "none" || state.kind === "future") return null;
+ const variant: "destructive" | "secondary" = state.kind === "overdue" ? "destructive" : "secondary";
+ return (
+
+ {state.kind === "overdue" ? (
+
+ ) : (
+
+ )}
+ {dueLabel(state)}
+
+ );
+}
diff --git a/src/lib/appwrite/dashboard-queries.ts b/src/lib/appwrite/dashboard-queries.ts
index a203e3c..7221e0b 100644
--- a/src/lib/appwrite/dashboard-queries.ts
+++ b/src/lib/appwrite/dashboard-queries.ts
@@ -28,6 +28,8 @@ export type DashboardData = {
approvedConnectionsCount: number;
recentJobs: DashboardJob[];
recentNotifications: Notification[];
+ /** Open jobs whose dueDate has already passed. */
+ overdueJobs: DashboardJob[];
};
export async function getDashboardData(
@@ -41,8 +43,16 @@ export async function getDashboardData(
// count separately for the stat card.
const jobsField = isLab ? "labTenantId" : "clinicTenantId";
- const [recentJobsRes, openJobsRes, pendingActionRes, financeRes, notifRes, unreadRes, connRes] =
- await Promise.all([
+ const [
+ recentJobsRes,
+ openJobsRes,
+ pendingActionRes,
+ financeRes,
+ notifRes,
+ unreadRes,
+ connRes,
+ overdueRes,
+ ] = await Promise.all([
tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.jobs,
@@ -110,12 +120,27 @@ export async function getDashboardData(
Query.limit(1),
],
}),
+ tablesDB.listRows({
+ databaseId: DATABASE_ID,
+ tableId: TABLES.jobs,
+ queries: [
+ Query.equal(jobsField, tenantId),
+ Query.notEqual("status", "delivered"),
+ Query.notEqual("status", "cancelled"),
+ Query.lessThan("dueDate", new Date().toISOString()),
+ Query.orderAsc("dueDate"),
+ Query.limit(10),
+ ],
+ }),
]);
const recentJobs = recentJobsRes.rows as unknown as Job[];
+ const overdueJobs = overdueRes.rows as unknown as Job[];
const counterpartIds = Array.from(
new Set(
- recentJobs.map((j) => (isLab ? j.clinicTenantId : j.labTenantId)).filter(Boolean),
+ [...recentJobs, ...overdueJobs]
+ .map((j) => (isLab ? j.clinicTenantId : j.labTenantId))
+ .filter(Boolean),
),
);
@@ -155,5 +180,10 @@ export async function getDashboardData(
counterpartMap.get(isLab ? j.clinicTenantId : j.labTenantId) ?? null,
})),
recentNotifications: notifRes.rows as unknown as Notification[],
+ overdueJobs: overdueJobs.map((j) => ({
+ ...j,
+ counterpartName:
+ counterpartMap.get(isLab ? j.clinicTenantId : j.labTenantId) ?? null,
+ })),
});
}
diff --git a/src/lib/appwrite/due-date.ts b/src/lib/appwrite/due-date.ts
new file mode 100644
index 0000000..5454ea5
--- /dev/null
+++ b/src/lib/appwrite/due-date.ts
@@ -0,0 +1,56 @@
+import type { Job } from "./schema";
+
+export type DueState =
+ | { kind: "none" }
+ | { kind: "future"; days: number }
+ | { kind: "soon"; days: number }
+ | { kind: "today" }
+ | { kind: "overdue"; days: number };
+
+const MS_PER_DAY = 24 * 60 * 60 * 1000;
+
+/**
+ * Bucket a job's due date into a UI-friendly label.
+ * - none → no due date
+ * - future → more than 3 days away
+ * - soon → 1-3 days away (warn)
+ * - today → today (warn)
+ * - overdue → past, work isn't delivered (error)
+ *
+ * Cancelled or delivered jobs always resolve to 'none' — nothing to warn
+ * about once the case is closed.
+ */
+export function dueState(
+ job: Pick,
+ now: Date = new Date(),
+): DueState {
+ if (!job.dueDate) return { kind: "none" };
+ if (job.status === "delivered" || job.status === "cancelled") {
+ return { kind: "none" };
+ }
+ const due = new Date(job.dueDate);
+ // Compare at day granularity so a deadline at 23:59 isn't 'overdue' a
+ // few seconds in.
+ const dueDay = Date.UTC(due.getUTCFullYear(), due.getUTCMonth(), due.getUTCDate());
+ const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
+ const diffDays = Math.round((dueDay - today) / MS_PER_DAY);
+ if (diffDays < 0) return { kind: "overdue", days: Math.abs(diffDays) };
+ if (diffDays === 0) return { kind: "today" };
+ if (diffDays <= 3) return { kind: "soon", days: diffDays };
+ return { kind: "future", days: diffDays };
+}
+
+export function dueLabel(state: DueState): string {
+ switch (state.kind) {
+ case "overdue":
+ return state.days === 1 ? "1 gün gecikti" : `${state.days} gün gecikti`;
+ case "today":
+ return "Bugün teslim";
+ case "soon":
+ return state.days === 1 ? "Yarın teslim" : `${state.days} gün kaldı`;
+ case "future":
+ return `${state.days} gün kaldı`;
+ case "none":
+ return "";
+ }
+}