diff --git a/src/app/dashboard/action-plans/page.tsx b/src/app/dashboard/action-plans/page.tsx new file mode 100644 index 0000000..3b10d47 --- /dev/null +++ b/src/app/dashboard/action-plans/page.tsx @@ -0,0 +1,175 @@ +'use client'; + +import { useState } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface ActionPlan { + id: string; title: string; description: string | null; + status: string; priority: string; owner: string | null; + dueDate: string | null; completedAt: string | null; + decisionId: string | null; goalId: string | null; projectId: string | null; + createdAt: string; +} + +const STATUS_META: Record = { + draft: { label: 'Draft', cls: 'text-ink-faint bg-muted', order: 0 }, + active: { label: 'Activ', cls: 'text-bronze-deep bg-primary/10', order: 1 }, + completed: { label: 'Finalizat', cls: 'text-signal-ok bg-signal-ok/10', order: 2 }, + cancelled: { label: 'Anulat', cls: 'text-signal-danger bg-signal-danger/10', order: 3 }, +}; + +const PRIORITY_DOT: Record = { + low: 'bg-sky-400', medium: 'bg-signal-warn', high: 'bg-orange-500', critical: 'bg-signal-danger', +}; + +const STATUSES = ['draft', 'active', 'completed', 'cancelled']; + +function isOverdue(ap: ActionPlan): boolean { + return Boolean(ap.dueDate && new Date(ap.dueDate) < new Date() && ap.status === 'active'); +} + +export default function ActionPlansPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + + const [filterStatus, setFilterStatus] = useState(''); + const [showCreate, setShowCreate] = useState(false); + const [form, setForm] = useState({ title: '', priority: 'medium', status: 'draft', owner: '', dueDate: '' }); + + const { data: plans = [], isLoading } = useQuery({ + queryKey: ['action-plans', tenantId, filterStatus], + queryFn: () => apiFetch(`/v1/action-plans${filterStatus ? `?status=${filterStatus}` : ''}`, { tenantId }), + enabled: Boolean(tenantId), + staleTime: 30_000, + }); + + const createMut = useMutation({ + mutationFn: (body: Record) => apiFetch('/v1/action-plans', { tenantId, method: 'POST', body }), + onSuccess: () => { qc.invalidateQueries({ queryKey: ['action-plans', tenantId] }); setShowCreate(false); setForm({ title: '', priority: 'medium', status: 'draft', owner: '', dueDate: '' }); }, + }); + + const patchMut = useMutation({ + mutationFn: ({ id, ...body }: { id: string; status: string }) => apiFetch(`/v1/action-plans/${id}`, { tenantId, method: 'PATCH', body }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['action-plans', tenantId] }), + }); + + const stats = STATUSES.map((s) => ({ status: s, count: plans.filter((p) => p.status === s).length })); + const overdue = plans.filter(isOverdue).length; + + return ( +
+
+
+

Planuri de Acțiune

+

+ {isLoading ? 'Se încarcă…' : `${plans.length} planuri${overdue > 0 ? ` · ${overdue} depășite` : ''}`} +

+
+ +
+ + {/* Summary chips */} +
+ + {stats.map(({ status, count }) => { + const m = STATUS_META[status]; + return ( + + ); + })} + {overdue > 0 && ( + + ⚠ {overdue} depășite + + )} +
+ + {/* Create form */} + {showCreate && ( +
+

Plan nou

+
+
+ setForm({ ...form, title: e.target.value })} + className="w-full rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ + setForm({ ...form, owner: e.target.value })} + className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> + setForm({ ...form, dueDate: e.target.value })} + className="rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring" /> +
+
+ + +
+
+ )} + + {/* List */} + {!isLoading && plans.length === 0 ? ( +
+ Niciun plan de acțiune. Creează primul plan. +
+ ) : ( +
+ {plans.map((ap) => { + const overdue = isOverdue(ap); + const m = STATUS_META[ap.status] ?? STATUS_META.draft; + return ( +
+
+
+
+ {ap.title} + {overdue && DEPĂȘIT} +
+
+ {m.label} + {ap.owner && 👤 {ap.owner}} + {ap.dueDate && 📅 {ap.dueDate}} + {ap.decisionId && ↗ decizie} + {ap.goalId && ↗ obiectiv} +
+
+
+ {ap.status === 'draft' && ( + + )} + {ap.status === 'active' && ( + + )} +
+
+ ); + })} +
+ )} +
+ ); +}