110 lines
5.3 KiB
TypeScript
110 lines
5.3 KiB
TypeScript
"use client";
|
||
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { useParams } from "next/navigation";
|
||
import Link from "next/link";
|
||
import { Button, PageHeader, StatCard, Card, CardContent } from "@/components/ds";
|
||
import { useTenantId } from "@/hooks/useTenantId";
|
||
import { workspaceApi } from "@/modules/workspace/services/workspace-api";
|
||
import { CONTENT_STATUS, displayPerson, formatDay, labelOf } from "@/modules/workspace/labels";
|
||
import { StatusChip } from "@/modules/workspace/components/StatusChip";
|
||
import { WorkspaceEmptyState, WorkspacePageError, WorkspacePageLoader } from "@/modules/workspace/pages/shared";
|
||
|
||
export const WorkspaceHubDashboard = function WorkspaceHubDashboardPage() {
|
||
const params = useParams<{ id: string }>();
|
||
const { tenantId } = useTenantId();
|
||
const hubId = params.id;
|
||
const dash = useQuery({
|
||
queryKey: ["workspace", tenantId, "dashboard", hubId],
|
||
queryFn: () => workspaceApi.hubs.dashboard(tenantId!, hubId),
|
||
enabled: !!tenantId && !!hubId,
|
||
});
|
||
const hub = useQuery({
|
||
queryKey: ["workspace", tenantId, "hub", hubId],
|
||
queryFn: () => workspaceApi.hubs.get(tenantId!, hubId),
|
||
enabled: !!tenantId && !!hubId,
|
||
});
|
||
|
||
if (!tenantId || dash.isLoading || hub.isLoading) return <WorkspacePageLoader />;
|
||
if (dash.error) return <WorkspacePageError error={dash.error} onRetry={() => dash.refetch()} />;
|
||
const data = dash.data!;
|
||
const overview = data.overview;
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<PageHeader
|
||
title={String(hub.data?.name || "داشبورد مرکز کار")}
|
||
description="نمای واقعی از پروژهها، وظایف و وضعیت تولید محتوا."
|
||
actions={
|
||
<div className="flex flex-wrap gap-2">
|
||
<Link href="/workspace/teams"><Button variant="outline">تیمها</Button></Link>
|
||
<Link href="/workspace/projects"><Button variant="outline">پروژهها</Button></Link>
|
||
<Link href="/workspace/tasks"><Button variant="outline">وظایف</Button></Link>
|
||
<Link href="/workspace/content"><Button variant="outline">محتوا</Button></Link>
|
||
</div>
|
||
}
|
||
/>
|
||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||
<StatCard label="پروژههای فعال" value={overview.active_projects} />
|
||
<StatCard label="وظایف باز" value={overview.open_tasks} hint={`${overview.overdue_tasks} عقبافتاده`} />
|
||
<StatCard label="وضعیت تولید" value={overview.content_in_production} />
|
||
<StatCard label="در انتظار بررسی" value={overview.content_awaiting_review} />
|
||
<StatCard label="زمانبندی شده" value={overview.scheduled_content} />
|
||
<StatCard label="صف تأیید" value={overview.pending_approvals} />
|
||
<StatCard label="اعضای تیم" value={overview.team_members} />
|
||
<StatCard label="مهلتهای نزدیک" value={overview.upcoming_deadlines} />
|
||
</div>
|
||
<div className="grid gap-4 lg:grid-cols-2">
|
||
<Card>
|
||
<CardContent className="space-y-3 p-5">
|
||
<h2 className="font-semibold text-secondary">قیف محتوا</h2>
|
||
{Object.entries(data.content).map(([status, count]) => (
|
||
<div key={status} className="flex items-center justify-between text-sm">
|
||
<span>{labelOf(CONTENT_STATUS, status)}</span>
|
||
<span className="font-medium">{count}</span>
|
||
</div>
|
||
))}
|
||
</CardContent>
|
||
</Card>
|
||
<Card>
|
||
<CardContent className="space-y-3 p-5">
|
||
<h2 className="font-semibold text-secondary">بار کاری تیم</h2>
|
||
{data.workload.length === 0 ? (
|
||
<WorkspaceEmptyState title="بار کاری ثبت نشده" description="با تخصیص وظیفه یا محتوا، این بخش پر میشود." />
|
||
) : (
|
||
data.workload.map((row) => (
|
||
<div key={row.user_id} className="flex items-center justify-between text-sm">
|
||
<span>{displayPerson(row.user_id)}</span>
|
||
<span>
|
||
{row.tasks} وظیفه · {row.content} محتوا · {row.overdue} عقبافتاده
|
||
</span>
|
||
</div>
|
||
))
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
<Card>
|
||
<CardContent className="space-y-3 p-5">
|
||
<h2 className="font-semibold text-secondary">مهلتهای پیشرو</h2>
|
||
{data.upcoming.length === 0 ? (
|
||
<p className="text-sm text-[var(--muted)]">مهلت نزدیکی ثبت نشده است.</p>
|
||
) : (
|
||
data.upcoming.map((task) => (
|
||
<div key={String(task.id)} className="flex items-center justify-between gap-3 text-sm">
|
||
<span>{String(task.title || "وظیفه")}</span>
|
||
<div className="flex items-center gap-2">
|
||
<StatusChip value={String(task.status || "")} kind="task" />
|
||
<span className="text-[var(--muted)]">{formatDay(String(task.due_date || ""))}</span>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default WorkspaceHubDashboard;
|