144 lines
6.6 KiB
TypeScript
144 lines
6.6 KiB
TypeScript
"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;
|