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. + + + + + + + )} + {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 ""; + } +}