diff --git a/src/app/dashboard/projects/page.tsx b/src/app/dashboard/projects/page.tsx new file mode 100644 index 0000000..a5b8729 --- /dev/null +++ b/src/app/dashboard/projects/page.tsx @@ -0,0 +1,220 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +interface Project { + id: string; name: string; description: string | null; + status: string; priority: string; organizationId: string | null; + startDate: string | null; dueDate: string | null; + tags: string[]; createdAt: string; +} +interface Organization { id: string; name: string; } + +const STATUS_META: Record = { + planning: { label: 'Planificare', cls: 'bg-sky-500/10 text-sky-700 dark:text-sky-300' }, + active: { label: 'Activ', cls: 'bg-signal-ok/10 text-signal-ok' }, + on_hold: { label: 'În pauză', cls: 'bg-signal-warn/10 text-signal-warn' }, + completed: { label: 'Completat', cls: 'bg-primary/10 text-primary' }, + cancelled: { label: 'Anulat', cls: 'bg-muted text-ink-faint' }, +}; +const PRIORITY_META: Record = { + low: { label: 'Scăzut', dot: 'bg-sky-400' }, + medium: { label: 'Mediu', dot: 'bg-signal-warn' }, + high: { label: 'Ridicat', dot: 'bg-orange-500' }, + critical: { label: 'Critic', dot: 'bg-signal-danger' }, +}; + +export default function ProjectsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const qc = useQueryClient(); + const [filterStatus, setFilterStatus] = useState(''); + const [showCreate, setShowCreate] = useState(false); + const [form, setForm] = useState({ + name: '', description: '', status: 'planning', priority: 'medium', + startDate: '', dueDate: '', + }); + + const { data: projects = [], isLoading } = useQuery({ + queryKey: ['projects', tenantId, filterStatus], + queryFn: () => apiFetch(`/v1/projects${filterStatus ? `?status=${filterStatus}` : ''}`, { tenantId }), + enabled: Boolean(tenantId), + staleTime: 60_000, + }); + const { data: orgs = [] } = useQuery({ + queryKey: ['orgs', tenantId], + queryFn: () => apiFetch('/v1/organizations', { tenantId }), + enabled: Boolean(tenantId), + staleTime: 300_000, + }); + const orgMap = Object.fromEntries(orgs.map((o) => [o.id, o.name])); + + const { mutate: createProject, isPending } = useMutation({ + mutationFn: () => apiFetch('/v1/projects', { method: 'POST', body: form, tenantId }), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['projects', tenantId] }); + setShowCreate(false); + setForm({ name: '', description: '', status: 'planning', priority: 'medium', startDate: '', dueDate: '' }); + }, + }); + + const now = new Date(); + const overdue = projects.filter((p) => p.dueDate && new Date(p.dueDate) < now && p.status !== 'completed' && p.status !== 'cancelled'); + + return ( +
+
+
+

Proiecte

+

+ {projects.length} proiecte{overdue.length > 0 ? ` · ${overdue.length} întârziate` : ''} +

+
+ +
+ + {/* Overdue alert */} + {overdue.length > 0 && ( +
+

+ ⚠ {overdue.length} proiect{overdue.length > 1 ? 'e cu termenul depășit' : ' cu termenul depășit'} +

+ {overdue.slice(0, 3).map((p) => ( +

+ {p.name} — termen {new Date(p.dueDate!).toLocaleDateString('ro-RO')} +

+ ))} +
+ )} + + {/* Create form */} + {showCreate && ( +
+

Proiect nou

+
+
+ + setForm((f) => ({ ...f, name: e.target.value }))} + placeholder="Numele proiectului…" + className="w-full rounded-lg border bg-card px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ {[ + { key: 'status', label: 'Status', opts: Object.entries(STATUS_META).map(([k, v]) => ({ value: k, label: v.label })) }, + { key: 'priority', label: 'Prioritate', opts: Object.entries(PRIORITY_META).map(([k, v]) => ({ value: k, label: v.label })) }, + ].map(({ key, label, opts }) => ( +
+ + +
+ ))} + {[ + { key: 'startDate', label: 'Data start' }, + { key: 'dueDate', label: 'Termen limită' }, + ].map(({ key, label }) => ( +
+ + )[key]} + onChange={(e) => setForm((f) => ({ ...f, [key]: e.target.value }))} + className="w-full rounded-lg border bg-card px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" /> +
+ ))} +
+ +