TorbatYar/frontend/modules/workspace/features/contentBoard.tsx
Mortezakoohjani a483b47369 feat(workspace): complete production workspace and content operations
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 11:54:17 +03:30

108 lines
5.2 KiB
TypeScript

"use client";
import { useMemo } from "react";
import Link from "next/link";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { Card, CardContent, PageHeader } from "@/components/ds";
import { useCurrentHub } from "@/modules/workspace/hooks/useCurrentHub";
import { workspaceApi } from "@/modules/workspace/services/workspace-api";
import { CHANNEL, CONTENT_BOARD_COLUMNS, CONTENT_TYPE, displayPerson, formatDay, labelOf } from "@/modules/workspace/labels";
import { StatusChip } from "@/modules/workspace/components/StatusChip";
import { WorkspaceEmptyState, WorkspacePageError, WorkspacePageLoader } from "@/modules/workspace/pages/shared";
import type { WorkspaceRecord } from "@/modules/workspace/types";
function columnStatus(columnId: string) {
return CONTENT_BOARD_COLUMNS.find((column) => column.id === columnId)?.statuses[0] || columnId;
}
export const WorkspaceContentBoard = function WorkspaceContentBoardPage() {
const queryClient = useQueryClient();
const { tenantId, hubId, loading } = useCurrentHub();
const items = useQuery({
queryKey: ["workspace", tenantId, "content", hubId],
queryFn: () => workspaceApi.content.list(tenantId!, { work_hub_id: hubId || undefined }),
enabled: !!tenantId && !!hubId,
});
const move = useMutation({
mutationFn: ({ id, status }: { id: string; status: string }) =>
workspaceApi.content.update(tenantId!, id, { status }),
onSuccess: () => {
toast.success("وضعیت محتوا ذخیره شد");
void queryClient.invalidateQueries({ queryKey: ["workspace", tenantId, "content"] });
},
onError: (err) => toast.error(err instanceof Error ? err.message : "این انتقال مجاز نیست"),
});
const grouped = useMemo(() => {
const map: Record<string, WorkspaceRecord[]> = {};
for (const column of CONTENT_BOARD_COLUMNS) map[column.id] = [];
for (const item of items.data ?? []) {
const column = CONTENT_BOARD_COLUMNS.find((entry) =>
(entry.statuses as readonly string[]).includes(String(item.status || "draft"))
);
const key = column?.id || "draft";
map[key] = [...(map[key] || []), item];
}
return map;
}, [items.data]);
if (!tenantId || loading || items.isLoading) return <WorkspacePageLoader />;
if (!hubId) return <WorkspaceEmptyState title="مرکز کار لازم است" description="ابتدا مرکز کار را انتخاب کنید." />;
if (items.error) return <WorkspacePageError error={items.error} onRetry={() => items.refetch()} />;
return (
<div className="space-y-6">
<PageHeader title="برد محتوا" description="گلوگاه تولید را ببینید. کشیدن کارت وضعیت را در سرور عوض می‌کند." />
<div className="grid gap-3 overflow-x-auto pb-4 lg:grid-cols-7">
{CONTENT_BOARD_COLUMNS.map((column) => (
<div
key={column.id}
className="min-w-[200px] rounded-2xl bg-[var(--surface-muted)] p-3"
onDragOver={(event) => event.preventDefault()}
onDrop={(event) => {
const id = event.dataTransfer.getData("text/content-id");
if (id) move.mutate({ id, status: columnStatus(column.id) });
}}
>
<h2 className="mb-3 text-sm font-semibold text-secondary">
{column.label} ({(grouped[column.id] ?? []).length})
</h2>
<div className="space-y-2">
{(grouped[column.id] ?? []).map((item) => (
<Card
key={String(item.id)}
draggable
onDragStart={(event) => event.dataTransfer.setData("text/content-id", String(item.id))}
className="cursor-grab"
>
<CardContent className="space-y-2 p-3">
<Link href={`/workspace/content/${item.id}`} className="text-sm font-medium text-secondary">
{String(item.title || "محتوا")}
</Link>
<p className="text-xs text-[var(--muted)]">
{labelOf(CONTENT_TYPE, String(item.content_type || ""))} · {labelOf(CHANNEL, String(item.channel || ""))}
</p>
<div className="flex flex-wrap items-center gap-1">
<StatusChip value={String(item.status || "")} />
{(item.approvals as unknown[] | undefined)?.length || item.status === "internal_review" || item.status === "client_review" ? (
<StatusChip value="pending" kind="approval" />
) : null}
</div>
<p className="text-xs text-[var(--muted)]">
{displayPerson(String(item.owner_user_id || ""))}
{item.scheduled_at ? ` · ${formatDay(String(item.scheduled_at))}` : ""}
</p>
</CardContent>
</Card>
))}
</div>
</div>
))}
</div>
</div>
);
};
export default WorkspaceContentBoard;