diff --git a/src/app/dashboard/tasks/page.tsx b/src/app/dashboard/tasks/page.tsx index c6c16ac..50441ae 100644 --- a/src/app/dashboard/tasks/page.tsx +++ b/src/app/dashboard/tasks/page.tsx @@ -1,131 +1,168 @@ 'use client'; import { useState } from 'react'; -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; -import { apiFetch, type Task, type TaskStatus } from '../../../lib/api'; +import { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import type { Task } from '../../../lib/api'; import { useSession } from '../../../components/session-provider'; -const createTaskSchema = z.object({ - title: z.string().min(1, 'Titlul este obligatoriu').max(500), -}); -type CreateTaskForm = z.infer; +const STATUS_CFG = { + open: { label: 'Deschis', cls: 'bg-paper-sunken text-ink-faint' }, + in_progress: { label: 'În lucru', cls: 'bg-bronze-wash text-bronze-deep' }, + blocked: { label: 'Blocat', cls: 'bg-signal-danger/10 text-signal-danger' }, + done: { label: 'Finalizat', cls: 'bg-signal-ok/10 text-signal-ok' }, + cancelled: { label: 'Anulat', cls: 'bg-paper-sunken text-ink-line line-through' }, +} as const; -const STATUS_LABELS: Record = { - open: 'Deschis', - in_progress: 'În lucru', - blocked: 'Blocat', - done: 'Finalizat', - cancelled: 'Anulat', -}; +const PRIORITY_LABEL: Record = { 1: 'Critic', 2: 'Înalt', 3: 'Normal', 4: 'Scăzut', 5: 'Minim' }; + +function fmtDue(s: string | null) { + if (!s) return null; + const d = new Date(s); + const now = new Date(); + const diff = d.getTime() - now.getTime(); + if (diff < 0) return { label: `Expirat ${d.toLocaleDateString('ro-RO')}`, cls: 'text-signal-danger' }; + if (diff < 86400_000) return { label: 'Azi', cls: 'text-signal-warn' }; + if (diff < 7 * 86400_000) return { label: `${d.toLocaleDateString('ro-RO', { weekday: 'short' })}`, cls: 'text-signal-warn' }; + return { label: d.toLocaleDateString('ro-RO', { month: 'short', day: 'numeric' }), cls: 'text-ink-faint' }; +} export default function TasksPage() { const { activeTenant } = useSession(); const tenantId = activeTenant?.tenantId ?? ''; - const queryClient = useQueryClient(); - const [isFormOpen, setIsFormOpen] = useState(false); - const { data: tasks = [], isLoading } = useQuery({ - queryKey: ['tasks', tenantId, 'all'], + const { data: tasks = [], isLoading, refetch } = useQuery({ + queryKey: ['tasks', tenantId], queryFn: () => apiFetch('/v1/tasks', { tenantId }), enabled: Boolean(tenantId), }); - const { - register, - handleSubmit, - reset, - formState: { errors, isSubmitting }, - } = useForm({ resolver: zodResolver(createTaskSchema) }); + const [showForm, setShowForm] = useState(false); + const [form, setForm] = useState({ title: '', priority: '3', dueAt: '' }); + const [saving, setSaving] = useState(false); + const [filter, setFilter] = useState('all'); - const createTask = useMutation({ - mutationFn: (values: CreateTaskForm) => - apiFetch('/v1/tasks', { method: 'POST', tenantId, body: values }), - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ['tasks', tenantId] }); - reset(); - setIsFormOpen(false); - }, - }); + const filtered = filter === 'all' ? tasks : tasks.filter((t) => t.status === filter); + const overdue = tasks.filter((t) => t.dueAt && new Date(t.dueAt) < new Date() && ['open', 'in_progress', 'blocked'].includes(t.status)); - const updateStatus = useMutation({ - mutationFn: ({ id, status }: { id: string; status: TaskStatus }) => - apiFetch(`/v1/tasks/${id}`, { method: 'PATCH', tenantId, body: { status } }), - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ['tasks', tenantId] }); - }, - }); + async function handleCreate(e: React.FormEvent) { + e.preventDefault(); + if (!form.title) return; + setSaving(true); + await apiFetch('/v1/tasks', { tenantId, method: 'POST', body: { title: form.title, priority: Number(form.priority), dueAt: form.dueAt || undefined } }); + setForm({ title: '', priority: '3', dueAt: '' }); + setShowForm(false); + setSaving(false); + refetch(); + } + + async function updateStatus(id: string, status: string) { + await apiFetch(`/v1/tasks/${id}`, { tenantId, method: 'PATCH', body: { status } }); + refetch(); + } return ( -
-
+
+
-

Sarcini

-

Sarcinile tale și termenele asociate.

+

Taskuri & Operațiuni

+

+ {tasks.length} taskuri active{overdue.length > 0 ? ` • ${overdue.length} expirate` : ''} +

-
- {isFormOpen && ( -
createTask.mutate(values))} - noValidate - className="card mb-6 space-y-4 p-5" - > -
- - - {errors.title &&

{errors.title.message}

} + {showForm && ( + +
+
+ + setForm((f) => ({ ...f, title: e.target.value }))} + required + /> +
+
+ + +
+
+ + setForm((f) => ({ ...f, dueAt: e.target.value }))} + /> +
+
+
+ +
- {createTask.isError && ( -

- {createTask.error instanceof Error ? createTask.error.message : 'Eroare'} -

- )} - )} - {isLoading &&

Se încarcă…

} - {!isLoading && tasks.length === 0 && ( -

Nicio sarcină încă. Adaugă prima mai sus.

- )} - -
    - {tasks.map((task) => ( -
  • -
    -

    - {task.title} -

    - {task.dueAt &&

    Termen: {new Date(task.dueAt).toLocaleDateString('ro-RO')}

    } -
    - -
  • + {/* Filters */} +
    + {[['all', 'Toate'], ...Object.entries(STATUS_CFG).map(([k, v]) => [k, v.label])].map(([k, label]) => ( + ))} -
+
+ + {isLoading ? ( +

Se încarcă…

+ ) : filtered.length === 0 ? ( +
+

{filter === 'all' ? 'Niciun task activ.' : `Niciun task cu statusul "${STATUS_CFG[filter as keyof typeof STATUS_CFG]?.label}".`}

+
+ ) : ( +
+ {filtered.map((task) => { + const due = fmtDue(task.dueAt); + return ( +
+ +
+

+ {task.title} +

+
+
+ {PRIORITY_LABEL[task.priority] ?? 'Normal'} + {due && {due.label}} +
+
+ ); + })} +
+ )}
); }