From 5fb0eab34fc9a9bdc0a44f034a6a881d22386586 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Fri, 31 Jul 2026 16:34:23 +0000 Subject: [PATCH] =?UTF-8?q?feat(goals):=20add=20Goals=20CRUD=20page=20?= =?UTF-8?q?=E2=80=94=20list,=20create,=20update=20status,=20delete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/dashboard/goals/page.tsx | 215 +++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 src/app/dashboard/goals/page.tsx diff --git a/src/app/dashboard/goals/page.tsx b/src/app/dashboard/goals/page.tsx new file mode 100644 index 0000000..8cfe679 --- /dev/null +++ b/src/app/dashboard/goals/page.tsx @@ -0,0 +1,215 @@ +'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 Goal, type GoalStatus } from '../../../lib/api'; +import { useSession } from '../../../components/session-provider'; + +const GOAL_STATUS_LABELS: Record = { + not_started: 'Neînceput', + on_track: 'Pe track', + at_risk: 'La risc', + achieved: 'Atins', + abandoned: 'Abandonat', +}; + +const STATUS_COLORS: Record = { + not_started: 'text-ink-faint', + on_track: 'text-signal-ok', + at_risk: 'text-signal-warn', + achieved: 'text-bronze-deep', + abandoned: 'text-ink-line', +}; + +const createGoalSchema = z.object({ + horizon: z.string().min(1, 'Orizontul este obligatoriu').max(500), + metric: z.string().min(1, 'Metrica este obligatorie').max(500), + target: z.string().min(1, 'Ținta este obligatorie').max(1000), +}); +type CreateGoalForm = z.infer; + +export default function GoalsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const queryClient = useQueryClient(); + const [isFormOpen, setIsFormOpen] = useState(false); + const [statusFilter, setStatusFilter] = useState(''); + + const { data: goals = [], isLoading } = useQuery({ + queryKey: ['goals', tenantId, statusFilter], + queryFn: () => + apiFetch( + `/v1/goals${statusFilter ? `?status=${statusFilter}` : ''}`, + { tenantId }, + ), + enabled: Boolean(tenantId), + }); + + const { register, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm({ + resolver: zodResolver(createGoalSchema), + }); + + const createGoal = useMutation({ + mutationFn: (values: CreateGoalForm) => + apiFetch('/v1/goals', { method: 'POST', tenantId, body: values }), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['goals', tenantId] }); + reset(); + setIsFormOpen(false); + }, + }); + + const updateStatus = useMutation({ + mutationFn: ({ id, status }: { id: string; status: GoalStatus }) => + apiFetch(`/v1/goals/${id}`, { method: 'PATCH', tenantId, body: { status } }), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['goals', tenantId] }); + }, + }); + + const deleteGoal = useMutation({ + mutationFn: (id: string) => + apiFetch<{ deleted: boolean }>(`/v1/goals/${id}`, { method: 'DELETE', tenantId }), + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['goals', tenantId] }); + }, + }); + + return ( +
+
+
+

Obiective

+

Obiectivele strategice urmărite în acest workspace.

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

{errors.horizon.message}

} +
+
+ + + {errors.metric &&

{errors.metric.message}

} +
+
+ + + {errors.target &&

{errors.target.message}

} +
+ {createGoal.isError && ( +

+ {createGoal.error instanceof Error ? createGoal.error.message : 'Eroare la creare'} +

+ )} + +
+ )} + +
+ + {(Object.keys(GOAL_STATUS_LABELS) as GoalStatus[]).map((status) => ( + + ))} +
+ + {isLoading &&

Se încarcă…

} + {!isLoading && goals.length === 0 && ( +

+ {statusFilter + ? `Niciun obiectiv cu statusul "${GOAL_STATUS_LABELS[statusFilter]}".` + : 'Niciun obiectiv încă. Adaugă primul mai sus.'} +

+ )} + +
    + {goals.map((goal) => ( +
  • +
    +
    +

    {goal.horizon}

    +

    {goal.metric}

    +

    {goal.target}

    +
    +
    + + +
    +
    +
  • + ))} +
+
+ ); +}