feat(dashboard): wire Anasayfa to live data

- getDashboardData aggregates open jobs, pending-action jobs, unread
  notifications, pending finance totals, approved connection count, recent
  jobs (up to 8) and recent notifications (up to 5) — single Promise.all so
  the dashboard renders in one round-trip.
- Four stat cards, each a Link to the relevant module; tone (positive /
  negative) flips between clinic (payable) and lab (receivable).
- Clinic users with zero approved connections see a 'Bağlantı Kur' prompt
  card so they don't get stuck on /jobs/new.
- Recent jobs table is role-aware: lab sees Klinik column + 'Son Gelen
  İşler' header, clinic sees Laboratuvar column + 'Son Giden İşler' header.
- Recent notifications panel with read/unread dot, clickable header arrow
  to /notifications.
- ActiveContext now carries 'kind' (mirror of TenantSettings.kind) so we no
  longer reach into ctx.settings?.kind in callers.
This commit is contained in:
kovakmedya
2026-05-21 20:41:39 +03:00
parent 97f397d2dd
commit c980ce1d8d
3 changed files with 402 additions and 19 deletions
+3 -1
View File
@@ -3,12 +3,13 @@ import "server-only";
import { Query } from "node-appwrite";
import { createAdminClient, getCurrentUser } from "./server";
import { DATABASE_ID, TABLES, type TenantSettings } from "./schema";
import { DATABASE_ID, TABLES, type TenantKind, type TenantSettings } from "./schema";
import { getActiveTenantId, getUserTeams } from "./tenant";
export type ActiveContext = {
user: { id: string; name: string; email: string };
tenantId: string;
kind: TenantKind | null;
settings: TenantSettings | null;
};
@@ -39,6 +40,7 @@ export async function getActiveContext(): Promise<ActiveContext | null> {
return {
user: { id: user.$id, name: user.name, email: user.email },
tenantId,
kind: settings?.kind ?? null,
settings,
};
}
+158
View File
@@ -0,0 +1,158 @@
import "server-only";
import { Query } from "node-appwrite";
import {
DATABASE_ID,
TABLES,
type FinanceEntry,
type Job,
type Notification,
type TenantKind,
type TenantSettings,
} from "./schema";
import { createAdminClient } from "./server";
export type DashboardJob = Job & {
counterpartName: string | null;
};
export type DashboardData = {
openJobsCount: number;
pendingActionCount: number; // jobs awaiting MY action
unreadCount: number;
receivablePending: number; // lab perspective
payablePending: number; // clinic perspective
currency: string;
approvedConnectionsCount: number;
recentJobs: DashboardJob[];
recentNotifications: Notification[];
};
export async function getDashboardData(
tenantId: string,
kind: TenantKind | null,
): Promise<DashboardData> {
const { tablesDB } = createAdminClient();
const isLab = kind === "lab";
// Jobs that involve this tenant — limit to 10 most recent for the list,
// count separately for the stat card.
const jobsField = isLab ? "labTenantId" : "clinicTenantId";
const [recentJobsRes, openJobsRes, pendingActionRes, financeRes, notifRes, unreadRes, connRes] =
await Promise.all([
tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.jobs,
queries: [
Query.equal(jobsField, tenantId),
Query.orderDesc("$createdAt"),
Query.limit(8),
],
}),
tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.jobs,
queries: [
Query.equal(jobsField, tenantId),
Query.notEqual("status", "delivered"),
Query.notEqual("status", "cancelled"),
Query.limit(1),
],
}),
tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.jobs,
queries: [
Query.equal(jobsField, tenantId),
Query.equal("status", isLab ? "pending" : "sent"),
Query.limit(1),
],
}),
tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.financeEntries,
queries: [
Query.equal("tenantId", tenantId),
Query.equal("status", "pending"),
Query.limit(200),
],
}),
tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.notifications,
queries: [
Query.equal("tenantId", tenantId),
Query.orderDesc("$createdAt"),
Query.limit(5),
],
}),
tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.notifications,
queries: [
Query.equal("tenantId", tenantId),
Query.equal("read", false),
Query.limit(1),
],
}),
tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.connections,
queries: [
Query.or([
Query.equal("clinicTenantId", tenantId),
Query.equal("labTenantId", tenantId),
]),
Query.equal("status", "approved"),
Query.limit(1),
],
}),
]);
const recentJobs = recentJobsRes.rows as unknown as Job[];
const counterpartIds = Array.from(
new Set(
recentJobs.map((j) => (isLab ? j.clinicTenantId : j.labTenantId)).filter(Boolean),
),
);
const counterpartMap = new Map<string, string>();
if (counterpartIds.length > 0) {
const counterpartsRes = await tablesDB.listRows({
databaseId: DATABASE_ID,
tableId: TABLES.tenantSettings,
queries: [Query.equal("tenantId", counterpartIds), Query.limit(200)],
});
for (const s of counterpartsRes.rows as unknown as TenantSettings[]) {
counterpartMap.set(s.tenantId, s.companyName);
}
}
const finance = financeRes.rows as unknown as FinanceEntry[];
let receivablePending = 0;
let payablePending = 0;
let currency = "TRY";
for (const e of finance) {
if (e.currency) currency = e.currency;
if (e.type === "receivable") receivablePending += e.amount;
if (e.type === "payable") payablePending += e.amount;
}
return {
openJobsCount: openJobsRes.total,
pendingActionCount: pendingActionRes.total,
unreadCount: unreadRes.total,
receivablePending,
payablePending,
currency,
approvedConnectionsCount: connRes.total,
recentJobs: recentJobs.map((j) => ({
...j,
counterpartName:
counterpartMap.get(isLab ? j.clinicTenantId : j.labTenantId) ?? null,
})),
recentNotifications: notifRes.rows as unknown as Notification[],
};
}