# TechXCar — Plan 3: Core Workshop Pages **Date:** 2026-06-22 **Status:** Approved ## Scope Build the 3 missing tenant-facing pages: Clients, Catalog, Work Orders. Backend has all 31 endpoints ready. Frontend needs hooks, pages, modals, and routing wired up. --- ## Architecture Approach: **hooks-per-domain + pages**. Each domain has a dedicated hook file in `frontend/src/hooks/` that wraps TanStack Query. Pages consume hooks and stay presentational. ### New files ``` frontend/src/ ├── hooks/ │ ├── useClients.ts │ ├── useCatalog.ts │ └── useWorkOrders.ts ├── components/ │ └── ui/ │ └── dialog.tsx ← shadcn Dialog (shared by all modals) └── pages/app/ ├── clients/ │ ├── ClientsPage.tsx │ └── ClientDetailPage.tsx ├── catalog/ │ └── CatalogPage.tsx └── work-orders/ ├── WorkOrdersPage.tsx └── WorkOrderDetailPage.tsx ``` ### New routes (added to `App.tsx`) ``` /app/clients → ClientsPage /app/clients/:id → ClientDetailPage /app/catalog → CatalogPage /app/work-orders → WorkOrdersPage /app/work-orders/:id → WorkOrderDetailPage ``` --- ## Domain: Clients ### `useClients.ts` | Hook | Method | Endpoint | |---|---|---| | `useClients()` | GET | `/clients` | | `useClient(id)` | GET | `/clients/:id` | | `useCreateClient()` | POST | `/clients` | | `useUpdateClient()` | PUT | `/clients/:id` | | `useDeleteClient()` | DELETE | `/clients/:id` | | `useClientVehicles(clientId)` | GET | `/clients/:id/vehicles` | | `useCreateVehicle(clientId)` | POST | `/clients/:id/vehicles` | | `useUpdateVehicle()` | PUT | `/vehicles/:id` | All mutations call `queryClient.invalidateQueries` on success. ### `ClientsPage` - Table columns: Nome, NIF, Telefone, Email, Data criação - "Novo Cliente" button → modal (create) - Edit icon per row → same modal prefilled - Modal fields: nome (required), NIF, telefone, email, morada, notas ### `ClientDetailPage` - Header: client name + fields (NIF, phone, email, address, notes) + Edit button - "Veículos" section below: table with matrícula, marca, modelo, ano - "Adicionar Veículo" → modal: matrícula (required), marca (required), modelo (required), ano, VIN, quilómetros, notas - Edit icon per vehicle row → same modal prefilled - Back link → `/app/clients` --- ## Domain: Catalog ### `useCatalog.ts` | Hook | Method | Endpoint | |---|---|---| | `useCatalog()` | GET | `/catalog` | | `useCreateCatalogItem()` | POST | `/catalog` | | `useUpdateCatalogItem()` | PUT | `/catalog/:id` | | `useDeleteCatalogItem()` | DELETE | `/catalog/:id` | ### `CatalogPage` - Table columns: Código, Nome, Categoria, Unidade, Preço Base, Estado (badge) - "Novo Item" button → modal (create) - Edit icon per row → modal prefilled - Delete: button toggles to "Confirmar?" before calling DELETE - Modal fields: código, nome (required), categoria, unidade (select: un/hora/litro/kg), preço base (number), activo (checkbox) --- ## Domain: Work Orders ### `useWorkOrders.ts` | Hook | Method | Endpoint | |---|---|---| | `useWorkOrders(status?)` | GET | `/work-orders?status=` | | `useWorkOrder(id)` | GET | `/work-orders/:id` | | `useCreateWorkOrder()` | POST | `/work-orders` | | `useTransitionWorkOrder()` | POST | `/work-orders/:id/transition` | | `useAddWOItem()` | POST | `/work-orders/:id/items` | | `useRemoveWOItem()` | DELETE | `/work-orders/:id/items/:itemId` | | `useAddStaffHours()` | POST | `/work-orders/:id/staff-hours` | | `useRemoveStaffHours()` | DELETE | `/work-orders/:id/staff-hours/:shId` | ### `WorkOrdersPage` - Table columns: Nº OT, Cliente, Veículo, Estado (badge colorido), Data - Status filter: tabs (Todas / Abertas / Em Progresso / Concluídas / Faturadas / Canceladas) - "Nova OT" → modal: cliente (searchable dropdown from `/clients`), veículo (filtered by client), notas internas, notas cliente - Click row → navigate to `/app/work-orders/:id` ### `WorkOrderDetailPage` Two-column layout: **Left column — info + transitions:** - OT number, status badge, client name, vehicle plate + brand/model - Internal notes, client notes (editable inline via PUT) - State stepper: `Aberta → Em Progresso → Concluída → Faturada` - Transition buttons: advance to next state, or "Cancelar OT" (any state except invoiced) - Back link → `/app/work-orders` **Right column — items + hours:** - "Peças / Serviços" table: descrição, qty, preço unitário, desconto %, total - "Adicionar Item" → modal: select catalog item (searchable), qty, unit price (prefilled from catalog), discount % - Remove icon per row (DELETE) - "Horas de Técnico" table: técnico (staff_id), horas, custo/hora, total - "Adicionar Horas" → modal: staff_id (text for now — staff module in Plan 4), horas, custo/hora - Remove icon per row (DELETE) - Totals row at bottom: subtotal peças + subtotal horas + total geral ### State badge colours | Status | Colour | |---|---| | open | slate | | in_progress | blue | | completed | green | | invoiced | purple | | cancelled | red | --- ## Shared components - **`dialog.tsx`** — shadcn Dialog, used by all create/edit modals. Install via shadcn CLI or copy pattern from existing ui components. - All pages follow existing dark slate theme: `bg-slate-950` root, `bg-slate-900` panels, `border-slate-700/800`, white text. - Loading states: `
A carregar...
` - Empty states: `Nenhum registo.
` - Error states: show `ApiError.message` in a red banner. --- ## Out of scope (Plan 4+) - Staff management (staff_id is free text for now) - PDF invoice generation - Notifications (Telegram / email) - Dashboard statistics - Billing / invoicing module