---
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 <td>, 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`/`<form>` (OFFER-10).
- `page.tsx` rewritten: remains an async Server Component calling `getAllServices()`, no longer imports `ServiceForm`, renders `CatalogSearch` (or the existing "Nessun servizio..." message, reworded to point at the table's quick-add row, when the catalog is empty).
- `ServiceForm.tsx` deleted entirely — its "+ Aggiungi servizio" full-form creation flow is fully replaced by `ServiceTable`'s `QuickAddRow`.
- `npx tsc --noEmit`: zero errors project-wide. `npx eslint` on all 3 touched files: zero issues. `npx next build`: 0 errors, 0 warnings — `/admin/catalog` (auth-gated dynamic route) was not statically pre-rendered, so no live-DB query was attempted (consistent with the plan's DB-state note; not a firewall block, build was simply clean).

## Task Commits

Each task was committed atomically:

1. **Task 1: Rewrite ServiceTable as database-view (inline edit, tags, quick-add, active/inactive split)** - `c0bedf3` (feat)
2. **Task 2: Add client-side search bar to page.tsx, remove ServiceForm** - `912a892` (feat)

**Plan metadata:** (this commit, following SUMMARY)

## Files Created/Modified

- `src/components/admin/catalog/ServiceTable.tsx` - Full rewrite: `ServiceRow` (EditableCell x5 + TagMultiSelect), `QuickAddRow`, `ServiceTable` with active/inactive split and "Servizi disattivati" divider
- `src/app/admin/catalog/CatalogSearch.tsx` - New client component: search input (Lucide `Search` icon) + `useMemo` name/tag filter wrapping `ServiceTable`
- `src/app/admin/catalog/page.tsx` - Removed `ServiceForm` import/render; now renders `CatalogSearch` with `ServiceWithTags[]` from `getAllServices()`
- `src/components/admin/catalog/ServiceForm.tsx` - Deleted (102 lines); quick-add functionality fully replaced by `ServiceTable`'s `QuickAddRow`

## Decisions Made

- Left `createService` (and the `serviceSchema` zod validator it shares with `updateService`) in `src/app/admin/catalog/actions.ts` as now-unused dead code, rather than editing `actions.ts` — that file was not in this plan's `files_modified` scope, and `updateService` still depends on `serviceSchema`. Logged to `deferred-items.md` (item #6) for a future cleanup pass.
- Followed the plan's prescribed code for `ServiceTable.tsx`/`CatalogSearch.tsx`/`page.tsx` verbatim — no changes needed to satisfy lint/typecheck (unlike Plan 03's `EditableCell`, no `useEffect`/ref patterns were introduced here that would trigger `react-hooks/set-state-in-effect` or `react-hooks/refs`).

## Deviations from Plan

None - plan executed exactly as written. All 12 behavior tests for Task 1 and all 7 behavior tests for Task 2 are satisfied by the code as specified in `11-04-PLAN.md`.

## Issues Encountered

None.

## Known Stubs

None - `ServiceTable` and `CatalogSearch` are both fully wired to live data (`getAllServices()` -> `ServiceWithTags[]`) and Plan 02's server actions (`updateServiceField`, `quickAddService`, `addTagToService`/`removeTagFromService` via `TagMultiSelect`). No hardcoded/empty/placeholder values flow to rendering.

## Threat Flags

None - this plan introduces no new endpoints, auth paths, or trust-boundary changes beyond what was already reviewed in `11-04-PLAN.md`'s threat model:

- T-11-14 (Information Disclosure, accept): `CatalogSearch` receives the full `ServiceWithTags[]` (including inactive services) regardless of search query — same as pre-Phase-11 `ServiceTable`, and `/admin/catalog` remains behind the Auth.js admin session guard.
- T-11-15 (Tampering, mitigate): `QuickAddRow` uses `startTransition` and only clears the input on a successful `quickAddService` resolve; a failed call surfaces an inline error without a partial row (atomic DB insert).
- T-11-16 (DoS, accept): catalog size remains bounded by manual admin entry; no bulk import in this phase.

## User Setup Required

None - no new environment variables or external service configuration needed.

### DB-state note (carried from Plans 01-03, non-blocking for this plan)

The `tags` table (Plan 01) and the `addTagToService`/`removeTagFromService`/`updateServiceField`/`quickAddService` server actions (Plan 02) remain unverified against the **live** Postgres DB — the migration apply step is firewalled and pending manual SSH+docker exec by the user (tracked in `STATE.md` Blockers since Plan 01). This plan's verification was code-only (`npx tsc --noEmit`, `eslint`, `next build`), per the plan's `<db_state_note>`. `next build` did not attempt to reach the live DB for `/admin/catalog` (auth-gated dynamic route, not statically rendered) — the build was simply clean, no firewall error encountered.

**End-to-end runtime verification of the full database-view UX (inline edit persisting, tag creation persisting, quick-add row creating a row, active/inactive split with real data) requires the Plan 01 `tags` table migration to be applied to the live DB first.** This is the same outstanding checkpoint documented in `11-01-SUMMARY.md`, `11-02-SUMMARY.md`, and `11-03-SUMMARY.md`.

## Next Phase Readiness

- **Phase 11 UI deliverable complete:** `/admin/catalog` now satisfies OFFER-07 (inline edit), OFFER-08 (tag multi-select), OFFER-09 (quick-add), OFFER-10 (search/filter), and D-13/D-14 (inactive split, no Azioni column) at the code level. All 4 plans of Phase 11 (01-04) are now executed.
- **Pending before milestone close:** Apply the Plan 01 `tags` table migration (`scripts/push-11-tags-migration.ts`) and the Phase 11 legacy-consolidation scripts (`migrate-services.ts`, `validate-services-migration.ts`, `migrate-tags.ts`, `validate-tags-migration.ts`) to the live Postgres DB via SSH+docker exec, per `STATE.md` Blockers. This is required for OFFER-13 and for end-to-end runtime verification of this plan's UI.
- **Recommendation for next session:** Resolve the DB-migration checkpoint (apply Plan 01's migration + Phase 11 consolidation scripts), then do a manual smoke test of `/admin/catalog`: inline-edit each column type, add/remove a tag (including creating a brand-new tag), use the quick-add row, toggle a service's active state, and verify the search bar filters by name/tag.
- **Phase 12 readiness:** `ServiceWithTags`/`getAllServices()`/`updateServiceField` etc. are the now-proven query/action/UI stack for `services` — Phase 12 (Offer Composition Drag&Drop & CSV Import) can build on this same foundation when rewiring `/admin/offers` to read from `services`.
- **Cleanup candidate (non-blocking):** `createService`/`serviceSchema` dead code in `src/app/admin/catalog/actions.ts` (deferred-items.md #6).

---
*Phase: 11-catalog-database-view-ux-legacy-consolidation*
*Completed: 2026-06-13*

## Self-Check: PASSED

- FOUND: src/components/admin/catalog/ServiceTable.tsx
- FOUND: src/app/admin/catalog/page.tsx
- FOUND: src/app/admin/catalog/CatalogSearch.tsx
- CONFIRMED DELETED: src/components/admin/catalog/ServiceForm.tsx
- FOUND: .planning/phases/11-catalog-database-view-ux-legacy-consolidation/11-04-SUMMARY.md
- FOUND commit: c0bedf3
- FOUND commit: 912a892
- FOUND commit: 9bc1e43
