diff --git a/src/app/dashboard/goals/page.tsx b/src/app/dashboard/goals/page.tsx index 8cfe679..f57bebb 100644 --- a/src/app/dashboard/goals/page.tsx +++ b/src/app/dashboard/goals/page.tsx @@ -1,215 +1,194 @@ '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 { useQuery } from '@tanstack/react-query'; +import { apiFetch } from '../../../lib/api'; +import type { Goal } 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; +const STATUS_CFG = { + not_started: { label: 'Neînceput', cls: 'bg-paper-sunken text-ink-faint' }, + on_track: { label: 'Pe plan', cls: 'bg-signal-ok/10 text-signal-ok' }, + at_risk: { label: 'La risc', cls: 'bg-signal-warn/10 text-signal-warn' }, + achieved: { label: 'Realizat', cls: 'bg-signal-ok/20 text-signal-ok font-semibold' }, + abandoned: { label: 'Abandonat', cls: 'bg-paper-sunken text-ink-line line-through' }, +} as const; 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 }, - ), + const { data: goals = [], isLoading, refetch } = useQuery({ + queryKey: ['goals', tenantId], + queryFn: () => apiFetch('/v1/goals', { tenantId }), enabled: Boolean(tenantId), }); - const { register, handleSubmit, reset, formState: { errors, isSubmitting } } = useForm({ - resolver: zodResolver(createGoalSchema), - }); + const [showForm, setShowForm] = useState(false); + const [form, setForm] = useState({ horizon: '', metric: '', target: '' }); + const [saving, setSaving] = useState(false); - 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 byStatus = { + at_risk: goals.filter((g) => g.status === 'at_risk'), + on_track: goals.filter((g) => g.status === 'on_track'), + not_started: goals.filter((g) => g.status === 'not_started'), + achieved: goals.filter((g) => g.status === 'achieved'), + abandoned: goals.filter((g) => g.status === 'abandoned'), + }; - 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] }); - }, - }); + async function handleCreate(e: React.FormEvent) { + e.preventDefault(); + if (!form.horizon || !form.metric || !form.target) return; + setSaving(true); + await apiFetch('/v1/goals', { tenantId, method: 'POST', body: form }); + setForm({ horizon: '', metric: '', target: '' }); + setShowForm(false); + setSaving(false); + refetch(); + } - const deleteGoal = useMutation({ - mutationFn: (id: string) => - apiFetch<{ deleted: boolean }>(`/v1/goals/${id}`, { method: 'DELETE', tenantId }), - onSuccess: async () => { - await queryClient.invalidateQueries({ queryKey: ['goals', tenantId] }); - }, - }); + async function updateStatus(id: string, status: string) { + await apiFetch(`/v1/goals/${id}`, { tenantId, method: 'PATCH', body: { status } }); + refetch(); + } return ( -
-
+
+
-

Obiective

-

Obiectivele strategice urmărite în acest workspace.

+

Obiective & OKR

+

+ Urmărire obiective pe orizont de timp: lunar, trimestrial, anual. +

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

{errors.horizon.message}

} + {showForm && ( + +

Obiectiv nou

+
+
+ + setForm((f) => ({ ...f, horizon: e.target.value }))} + required + /> +
+
+ + setForm((f) => ({ ...f, metric: e.target.value }))} + required + /> +
+
+ + setForm((f) => ({ ...f, target: e.target.value }))} + required + /> +
-
- - - {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}

    -
    -
    - - + {isLoading ? ( +

    Se încarcă…

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

    Niciun obiectiv definit

    +

    Adaugă primul obiectiv pentru a urmări progresul companiei.

    +
    + ) : ( + <> + {/* Summary stats */} +
    + {(Object.keys(STATUS_CFG) as (keyof typeof STATUS_CFG)[]).map((s) => ( +
    +

    {STATUS_CFG[s].label}

    +

    {byStatus[s].length}

    + ))} +
    +

    Total

    +

    {goals.length}

    -
  • - ))} -
+
+ + {/* Goals grouped by status */} + {(['at_risk', 'on_track', 'not_started', 'achieved', 'abandoned'] as const).map((s) => { + const group = byStatus[s]; + if (group.length === 0) return null; + return ( +
+

+ {STATUS_CFG[s].label} ({group.length}) +

+
+ {group.map((goal) => ( +
+
+
+
+ + {goal.horizon} + + + {STATUS_CFG[goal.status].label} + +
+

{goal.metric}

+

{goal.target}

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