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

144 lines
6.6 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 Link from "next/link";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { Button, Card, CardContent, ConfirmDialog, Dialog, Input, PageHeader, Textarea } from "@/components/ds";
import { useCurrentHub, storeHubId } from "@/modules/workspace/hooks/useCurrentHub";
import { workspaceApi } from "@/modules/workspace/services/workspace-api";
import { HUB_STATUS, labelOf } from "@/modules/workspace/labels";
import { WorkspaceEmptyState, WorkspacePageError, WorkspacePageLoader } from "@/modules/workspace/pages/shared";
import type { WorkspaceRecord } from "@/modules/workspace/types";
export const WorkspaceHubs = function WorkspaceHubsPage() {
const queryClient = useQueryClient();
const { tenantId, hubs, loading, error, refetch } = useCurrentHub();
const [open, setOpen] = useState(false);
const [editing, setEditing] = useState<WorkspaceRecord | null>(null);
const [archiveId, setArchiveId] = useState<string | null>(null);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const invalidate = () => void queryClient.invalidateQueries({ queryKey: ["workspace", tenantId, "hubs"] });
const create = useMutation({
mutationFn: () => workspaceApi.hubs.create(tenantId!, { name, description }),
onSuccess: (hub) => {
if (hub.id) storeHubId(String(hub.id));
toast.success("مرکز کار ایجاد شد");
setOpen(false);
setName("");
setDescription("");
invalidate();
},
onError: (err) => toast.error(err instanceof Error ? err.message : "ایجاد مرکز کار ناموفق بود"),
});
const update = useMutation({
mutationFn: () => workspaceApi.hubs.update(tenantId!, String(editing?.id), { name, description }),
onSuccess: () => {
toast.success("مرکز کار به‌روز شد");
setEditing(null);
invalidate();
},
onError: (err) => toast.error(err instanceof Error ? err.message : "ویرایش ناموفق بود"),
});
const archive = useMutation({
mutationFn: () => workspaceApi.hubs.update(tenantId!, archiveId!, { status: "archived" }),
onSuccess: () => {
toast.success("مرکز کار بایگانی شد");
setArchiveId(null);
invalidate();
},
onError: (err) => toast.error(err instanceof Error ? err.message : "بایگانی ناموفق بود"),
});
if (!tenantId || loading) return <WorkspacePageLoader />;
if (error) return <WorkspacePageError error={error} onRetry={() => refetch()} />;
return (
<div className="space-y-6">
<PageHeader
title="مراکز کار"
description="هر سازمان می‌تواند چند مرکز کار مستقل داشته باشد."
actions={<Button onClick={() => setOpen(true)}>مرکز کار جدید</Button>}
/>
{hubs.length === 0 ? (
<WorkspaceEmptyState
title="هنوز مرکز کاری ندارید"
description="برای شروع پروژه‌ها و محتوا، یک مرکز کار بسازید."
action={<Button onClick={() => setOpen(true)}>ایجاد مرکز کار</Button>}
/>
) : (
<div className="grid gap-4 md:grid-cols-2">
{hubs.map((hub) => (
<Card key={String(hub.id)}>
<CardContent className="space-y-3 p-5">
<div className="flex items-start justify-between gap-3">
<div>
<h2 className="text-lg font-semibold text-secondary">{String(hub.name || "مرکز کار")}</h2>
<p className="mt-1 text-sm text-[var(--muted)]">{String(hub.description || "بدون توضیح")}</p>
</div>
<span className="text-xs text-[var(--muted)]">{labelOf(HUB_STATUS, String(hub.status || ""))}</span>
</div>
<div className="flex flex-wrap gap-2">
<Link href={`/workspace/hubs/${hub.id}`} className="flex-1">
<Button className="w-full" onClick={() => hub.id && storeHubId(String(hub.id))}>
باز کردن
</Button>
</Link>
<Button
variant="outline"
onClick={() => {
setEditing(hub);
setName(String(hub.name || ""));
setDescription(String(hub.description || ""));
}}
>
ویرایش
</Button>
{hub.status !== "archived" ? (
<Button variant="outline" onClick={() => setArchiveId(String(hub.id))}>
بایگانی
</Button>
) : null}
</div>
</CardContent>
</Card>
))}
</div>
)}
<Dialog open={open} onClose={() => setOpen(false)} title="مرکز کار جدید">
<div className="space-y-3">
<Input value={name} onChange={(event) => setName(event.target.value)} placeholder="نام مرکز کار" />
<Textarea value={description} onChange={(event) => setDescription(event.target.value)} placeholder="توضیح" rows={3} />
<Button className="w-full" disabled={!name.trim() || create.isPending} onClick={() => create.mutate()}>
ایجاد
</Button>
</div>
</Dialog>
<Dialog open={!!editing} onClose={() => setEditing(null)} title="ویرایش مرکز کار">
<div className="space-y-3">
<Input value={name} onChange={(event) => setName(event.target.value)} placeholder="نام مرکز کار" />
<Textarea value={description} onChange={(event) => setDescription(event.target.value)} placeholder="توضیح" rows={3} />
<Button className="w-full" disabled={!name.trim() || update.isPending} onClick={() => update.mutate()}>
ذخیره
</Button>
</div>
</Dialog>
<ConfirmDialog
open={!!archiveId}
onClose={() => setArchiveId(null)}
onConfirm={() => archive.mutate()}
title="بایگانی مرکز کار"
description="پس از بایگانی، این مرکز از فهرست فعال خارج می‌شود."
confirmLabel="بایگانی"
danger
loading={archive.isPending}
/>
</div>
);
};
export default WorkspaceHubs;