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:
@@ -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,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<Job, "dueDate" | "status">,
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user