diff --git a/src/app/dashboard/projects/page.tsx b/src/app/dashboard/projects/page.tsx index a5b8729..50d79ee 100644 --- a/src/app/dashboard/projects/page.tsx +++ b/src/app/dashboard/projects/page.tsx @@ -1,214 +1,203 @@ 'use client'; -import { useState } from 'react'; +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 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; } +interface Goal { id: string; title: string; description: string | null; status: string; progress: number | null; tags: string[]; targetDate: string | null; createdAt: string; } +interface Task { id: string; title: string; status: string; tags: string[]; createdAt: 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' }, +const PROJECT_TAGS = ['proiect','project','initiative','program']; +const STATUS_COLORS: Record = { + active:'text-signal-ok', completed:'text-ink-faint', cancelled:'text-signal-danger', paused:'text-warn', archived:'text-ink-faint', }; 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 [showAdd, setShowAdd] = useState(false); + const [expanded, setExpanded] = useState(null); + const [form, setForm] = useState({ title: '', description: '', targetDate: '' }); + + const { data: goals = [], isLoading: loadG } = useQuery({ + queryKey: ['projects-goals', tenantId], + queryFn: () => apiFetch('/v1/goals?limit=200', { tenantId }), + enabled: Boolean(tenantId), staleTime: 60_000, }); - 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: tasks = [], isLoading: loadT } = useQuery({ + queryKey: ['projects-tasks', tenantId], + queryFn: () => apiFetch('/v1/tasks?limit=500', { 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 }), + const projects = useMemo(() => + goals.filter((g) => g.tags.some((t) => PROJECT_TAGS.includes(t.toLowerCase()))), + [goals]); + + function projectTasks(projectId: string): Task[] { + return tasks.filter((t) => t.tags.includes(`project-${projectId}`) || t.tags.includes(`goal-${projectId}`)); + } + + function computeProgress(p: Goal): number { + if (p.progress !== null) return p.progress; + const pts = projectTasks(p.id); + if (pts.length === 0) return 0; + return Math.round((pts.filter((t) => t.status === 'completed').length / pts.length) * 100); + } + + const patchMut = useMutation({ + mutationFn: ({ id, status }: { id: string; status: string }) => + apiFetch(`/v1/goals/${id}`, { tenantId, method: 'PATCH', body: { status } }), + onSuccess: () => qc.invalidateQueries({ queryKey: ['projects-goals', tenantId] }), + }); + + const addMut = useMutation({ + mutationFn: () => apiFetch('/v1/goals', { tenantId, method: 'POST', body: { + title: form.title, + description: form.description || undefined, + tags: ['proiect', 'project'], + status: 'active', + progress: 0, + targetDate: form.targetDate || undefined, + }}), onSuccess: () => { - qc.invalidateQueries({ queryKey: ['projects', tenantId] }); - setShowCreate(false); - setForm({ name: '', description: '', status: 'planning', priority: 'medium', startDate: '', dueDate: '' }); + qc.invalidateQueries({ queryKey: ['projects-goals', tenantId] }); + setShowAdd(false); + setForm({ title: '', description: '', targetDate: '' }); }, }); - const now = new Date(); - const overdue = projects.filter((p) => p.dueDate && new Date(p.dueDate) < now && p.status !== 'completed' && p.status !== 'cancelled'); + const byStatus = useMemo(() => { + const map: Record = {}; + for (const p of projects) { + const s = p.status ?? 'active'; + map[s] = [...(map[s] ?? []), p]; + } + return map; + }, [projects]); return ( -
-
+
+
-

Proiecte

+

Project Tracker

- {projects.length} proiecte{overdue.length > 0 ? ` · ${overdue.length} întârziate` : ''} + {projects.filter((p) => p.status === 'active').length} active · {projects.filter((p) => p.status === 'completed').length} finalizate

-
- {/* 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" /> -
- ))} -
- -