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:
@@ -1,56 +1,279 @@
|
|||||||
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { ArrowRight, FlaskConical, Link2, Plus, Stethoscope } from "lucide-react";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
import { getActiveContext } from "@/lib/appwrite/active-context";
|
import { getActiveContext } from "@/lib/appwrite/active-context";
|
||||||
|
import { getDashboardData } from "@/lib/appwrite/dashboard-queries";
|
||||||
|
import { JOB_STATUS_LABELS, PROSTHETIC_TYPE_LABELS } from "@/lib/appwrite/job-types";
|
||||||
|
import type { JobStatus } from "@/lib/appwrite/schema";
|
||||||
|
|
||||||
|
const dateFormatter = new Intl.DateTimeFormat("tr-TR", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
const datetimeFormatter = new Intl.DateTimeFormat("tr-TR", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatMoney(amount: number, currency: string): string {
|
||||||
|
try {
|
||||||
|
return new Intl.NumberFormat("tr-TR", { style: "currency", currency }).format(amount);
|
||||||
|
} catch {
|
||||||
|
return `${amount.toFixed(2)} ${currency}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusVariant(s: JobStatus): "default" | "secondary" | "outline" | "destructive" {
|
||||||
|
if (s === "delivered") return "default";
|
||||||
|
if (s === "sent" || s === "in_progress") return "secondary";
|
||||||
|
if (s === "cancelled") return "destructive";
|
||||||
|
return "outline";
|
||||||
|
}
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export default async function DashboardPage() {
|
||||||
const ctx = await getActiveContext();
|
const ctx = await getActiveContext();
|
||||||
if (!ctx) redirect("/onboarding");
|
if (!ctx) redirect("/onboarding");
|
||||||
|
|
||||||
|
const data = await getDashboardData(ctx.tenantId, ctx.kind);
|
||||||
const firstName = ctx.user.name?.split(" ")[0] ?? "";
|
const firstName = ctx.user.name?.split(" ")[0] ?? "";
|
||||||
const companyName = ctx.settings?.companyName ?? "Çalışma alanı";
|
const companyName = ctx.settings?.companyName ?? "Çalışma alanı";
|
||||||
|
const isLab = ctx.kind === "lab";
|
||||||
|
const isClinic = ctx.kind === "clinic";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 space-y-6 px-6 pt-0">
|
<div className="flex-1 space-y-6 px-6 pt-0">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<p className="text-muted-foreground text-sm">{companyName}</p>
|
<p className="text-muted-foreground flex items-center gap-2 text-sm">
|
||||||
|
{isLab ? <FlaskConical className="size-3.5" /> : <Stethoscope className="size-3.5" />}
|
||||||
|
{companyName}
|
||||||
|
</p>
|
||||||
<h1 className="text-2xl font-bold tracking-tight">
|
<h1 className="text-2xl font-bold tracking-tight">
|
||||||
{firstName ? `Hoş geldiniz, ${firstName}` : "Anasayfa"}
|
{firstName ? `Hoş geldiniz, ${firstName}` : "Anasayfa"}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
Açık işleri, bildirimleri ve istatistikleri buradan takip edin.
|
Açık işleri, finansal akışı ve bildirimleri buradan takip edin.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||||
<Card>
|
<StatCard
|
||||||
|
label="Açık işler"
|
||||||
|
value={String(data.openJobsCount)}
|
||||||
|
hint={
|
||||||
|
data.pendingActionCount > 0
|
||||||
|
? `${data.pendingActionCount} kalem sizden eylem bekliyor`
|
||||||
|
: "Hepsi yolunda"
|
||||||
|
}
|
||||||
|
href={isLab ? "/jobs/inbound" : "/jobs/outbound"}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label={isLab ? "Bekleyen alacak" : "Bekleyen borç"}
|
||||||
|
value={formatMoney(
|
||||||
|
isLab ? data.receivablePending : data.payablePending,
|
||||||
|
data.currency,
|
||||||
|
)}
|
||||||
|
hint="Tahsilat / ödeme bekliyor"
|
||||||
|
href="/finance"
|
||||||
|
tone={isLab ? "positive" : "negative"}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Okunmamış bildirim"
|
||||||
|
value={String(data.unreadCount)}
|
||||||
|
hint={data.unreadCount > 0 ? "Yeni etkinlikler var" : "Hepsi okundu"}
|
||||||
|
href="/notifications"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Bağlantı"
|
||||||
|
value={String(data.approvedConnectionsCount)}
|
||||||
|
hint={
|
||||||
|
data.approvedConnectionsCount > 0
|
||||||
|
? `Onaylı ${isLab ? "klinik" : "laboratuvar"}`
|
||||||
|
: "Henüz bağlantınız yok"
|
||||||
|
}
|
||||||
|
href="/connections"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isClinic && data.approvedConnectionsCount === 0 && (
|
||||||
|
<Card className="border-primary/20 bg-primary/5">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Açık işler</CardTitle>
|
<CardTitle className="text-base">Bir laboratuvarla bağlantı kurun</CardTitle>
|
||||||
<CardDescription>Gelen ve giden iş özetleri burada listelenecek.</CardDescription>
|
<CardDescription>
|
||||||
|
İş gönderebilmeniz için en az bir onaylı laboratuvar bağlantınız olmalı.
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-muted-foreground text-sm">
|
<CardContent>
|
||||||
Modül yapım aşamasında.
|
<Button asChild>
|
||||||
|
<Link href="/connections">
|
||||||
|
<Link2 className="size-4" />
|
||||||
|
Bağlantı Kur
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid gap-6 xl:grid-cols-[2fr_1fr]">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-start justify-between">
|
||||||
<CardTitle>İşlem bekleyen</CardTitle>
|
<div>
|
||||||
<CardDescription>Onay/işlem bekleyen kalemler.</CardDescription>
|
<CardTitle>{isLab ? "Son Gelen İşler" : "Son Giden İşler"}</CardTitle>
|
||||||
|
<CardDescription>En son 8 kayıt.</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{isClinic && (
|
||||||
|
<Button asChild size="sm">
|
||||||
|
<Link href="/jobs/new">
|
||||||
|
<Plus className="size-4" />
|
||||||
|
Yeni İş
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button asChild size="sm" variant="outline">
|
||||||
|
<Link href={isLab ? "/jobs/inbound" : "/jobs/outbound"}>
|
||||||
|
Tümü
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-muted-foreground text-sm">
|
<CardContent>
|
||||||
Modül yapım aşamasında.
|
{data.recentJobs.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground py-6 text-center text-sm">
|
||||||
|
Henüz iş kaydı yok.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>{isLab ? "Klinik" : "Laboratuvar"}</TableHead>
|
||||||
|
<TableHead>Hasta</TableHead>
|
||||||
|
<TableHead>Tür</TableHead>
|
||||||
|
<TableHead>Durum</TableHead>
|
||||||
|
<TableHead>Tarih</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data.recentJobs.map((j) => (
|
||||||
|
<TableRow
|
||||||
|
key={j.$id}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onClick={undefined}
|
||||||
|
>
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
<Link href={`/jobs/${j.$id}`} className="hover:underline">
|
||||||
|
{j.counterpartName ?? "—"}
|
||||||
|
</Link>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">{j.patientCode}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground text-xs">
|
||||||
|
{PROSTHETIC_TYPE_LABELS[j.prostheticType]}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={statusVariant(j.status)}>
|
||||||
|
{JOB_STATUS_LABELS[j.status]}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground text-xs">
|
||||||
|
{dateFormatter.format(new Date(j.$createdAt))}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="flex flex-row items-start justify-between">
|
||||||
<CardTitle>Bildirimler</CardTitle>
|
<div>
|
||||||
<CardDescription>Bağlantılarınızdan gelen son bildirimler.</CardDescription>
|
<CardTitle>Son Bildirimler</CardTitle>
|
||||||
|
<CardDescription>Bağlantı ve iş etkinlikleri.</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button asChild size="sm" variant="outline">
|
||||||
|
<Link href="/notifications">
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-muted-foreground text-sm">
|
<CardContent>
|
||||||
Modül yapım aşamasında.
|
{data.recentNotifications.length === 0 ? (
|
||||||
|
<p className="text-muted-foreground py-6 text-center text-sm">
|
||||||
|
Bildirim yok.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul className="divide-y">
|
||||||
|
{data.recentNotifications.map((n) => (
|
||||||
|
<li
|
||||||
|
key={n.$id}
|
||||||
|
className={`flex items-start gap-3 py-2.5 ${n.read ? "opacity-70" : ""}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`mt-1.5 size-2 shrink-0 rounded-full ${n.read ? "bg-muted" : "bg-primary"}`}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-sm leading-tight">{n.message}</p>
|
||||||
|
<p className="text-muted-foreground mt-0.5 text-xs">
|
||||||
|
{datetimeFormatter.format(new Date(n.$createdAt))}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StatCard({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
hint,
|
||||||
|
href,
|
||||||
|
tone = "neutral",
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
hint?: string;
|
||||||
|
href: string;
|
||||||
|
tone?: "positive" | "negative" | "neutral";
|
||||||
|
}) {
|
||||||
|
const color =
|
||||||
|
tone === "positive"
|
||||||
|
? "text-emerald-600 dark:text-emerald-400"
|
||||||
|
: tone === "negative"
|
||||||
|
? "text-rose-600 dark:text-rose-400"
|
||||||
|
: "text-foreground";
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className="hover:bg-muted/40 group block rounded-lg border bg-card p-4 transition-colors"
|
||||||
|
>
|
||||||
|
<p className="text-muted-foreground text-xs font-medium uppercase tracking-wide">
|
||||||
|
{label}
|
||||||
|
</p>
|
||||||
|
<p className={`mt-1 text-2xl font-semibold tabular-nums ${color}`}>{value}</p>
|
||||||
|
{hint && <p className="text-muted-foreground mt-1 text-xs">{hint}</p>}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import "server-only";
|
|||||||
import { Query } from "node-appwrite";
|
import { Query } from "node-appwrite";
|
||||||
|
|
||||||
import { createAdminClient, getCurrentUser } from "./server";
|
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";
|
import { getActiveTenantId, getUserTeams } from "./tenant";
|
||||||
|
|
||||||
export type ActiveContext = {
|
export type ActiveContext = {
|
||||||
user: { id: string; name: string; email: string };
|
user: { id: string; name: string; email: string };
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
|
kind: TenantKind | null;
|
||||||
settings: TenantSettings | null;
|
settings: TenantSettings | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ export async function getActiveContext(): Promise<ActiveContext | null> {
|
|||||||
return {
|
return {
|
||||||
user: { id: user.$id, name: user.name, email: user.email },
|
user: { id: user.$id, name: user.name, email: user.email },
|
||||||
tenantId,
|
tenantId,
|
||||||
|
kind: settings?.kind ?? null,
|
||||||
settings,
|
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