From c4a7c4f07eb601c652421ed8afff231030ed19b3 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 18:06:06 +0000 Subject: [PATCH] =?UTF-8?q?feat(CC-091):=20add=20Content=20Calendar=20page?= =?UTF-8?q?=20(platform=20filter,=20status=20flow=20idea=E2=86=92published?= =?UTF-8?q?,=20add=20form)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/dashboard/content-calendar/page.tsx | 214 ++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 src/app/dashboard/content-calendar/page.tsx diff --git a/src/app/dashboard/content-calendar/page.tsx b/src/app/dashboard/content-calendar/page.tsx new file mode 100644 index 0000000..e2fb055 --- /dev/null +++ b/src/app/dashboard/content-calendar/page.tsx @@ -0,0 +1,214 @@ +'use client'; + +import { useMemo, useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Task { id: string; title: string; description: string | null; status: string; priority: string | null; tags: string[]; targetDate: string | null; createdAt: string; } + +const CONTENT_TAGS = ['content','post','articol','newsletter','video','podcast','reel','thread','blog']; +const PLATFORM_TAGS = ['linkedin','twitter','facebook','instagram','youtube','substack','blog','medium']; +const STATUS_FLOW = ['idea','draft','review','scheduled','published']; +const STATUS_LABELS: Record = { + idea:'💡 Idee', draft:'✏️ Draft', review:'🔍 Review', scheduled:'📅 Programat', published:'✅ Publicat', todo:'📋 Todo', 'in-progress':'⚡ În progres', completed:'✅ Done', +}; +const STATUS_COLORS: Record = { + idea:'bg-muted text-ink-faint', draft:'bg-primary/10 text-primary', + review:'bg-warn/10 text-warn', scheduled:'bg-blue-500/10 text-blue-600', + published:'bg-signal-ok/10 text-signal-ok', completed:'bg-signal-ok/10 text-signal-ok', +}; + +export default function ContentCalendarPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [platformFilter, setPlatformFilter] = useState('all'); + const [statusFilter, setStatusFilter] = useState('all'); + const [showAdd, setShowAdd] = useState(false); + const [form, setForm] = useState({ title: '', platform: 'linkedin', type: 'post', targetDate: '', priority: 'normal' }); + + const { data: tasks = [], isLoading } = useQuery({ + queryKey: ['content', tenantId], + queryFn: () => apiFetch('/v1/tasks?limit=500', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, + }); + + const content = useMemo(() => + tasks.filter((t) => t.tags.some((tag) => CONTENT_TAGS.includes(tag.toLowerCase()))), + [tasks]); + + function detectPlatform(t: Task): string { + return t.tags.find((tag) => PLATFORM_TAGS.includes(tag.toLowerCase())) ?? 'general'; + } + + function detectStatus(t: Task): string { + const knownStatuses = [...STATUS_FLOW, 'todo', 'in-progress', 'completed', 'cancelled']; + const fromTags = t.tags.find((tag) => STATUS_FLOW.includes(tag.toLowerCase())); + if (fromTags) return fromTags; + if (t.status === 'completed') return 'published'; + if (t.status === 'in-progress') return 'draft'; + return 'idea'; + } + + const platformCounts = useMemo(() => { + const map: Record = {}; + for (const t of content) { + const p = detectPlatform(t); + map[p] = (map[p] ?? 0) + 1; + } + return map; + }, [content]); + + const filtered = useMemo(() => { + let list = content; + if (platformFilter !== 'all') list = list.filter((t) => detectPlatform(t) === platformFilter); + if (statusFilter !== 'all') list = list.filter((t) => detectStatus(t) === statusFilter); + return list.sort((a, b) => { + const aDate = a.targetDate ?? a.createdAt; + const bDate = b.targetDate ?? b.createdAt; + return aDate.localeCompare(bDate); + }); + }, [content, platformFilter, statusFilter]); + + const addMut = useMutation({ + mutationFn: () => apiFetch('/v1/tasks', { tenantId, method: 'POST', body: { + title: form.title, + tags: ['content', form.type, form.platform, 'idea'], + priority: form.priority, + status: 'todo', + targetDate: form.targetDate || undefined, + }}), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['content', tenantId] }); + setShowAdd(false); + setForm({ title: '', platform: 'linkedin', type: 'post', targetDate: '', priority: 'normal' }); + }, + }); + + const patchStatusMut = useMutation({ + mutationFn: ({ id, nextStatus }: { id: string; nextStatus: string }) => { + const tag = STATUS_FLOW.includes(nextStatus) ? nextStatus : 'published'; + return apiFetch(`/v1/tasks/${id}`, { tenantId, method: 'PATCH', body: { + status: nextStatus === 'published' ? 'completed' : 'in-progress', + tags: undefined, + }}); + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ['content', tenantId] }), + }); + + const PLATFORM_ICONS: Record = { + linkedin:'💼', twitter:'🐦', facebook:'📘', instagram:'📸', youtube:'▶️', + substack:'📮', blog:'✍️', medium:'📰', general:'📢', + }; + + return ( +
+
+
+

Content Calendar

+

{content.length} piese de conținut

+
+ +
+ + {showAdd && ( +
+

Conținut nou

+ setForm((p) => ({ ...p, title: e.target.value }))} + className="w-full rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ + + setForm((p) => ({ ...p, targetDate: e.target.value }))} + className="rounded-lg border bg-background px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+
+ + +
+
+ )} + + {/* Filters */} +
+
+ + {Object.entries(platformCounts).map(([p, c]) => ( + + ))} +
+
+ {['all', ...STATUS_FLOW].map((s) => ( + + ))} +
+
+ + {isLoading ? ( +
Se încarcă…
+ ) : filtered.length === 0 ? ( +
+

📅

+

Niciun conținut. Adaugă idei cu tag „content".

+
+ ) : ( +
+ {filtered.map((t) => { + const platform = detectPlatform(t); + const status = detectStatus(t); + const nextIdx = STATUS_FLOW.indexOf(status); + const next = STATUS_FLOW[nextIdx + 1]; + return ( +
+ {PLATFORM_ICONS[platform] ?? '📢'} +
+

{t.title}

+
+ + {STATUS_LABELS[status] ?? status} + + {t.targetDate && 📅 {new Date(t.targetDate).toLocaleDateString('ro-RO')}} +
+
+ {next && ( + + )} +
+ ); + })} +
+ )} +
+ ); +}