diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx
index 315bfb0..22c316d 100644
--- a/src/app/(dashboard)/dashboard/page.tsx
+++ b/src/app/(dashboard)/dashboard/page.tsx
@@ -1,56 +1,279 @@
+import Link from "next/link";
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 {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
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() {
const ctx = await getActiveContext();
if (!ctx) redirect("/onboarding");
+ const data = await getDashboardData(ctx.tenantId, ctx.kind);
const firstName = ctx.user.name?.split(" ")[0] ?? "";
const companyName = ctx.settings?.companyName ?? "Çalışma alanı";
+ const isLab = ctx.kind === "lab";
+ const isClinic = ctx.kind === "clinic";
return (
-
{companyName}
+
+ {isLab ? : }
+ {companyName}
+
{firstName ? `Hoş geldiniz, ${firstName}` : "Anasayfa"}
- Açık işleri, bildirimleri ve istatistikleri buradan takip edin.
+ Açık işleri, finansal akışı ve bildirimleri buradan takip edin.
-
-
+
+ 0
+ ? `${data.pendingActionCount} kalem sizden eylem bekliyor`
+ : "Hepsi yolunda"
+ }
+ href={isLab ? "/jobs/inbound" : "/jobs/outbound"}
+ />
+
+ 0 ? "Yeni etkinlikler var" : "Hepsi okundu"}
+ href="/notifications"
+ />
+ 0
+ ? `Onaylı ${isLab ? "klinik" : "laboratuvar"}`
+ : "Henüz bağlantınız yok"
+ }
+ href="/connections"
+ />
+
+
+ {isClinic && data.approvedConnectionsCount === 0 && (
+
- Açık işler
- Gelen ve giden iş özetleri burada listelenecek.
+ Bir laboratuvarla bağlantı kurun
+
+ İş gönderebilmeniz için en az bir onaylı laboratuvar bağlantınız olmalı.
+
-
- Modül yapım aşamasında.
+
+
+
+
+ Bağlantı Kur
+
+
+ )}
+
+
-
- İşlem bekleyen
- Onay/işlem bekleyen kalemler.
+
+
+ {isLab ? "Son Gelen İşler" : "Son Giden İşler"}
+ En son 8 kayıt.
+
+
+ {isClinic && (
+
+
+
+ Yeni İş
+
+
+ )}
+
+
+ Tümü
+
+
+
+
-
- Modül yapım aşamasında.
+
+ {data.recentJobs.length === 0 ? (
+
+ Henüz iş kaydı yok.
+
+ ) : (
+
+
+
+ {isLab ? "Klinik" : "Laboratuvar"}
+ Hasta
+ Tür
+ Durum
+ Tarih
+
+
+
+ {data.recentJobs.map((j) => (
+
+
+
+ {j.counterpartName ?? "—"}
+
+
+ {j.patientCode}
+
+ {PROSTHETIC_TYPE_LABELS[j.prostheticType]}
+
+
+
+ {JOB_STATUS_LABELS[j.status]}
+
+
+
+ {dateFormatter.format(new Date(j.$createdAt))}
+
+
+ ))}
+
+
+ )}
+
-
- Bildirimler
- Bağlantılarınızdan gelen son bildirimler.
+
+
+ Son Bildirimler
+ Bağlantı ve iş etkinlikleri.
+
+
+
+
+
+
-
- Modül yapım aşamasında.
+
+ {data.recentNotifications.length === 0 ? (
+
+ Bildirim yok.
+
+ ) : (
+
+ {data.recentNotifications.map((n) => (
+
+
+
+
{n.message}
+
+ {datetimeFormatter.format(new Date(n.$createdAt))}
+
+
+
+ ))}
+
+ )}
);
}
+
+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 (
+
+
+ {label}
+
+
{value}
+ {hint &&
{hint}
}
+
+ );
+}
diff --git a/src/lib/appwrite/active-context.ts b/src/lib/appwrite/active-context.ts
index 58a6e20..3c59faa 100644
--- a/src/lib/appwrite/active-context.ts
+++ b/src/lib/appwrite/active-context.ts
@@ -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
{
return {
user: { id: user.$id, name: user.name, email: user.email },
tenantId,
+ kind: settings?.kind ?? null,
settings,
};
}
diff --git a/src/lib/appwrite/dashboard-queries.ts b/src/lib/appwrite/dashboard-queries.ts
new file mode 100644
index 0000000..2f61a30
--- /dev/null
+++ b/src/lib/appwrite/dashboard-queries.ts
@@ -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 {
+ 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();
+ 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[],
+ };
+}