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:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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[],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user