TorbatYar/frontend/modules/workspace/features/approvals.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

112 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import Link from "next/link";
import { toast } from "sonner";
import { Button, Card, CardContent, PageHeader, Textarea } from "@/components/ds";
import { useCurrentHub } from "@/modules/workspace/hooks/useCurrentHub";
import { workspaceApi } from "@/modules/workspace/services/workspace-api";
import { APPROVAL_TYPE, displayPerson, formatWhen, labelOf } from "@/modules/workspace/labels";
import { StatusChip } from "@/modules/workspace/components/StatusChip";
import { WorkspaceEmptyState, WorkspacePageError, WorkspacePageLoader } from "@/modules/workspace/pages/shared";
export const WorkspaceApprovals = function WorkspaceApprovalsPage() {
const queryClient = useQueryClient();
const { tenantId, hubId, loading } = useCurrentHub();
const [comments, setComments] = useState<Record<string, string>>({});
const approvals = useQuery({
queryKey: ["workspace", tenantId, "approvals", hubId],
queryFn: () => workspaceApi.approvals.list(tenantId!, { work_hub_id: hubId || undefined }),
enabled: !!tenantId && !!hubId,
});
const respond = useMutation({
mutationFn: ({ id, status, comment }: { id: string; status: string; comment?: string }) =>
workspaceApi.approvals.respond(tenantId!, id, { status, comment }),
onSuccess: () => {
toast.success("پاسخ تأیید ذخیره شد");
void queryClient.invalidateQueries({ queryKey: ["workspace", tenantId, "approvals"] });
},
onError: (err) => toast.error(err instanceof Error ? err.message : "ثبت پاسخ ناموفق بود"),
});
if (!tenantId || loading || approvals.isLoading) return <WorkspacePageLoader />;
if (!hubId) return <WorkspaceEmptyState title="مرکز کار لازم است" description="ابتدا مرکز کار را انتخاب کنید." />;
if (approvals.error) return <WorkspacePageError error={approvals.error} onRetry={() => approvals.refetch()} />;
return (
<div className="space-y-6">
<PageHeader title="تأییدیه‌ها" description="تأیید داخلی، مشتری و نهایی با تاریخچه واقعی." />
{(approvals.data ?? []).length === 0 ? (
<WorkspaceEmptyState title="تأییدیه‌ای نیست" description="از صفحه محتوا درخواست تأیید ثبت کنید." />
) : (
<div className="space-y-3">
{(approvals.data ?? []).map((row) => (
<Card key={String(row.id)}>
<CardContent className="flex flex-wrap items-center justify-between gap-3 p-5">
<div>
<p className="font-medium text-secondary">{labelOf(APPROVAL_TYPE, String(row.approval_type || ""))}</p>
<p className="text-sm text-[var(--muted)]">
تأییدکننده: {displayPerson(String(row.approver_user_id || ""))} · {formatWhen(String(row.requested_at || row.created_at || ""))}
</p>
{row.comment ? <p className="mt-1 text-sm">{String(row.comment)}</p> : null}
</div>
<div className="flex items-center gap-2">
<StatusChip value={String(row.status || "")} kind="approval" />
<Link href={`/workspace/content/${row.content_id}`} className="text-sm text-primary">
مشاهده محتوا
</Link>
{row.status === "pending" ? (
<div className="flex w-full flex-col gap-2 sm:w-auto">
<Textarea
value={comments[String(row.id)] || ""}
onChange={(event) => setComments((prev) => ({ ...prev, [String(row.id)]: event.target.value }))}
placeholder="یادداشت بررسی"
rows={2}
/>
<div className="flex flex-wrap gap-2">
<Button
size="sm"
onClick={() =>
respond.mutate({ id: String(row.id), status: "approved", comment: comments[String(row.id)] })
}
>
تأیید
</Button>
<Button
size="sm"
variant="outline"
onClick={() =>
respond.mutate({
id: String(row.id),
status: "changes_requested",
comment: comments[String(row.id)] || "نیاز به اصلاح",
})
}
>
اصلاح
</Button>
<Button
size="sm"
variant="danger"
onClick={() =>
respond.mutate({ id: String(row.id), status: "rejected", comment: comments[String(row.id)] })
}
>
رد
</Button>
</div>
</div>
) : null}
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
};
export default WorkspaceApprovals;