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

182 lines
8.3 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, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { Button, Card, CardContent, Dialog, Input, PageHeader, Select, Textarea } from "@/components/ds";
import { useCurrentHub } from "@/modules/workspace/hooks/useCurrentHub";
import { workspaceApi } from "@/modules/workspace/services/workspace-api";
import { BRIEF_FIELDS, CHANNEL, CONTENT_TYPE, labelOf } from "@/modules/workspace/labels";
import { StatusChip } from "@/modules/workspace/components/StatusChip";
import { IdentityUserSelect } from "@/modules/workspace/components/IdentityUserSelect";
import { WorkspaceEmptyState, WorkspacePageError, WorkspacePageLoader } from "@/modules/workspace/pages/shared";
export const WorkspaceContentStudio = function WorkspaceContentStudioPage() {
const queryClient = useQueryClient();
const { tenantId, hubId, loading } = useCurrentHub();
const [open, setOpen] = useState(false);
const [title, setTitle] = useState("");
const [contentType, setContentType] = useState("instagram_post");
const [projectId, setProjectId] = useState("");
const [brief, setBrief] = useState<Record<string, string>>({});
const [ownerId, setOwnerId] = useState("");
const [reviewerId, setReviewerId] = useState("");
const [teamId, setTeamId] = useState("");
const [search, setSearch] = useState("");
const types = useQuery({
queryKey: ["workspace", tenantId, "content-types"],
queryFn: () => workspaceApi.contentTypes.list(tenantId!),
enabled: !!tenantId,
});
const projects = useQuery({
queryKey: ["workspace", tenantId, "projects", hubId],
queryFn: () => workspaceApi.projects.list(tenantId!, { work_hub_id: hubId || undefined }),
enabled: !!tenantId && !!hubId,
});
const teams = useQuery({
queryKey: ["workspace", tenantId, "teams", hubId],
queryFn: () => workspaceApi.teams.list(tenantId!, hubId || undefined),
enabled: !!tenantId && !!hubId,
});
const items = useQuery({
queryKey: ["workspace", tenantId, "content", hubId],
queryFn: () => workspaceApi.content.list(tenantId!, { work_hub_id: hubId || undefined }),
enabled: !!tenantId && !!hubId,
});
const create = useMutation({
mutationFn: () =>
workspaceApi.content.create(tenantId!, {
work_hub_id: hubId,
project_id: projectId || undefined,
title,
content_type: contentType,
brief,
owner_user_id: ownerId || undefined,
reviewer_user_id: reviewerId || undefined,
team_id: teamId || undefined,
}),
onSuccess: () => {
toast.success("محتوا ایجاد شد");
setOpen(false);
setTitle("");
setBrief({});
void queryClient.invalidateQueries({ queryKey: ["workspace", tenantId, "content"] });
},
onError: (err) => toast.error(err instanceof Error ? err.message : "ایجاد محتوا ناموفق بود"),
});
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="بریف ساختاریافته، تولید، بررسی و زمان‌بندی در یک استودیو عملیاتی."
actions={
<div className="flex gap-2">
<Link href="/workspace/content/board">
<Button variant="outline">برد محتوا</Button>
</Link>
<Link href="/workspace/content/calendar">
<Button variant="outline">تقویم محتوا</Button>
</Link>
<Button onClick={() => setOpen(true)}>محتوای جدید</Button>
</div>
}
/>
<Input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="جستجوی عنوان محتوا" />
{(items.data ?? []).filter((item) => String(item.title || "").includes(search)).length === 0 ? (
<WorkspaceEmptyState
title="محتوایی نیست"
description="اولین آیتم محتوا را با بریف ساختاریافته بسازید."
action={<Button onClick={() => setOpen(true)}>ایجاد محتوا</Button>}
/>
) : (
<div className="grid gap-4 md:grid-cols-2">
{(items.data ?? [])
.filter((item) => String(item.title || "").includes(search))
.map((item) => (
<Card key={String(item.id)}>
<CardContent className="space-y-3 p-5">
<div className="flex items-start justify-between gap-2">
<h2 className="font-semibold text-secondary">{String(item.title || "محتوا")}</h2>
<StatusChip value={String(item.status || "")} />
</div>
<p className="text-sm text-[var(--muted)]">
{labelOf(CONTENT_TYPE, String(item.content_type || ""), String(item.content_type || "سایر"))} ·{" "}
{labelOf(CHANNEL, String(item.channel || ""), "بدون کانال")}
</p>
<Link href={`/workspace/content/${item.id}`}>
<Button variant="outline" className="w-full">
باز کردن
</Button>
</Link>
</CardContent>
</Card>
))}
</div>
)}
<Dialog open={open} onClose={() => setOpen(false)} title="محتوای جدید" size="lg">
<div className="space-y-3">
<Input value={title} onChange={(event) => setTitle(event.target.value)} placeholder="عنوان" />
<Select value={contentType} onChange={(event) => setContentType(event.target.value)}>
{(types.data ?? []).map((type) => (
<option key={String(type.code)} value={String(type.code)}>
{String(type.label || labelOf(CONTENT_TYPE, String(type.code || "")))}
</option>
))}
</Select>
<Select value={projectId} onChange={(event) => setProjectId(event.target.value)}>
<option value="">بدون پروژه</option>
{(projects.data ?? []).map((project) => (
<option key={String(project.id)} value={String(project.id)}>
{String(project.name)}
</option>
))}
</Select>
<Select value={teamId} onChange={(event) => setTeamId(event.target.value)}>
<option value="">بدون تیم</option>
{(teams.data ?? []).map((team) => (
<option key={String(team.id)} value={String(team.id)}>
{String(team.name)}
</option>
))}
</Select>
<div className="grid gap-2 md:grid-cols-2">
<IdentityUserSelect value={ownerId} onChange={setOwnerId} emptyLabel="سازنده" />
<IdentityUserSelect value={reviewerId} onChange={setReviewerId} emptyLabel="بازبین" />
</div>
<div className="grid gap-3 md:grid-cols-2">
{BRIEF_FIELDS.map((field) =>
field.multiline ? (
<Textarea
key={field.key}
value={brief[field.key] || ""}
onChange={(event) => setBrief((prev) => ({ ...prev, [field.key]: event.target.value }))}
placeholder={field.label}
rows={2}
/>
) : (
<Input
key={field.key}
value={brief[field.key] || ""}
onChange={(event) => setBrief((prev) => ({ ...prev, [field.key]: event.target.value }))}
placeholder={field.label}
/>
)
)}
</div>
<Button className="w-full" disabled={!title.trim() || create.isPending} onClick={() => create.mutate()}>
ایجاد
</Button>
</div>
</Dialog>
</div>
);
};
export default WorkspaceContentStudio;