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

224 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { useParams } from "next/navigation";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { Button, Card, CardContent, ConfirmDialog, Dialog, Input, PageHeader, Select, Textarea } from "@/components/ds";
import { useTenantId } from "@/hooks/useTenantId";
import { workspaceApi } from "@/modules/workspace/services/workspace-api";
import { ACTIVITY_ACTION, TEAM_ROLE, displayPerson, formatWhen, labelOf } from "@/modules/workspace/labels";
import { IdentityUserSelect } from "@/modules/workspace/components/IdentityUserSelect";
import { CommentThread } from "@/modules/workspace/components/CommentThread";
import { StatusChip } from "@/modules/workspace/components/StatusChip";
import { WorkspacePageError, WorkspacePageLoader } from "@/modules/workspace/pages/shared";
import { useTenantMembers } from "@/modules/workspace/hooks/useTenantMembers";
export const WorkspaceTeamDetail = function WorkspaceTeamDetailPage() {
const params = useParams<{ id: string }>();
const { tenantId } = useTenantId();
const queryClient = useQueryClient();
const directory = useTenantMembers();
const names = Object.fromEntries((directory.data ?? []).map((person) => [person.id, person.label]));
const [userId, setUserId] = useState("");
const [role, setRole] = useState("member");
const [editOpen, setEditOpen] = useState(false);
const [archiveOpen, setArchiveOpen] = useState(false);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const team = useQuery({
queryKey: ["workspace", tenantId, "team", params.id],
queryFn: () => workspaceApi.teams.get(tenantId!, params.id),
enabled: !!tenantId && !!params.id,
});
const hubId = team.data?.work_hub_id ? String(team.data.work_hub_id) : "";
const projects = useQuery({
queryKey: ["workspace", tenantId, "projects", hubId, params.id],
queryFn: () => workspaceApi.projects.list(tenantId!, { work_hub_id: hubId, team_id: params.id }),
enabled: !!tenantId && !!hubId,
});
const tasks = useQuery({
queryKey: ["workspace", tenantId, "tasks", hubId],
queryFn: () => workspaceApi.tasks.list(tenantId!, { work_hub_id: hubId }),
enabled: !!tenantId && !!hubId,
});
const content = useQuery({
queryKey: ["workspace", tenantId, "content", hubId],
queryFn: () => workspaceApi.content.list(tenantId!, { work_hub_id: hubId }),
enabled: !!tenantId && !!hubId,
});
const activity = useQuery({
queryKey: ["workspace", tenantId, "activity", hubId],
queryFn: () => workspaceApi.activity.list(tenantId!, hubId),
enabled: !!tenantId && !!hubId,
});
const invalidate = () => void queryClient.invalidateQueries({ queryKey: ["workspace", tenantId, "team", params.id] });
const addMember = useMutation({
mutationFn: () => workspaceApi.members.add(tenantId!, { team_id: params.id, user_id: userId, role }),
onSuccess: () => {
toast.success("عضو اضافه شد");
setUserId("");
invalidate();
},
onError: (err) => toast.error(err instanceof Error ? err.message : "افزودن عضو ناموفق بود"),
});
const updateMember = useMutation({
mutationFn: ({ id, body }: { id: string; body: Record<string, unknown> }) =>
workspaceApi.members.update(tenantId!, id, body),
onSuccess: () => {
toast.success("عضو به‌روز شد");
invalidate();
},
onError: (err) => toast.error(err instanceof Error ? err.message : "به‌روزرسانی عضو ناموفق بود"),
});
const saveTeam = useMutation({
mutationFn: () => workspaceApi.teams.update(tenantId!, params.id, { name, description }),
onSuccess: () => {
toast.success("تیم به‌روز شد");
setEditOpen(false);
invalidate();
},
});
const archive = useMutation({
mutationFn: () => workspaceApi.teams.update(tenantId!, params.id, { status: "archived" }),
onSuccess: () => {
toast.success("تیم بایگانی شد");
setArchiveOpen(false);
invalidate();
},
});
if (!tenantId || team.isLoading) return <WorkspacePageLoader />;
if (team.error) return <WorkspacePageError error={team.error} onRetry={() => team.refetch()} />;
const members = (team.data?.members as Record<string, unknown>[]) || [];
const teamTasks = (tasks.data ?? []).filter((task) => String(task.team_id || "") === params.id);
const teamContent = (content.data ?? []).filter((item) => String(item.team_id || "") === params.id);
return (
<div className="space-y-6">
<PageHeader
title={String(team.data?.name || "تیم")}
description={String(team.data?.description || "اعضای تیم، پروژه‌ها و فعالیت‌ها")}
actions={
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => {
setName(String(team.data?.name || ""));
setDescription(String(team.data?.description || ""));
setEditOpen(true);
}}
>
ویرایش
</Button>
<Button variant="outline" onClick={() => setArchiveOpen(true)}>
بایگانی تیم
</Button>
</div>
}
/>
<Card>
<CardContent className="space-y-4 p-5">
<h2 className="font-semibold text-secondary">اعضای تیم</h2>
<div className="grid gap-2 md:grid-cols-[1fr_160px_auto]">
<IdentityUserSelect value={userId} onChange={setUserId} emptyLabel="انتخاب عضو سازمان" />
<Select value={role} onChange={(event) => setRole(event.target.value)}>
{Object.entries(TEAM_ROLE).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
))}
</Select>
<Button disabled={!userId.trim() || addMember.isPending} onClick={() => addMember.mutate()}>
افزودن عضو
</Button>
</div>
<div className="space-y-2">
{members.map((member) => (
<div key={String(member.id)} className="flex flex-wrap items-center justify-between gap-2 rounded-xl border border-[var(--border)] px-3 py-2">
<div>
<p className="text-sm font-medium">{names[String(member.user_id || "")] || displayPerson(String(member.user_id || ""))}</p>
<p className="text-xs text-[var(--muted)]">{labelOf(TEAM_ROLE, String(member.role || ""))}</p>
</div>
<div className="flex gap-2">
<Select
value={String(member.role || "member")}
onChange={(event) => updateMember.mutate({ id: String(member.id), body: { role: event.target.value } })}
>
{Object.entries(TEAM_ROLE).map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
))}
</Select>
<Button variant="outline" size="sm" onClick={() => updateMember.mutate({ id: String(member.id), body: { status: "removed" } })}>
حذف
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
<div className="grid gap-4 lg:grid-cols-2">
<Card>
<CardContent className="space-y-2 p-5">
<h2 className="font-semibold text-secondary">پروژه‌های تیم</h2>
{(projects.data ?? []).length === 0 ? (
<p className="text-sm text-[var(--muted)]">پروژه‌ای به این تیم وصل نیست.</p>
) : (
(projects.data ?? []).map((project) => (
<Link key={String(project.id)} href={`/workspace/projects/${project.id}`} className="flex justify-between text-sm">
<span>{String(project.name)}</span>
<StatusChip value={String(project.status || "")} kind="project" />
</Link>
))
)}
</CardContent>
</Card>
<Card>
<CardContent className="space-y-2 p-5">
<h2 className="font-semibold text-secondary">وظایف و محتوا</h2>
<p className="text-sm text-[var(--muted)]">{teamTasks.length} وظیفه · {teamContent.length} محتوا</p>
</CardContent>
</Card>
</div>
<Card>
<CardContent className="space-y-2 p-5">
<h2 className="font-semibold text-secondary">فعالیت تیم</h2>
{(activity.data ?? []).slice(0, 12).map((item) => (
<p key={String(item.id)} className="text-sm text-[var(--muted)]">
{labelOf(ACTIVITY_ACTION, String(item.action || ""), "رویداد")} · {names[String(item.actor_user_id || "")] || displayPerson(String(item.actor_user_id || ""))} · {formatWhen(String(item.created_at || ""))}
</p>
))}
</CardContent>
</Card>
{hubId ? (
<CommentThread tenantId={tenantId} workHubId={hubId} subjectType="team" subjectId={params.id} />
) : null}
<Dialog open={editOpen} onClose={() => setEditOpen(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="توضیح" />
<Button className="w-full" disabled={!name.trim() || saveTeam.isPending} onClick={() => saveTeam.mutate()}>
ذخیره
</Button>
</div>
</Dialog>
<ConfirmDialog
open={archiveOpen}
onClose={() => setArchiveOpen(false)}
onConfirm={() => archive.mutate()}
title="بایگانی تیم"
confirmLabel="بایگانی"
danger
loading={archive.isPending}
/>
</div>
);
};
export default WorkspaceTeamDetail;