---
phase: 11-catalog-database-view-ux-legacy-consolidation
plan: 04
subsystem: ui
tags: [react, nextjs, tailwind, server-actions, catalog, inline-edit, tags, search]
# Dependency graph
requires:
- phase: 11-catalog-database-view-ux-legacy-consolidation
plan: "11-02"
provides: "ServiceWithTags type, getAllServices(), updateServiceField/quickAddService/addTagToService/removeTagFromService server actions"
- phase: 11-catalog-database-view-ux-legacy-consolidation
plan: "11-03"
provides: "EditableCell (text/number/textarea/toggle) and TagMultiSelect shared UI primitives"
provides:
- "Database-view /admin/catalog: 6-column inline-editable ServiceTable (Nome, Descrizione, Categoria, Prezzo, Tag, Stato), no Azioni column"
- "QuickAddRow — creates services via quickAddService(name) on Enter, unit_price=0 (D-12)"
- "Active/inactive split with 'Servizi disattivati' divider + opacity-50 (D-13)"
- "CatalogSearch client component — instant client-side name/tag filter, no reload (OFFER-10)"
affects: ["12"]
# Tech tracking
tech-stack:
added: []
patterns:
- "Co-located client filter wrapper (CatalogSearch) between an async Server Component page and a presentational table — useMemo filter over already-fetched data, zero new fetches on query change"
- "ServiceRow per-row error display via a conditional colSpan=6 trailing
, keeping row height stable in the success path"
key-files:
created:
- src/app/admin/catalog/CatalogSearch.tsx
modified:
- src/components/admin/catalog/ServiceTable.tsx
- src/app/admin/catalog/page.tsx
deleted:
- src/components/admin/catalog/ServiceForm.tsx
key-decisions:
- "Left createService (and its serviceSchema) in src/app/admin/catalog/actions.ts as unused dead code rather than editing actions.ts, which was outside this plan's files_modified scope — logged to deferred-items.md for a future cleanup pass."
patterns-established:
- "ServiceTable/CatalogSearch is now the canonical /admin/catalog database-view UX — any future catalog-adjacent admin table (Phase 12+) should follow the same EditableCell/TagMultiSelect/QuickAddRow/search-filter pattern per DESIGN-SYSTEM.md"
requirements-completed: [OFFER-07, OFFER-08, OFFER-09, OFFER-10]
# Metrics
duration: 12min
completed: 2026-06-13
---
# Phase 11 Plan 4: Catalog Database-View UX — ServiceTable Rewrite + Search Summary
**`/admin/catalog` is now a Notion/Airtable-style database view: every `services` cell (name, description, category, price, tags, active status) is click-to-edit via `EditableCell`/`TagMultiSelect`, a quick-add row creates new services on Enter, a search bar filters client-side by name/tag instantly, and inactive services sink below a "Servizi disattivati" divider at reduced opacity — with zero "Azioni" column anywhere.**
## Performance
- **Duration:** ~12 min
- **Started:** 2026-06-13T13:55:00Z
- **Completed:** 2026-06-13T14:02:06Z
- **Tasks:** 2 of 2
- **Files modified:** 3 (1 created, 1 deleted, 2 modified — counting page.tsx + ServiceTable.tsx as modified, CatalogSearch.tsx as created, ServiceForm.tsx as deleted)
## Accomplishments
- `ServiceTable.tsx` fully rewritten as a database-view table: 6 columns (Nome, Descrizione, Categoria, Prezzo, Tag, Stato), every cell rendered via `EditableCell` (text/textarea/number/toggle) or `TagMultiSelect`, no "Azioni" column (D-14).
- `ServiceRow` wires each field's `onSave` to `updateServiceField(service.id, field, value)` and the tags column to `TagMultiSelect`'s `onTagsChanged`; both call `router.refresh()` after the server action resolves. Per-row save errors render inline via a `colSpan={6}` trailing cell without affecting row height in the success path.
- `QuickAddRow` renders a borderless `Input` with placeholder "+ Aggiungi servizio"; pressing Enter with non-empty text calls `quickAddService(name)`, clears the input, and refreshes (D-12).
- Services are partitioned into `activeServices`/`inactiveServices`; active services render first, then `QuickAddRow`, then (if any) a "Servizi disattivati" divider row followed by inactive services with `opacity-50` (D-13).
- New `src/app/admin/catalog/CatalogSearch.tsx` client component: holds search query state, filters `ServiceWithTags[]` by `name` or any `tags[]` entry (case-insensitive substring match) via `useMemo`, and renders `ServiceTable` with the filtered list — purely client-state, no `router.push`/`refresh`/` |