docs(12): create phase plan

Re-scoped Offer Editor (Tier A/B/C, Tag & Prezzo Pubblico) per 2026-06-14
user decision: 5-plan wave structure (schema migration -> prod apply
checkpoint -> query/action layer -> list + detail editor UI), OFFER-12
CSV/Notion import deferred.
This commit is contained in:
2026-06-14 21:00:32 +02:00
parent 9bff7623da
commit 640f986967
11 changed files with 3303 additions and 28 deletions
@@ -0,0 +1,578 @@
---
phase: 12
slug: offer-editor-tier-tag-prezzo-pubblico
status: draft
shadcn_initialized: true
preset: default
created: 2026-06-14
---
# Phase 12 — UI Design Contract
## Offer Editor — Tier A/B/C, Tag & Prezzo Pubblico
> Visual and interaction contract for the Offer Editor: list page + detail editor with 3-tier checkbox matrix, multi-dimensional tags, and structured transformation promise. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | shadcn/ui |
| Preset | default (existing, Phase 11 forward) |
| Component library | radix |
| Icon library | Lucide React |
| Font | Geist Sans |
| Design direction | ClickUp/Pipedrive minimal flat style (per DESIGN-SYSTEM.md) |
**Notes:** Phase 12 builds on Phase 11's established database-view UX (inline edit, tag multi-select, EditableCell). All colors, spacing, typography inherited from Phase 11 and project globals. No new design system tokens introduced.
---
## Spacing Scale
Multiples of 4px per project standard:
| Token | Value | Usage |
|-------|-------|-------|
| xs | 4px | Icon gaps, inline padding |
| sm | 8px | Compact element spacing, tag chip gaps |
| md | 16px | Default cell/control spacing, table row padding |
| lg | 24px | Section padding, card margin, tag dimension spacing |
| xl | 32px | Layout gaps between sections |
| 2xl | 48px | Major section breaks |
| 3xl | 64px | Page-level spacing |
Exceptions: None
---
## Typography
Phase 12 reuses Phase 11 typography (no new sizes introduced). Two weights only (400 regular, 600 bold):
| Role | Size | Weight | Line Height |
|------|------|--------|-------------|
| Body | 14px | 400 | 1.5 |
| Label | 12px | 400 | 1.5 |
| Heading (h3) | 16px | 600 | 1.2 |
| Heading (h2) | 20px | 600 | 1.2 |
---
## Color
Phase 12 uses Phase 11 color contract (invariant brand tokens):
| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | #ffffff | Page/card backgrounds, editor surfaces |
| Secondary (30%) | #f9f9f9 | Muted backgrounds, table headers, category filter chips (inactive) |
| Accent (10%) | #1A463C | Primary CTAs ("Salva Offerta"), focus ring, active/selected state, active filter chips |
| Accent secondary | #DEF168 | Status indicators (archived badge), active offer state |
| Destructive | #dc2626 | Archive/delete actions only |
| Muted text | #71717a | Placeholders, metadata, disabled states, inactive labels |
| Border | #e5e7eb | Cell/block dividers, table borders |
**Accent reserved for:**
- "Salva Offerta" primary CTA button
- Focus ring on EditableCell, TagMultiSelect, and input fields
- Active state indicator (category filter chip, active tags)
- Selection highlight in checkbox matrix
---
## Components & Interaction Patterns
All patterns inherited from Phase 11 (EditableCell, TagMultiSelect, database-view table) and extended for offer composition and tier management.
### 1. Offer List Page (`/admin/offers`)
**Purpose:** Display all offers (active + optionally archived) in a filterable card grid. Admin can create new offers, filter by category, and toggle archive view.
**Layout Structure:**
```
┌──────────────────────────────────────────────────────────────┐
│ Offerte [+ Nuova Offerta] │
├──────────────────────────────────────────────────────────────┤
│ Filter row: │
│ [Tutti] [Entry Offer] [Signature Offer] [Retainer Offer] │
│ Mostra offerte archiviate: [toggle off] │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Offerta 1 │ │ Offerta 2 │ │ Offerta 3 │ │
│ │ Breve desc │ │ Breve desc │ │ Breve desc │ │
│ │ [Signature] │ │ [Entry] │ │ [Retainer] │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ [card 4] [card 5] [card 6] │
└──────────────────────────────────────────────────────────────┘
```
#### A. Page Header
- **Title:** "Offerte" (h2, 20px/600, black)
- **Primary CTA:** "+ Nuova Offerta" button (bg #1A463C, text white, right-aligned)
#### B. Filter Row
- **Category filter chips:** Buttons for "Tutti", "Entry Offer", "Signature Offer", "Retainer Offer"
- Inactive: border #e5e7eb, text #71717a, bg transparent
- Active: border #1A463C, text #1A463C, bg transparent (or solid #1A463C text white — planner decides)
- Spacing between chips: 8px (sm)
- **Archive toggle:** "Mostra offerte archiviate" text label + checkbox or toggle switch
- Position: right side of filter row OR below filter chips on tablet/mobile
- Label color: #71717a
- Toggle: standard checkbox or `<Switch>` component from shadcn
#### C. Offer Card Grid
- **Grid layout:** 3 columns on desktop (≥1024px), 2 on tablet (7681023px), 1 on mobile (<768px)
- **Card dimensions:** ~280px width, responsive height (min 140px)
- **Card structure:**
```
┌───────────────────────────┐
│ Offerta Name (editable?) │ ← h3/16px/600
│ │
│ Breve descrizione │ ← body/14px/400, 2 lines max, ellipsis
│ │
│ [Categoria Chip] │ ← single category badge
│ │
│ [archived badge] [edit] │ ← conditional
└───────────────────────────┘
```
- **Card styling:**
- Background: white (#ffffff)
- Border: #e5e7eb, 1px
- Padding: 16px (md)
- Border-radius: 8px
- Hover: `cursor-pointer`, subtle shadow (box-shadow: 0 4px 12px rgba(0,0,0,0.08))
- **Offer name:** Clickable, links to detail editor
- **Short description:** Body text, truncated at 2 lines
- **Category badge:** Single chip (Entry/Signature/Retainer), color-coded (TBD by planner; suggest brand palette shades)
- **Archived badge:** Red text "Archiviata" (only if offer is archived)
- **Edit icon:** Optional Lucide pencil icon (top-right corner), links to detail editor
#### D. Empty State
- When no offers exist: centered section with
- Icon: Lucide briefcase or package (48px, #71717a)
- Heading: "Nessuna offerta" (h3)
- Copy: "Inizia creando la tua prima offerta" (body, #71717a)
- CTA: "+ Nuova Offerta" button
---
### 2. Offer Editor (Detail Page) (`/admin/offers/[id]/edit` or modal)
**Purpose:** Create/edit a complete offer with 3 tiers (A/B/C), service composition via checkbox matrix, tag system, and transformation promise.
**Page Structure:**
```
┌───────────────────────────────────────────────────────────────┐
│ ← Indietro | Modifica Offerta │
├───────────────────────────────────────────────────────────────┤
│ │
│ [Offerta Name — editable inline] [Category] [Ticket] │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Tags (4 dimensions): │ │
│ │ Categoria: [Signature Offer] │ │
│ │ Ticket: [Low Ticket / Mid Ticket / High Ticket] │ │
│ │ Tipo: [Audit] [Done For You] [+ Crea Tipo] │ │
│ │ Obiettivo: [Primo Acquisto] [Up Scala] [+ Crea Obj] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ [Divider] │
│ │
│ SERVIZI INCLUSI │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Servizio Prezzo │ A │ B │ C │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ Service 1 €100 │ ☑ │ ☐ │ ☐ │ │ │
│ │ Service 2 €250 │ ☐ │ ☑ │ ☑ │ │ │
│ │ ... ... │...│...│... │ │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ Totale Servizi — │€350│€500│€250│ │ │
│ ├─────────────────────────────────────────────────────────┤ │
│ │ Prezzo Pubblico — │€499│€699│€349│ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ [Divider] │
│ │
│ PROMESSA DI TRASFORMAZIONE │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Aiuto: [Client Ideal — editable inline] │ │
│ │ A ottenere: [Result — editable inline] │ │
│ │ In: [Estimated Time — editable inline] │ │
│ │ Senza: [Pain Point — editable inline] │ │
│ │ Grazie a: [Proprietary Method — editable inline] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ [Salva Offerta] [Annulla] [Archivia] │
└───────────────────────────────────────────────────────────────┘
```
#### A. Header & Navigation
- **Back button:** "← Indietro" (left-aligned, links to `/admin/offers`)
- **Page title:** "Modifica Offerta" (h2, 20px/600) OR "Crea Offerta" if new
#### B. Offer Name & Quick Category/Ticket Display
- **Name field:** EditableCell (text), required, displayed as h3
- **Category chip:** Single-select tag (Entry/Signature/Retainer), visually prominent
- **Ticket chip:** Single-select tag (Low/Mid/High Ticket), visually secondary
#### C. Tags Block (4 Dimensions)
**Structure:**
- Section label: "Tag" (h3, 16px/600)
- Four rows (one per dimension):
1. **Categoria** (single): "Entry Offer" / "Signature Offer" / "Retainer Offer"
2. **Ticket** (single): "Low Ticket" / "Mid Ticket" / "High Ticket"
3. **Tipo** (multi): "Audit", "Done For You", "+ Crea Tipo" (add new)
4. **Obiettivo** (multi): "Primo Acquisto", "Up Scala Valore", "Lead Generation", "+ Crea Obiettivo"
**Styling per dimension:**
- Dimension label: 12px/400, bold, #71717a
- Tags/chips: Use inherited TagMultiSelect component (from Phase 11)
- Single-select (Categoria, Ticket): radio-like, one active chip at a time
- Multi-select (Tipo, Obiettivo): checkbox-like, multiple active chips
- Color: deterministic hash-based colors (6-8 color palette, per Phase 11 pattern)
- Add action: "+ Crea [Dimension Name]" link/button inline
- Spacing between rows: 16px (md)
#### D. Services Matrix (Tier A/B/C)
**Section label:** "Servizi Inclusi" (h3)
**Table structure:**
- **Columns:** Service Name | Unit Price | Tier A (checkbox) | Tier B (checkbox) | Tier C (checkbox)
- **Header row:**
- Left: "Servizio" (12px/400, bold)
- Cols: "Prezzo" (right-aligned) + "A", "B", "C" (centered, column headers)
- Header background: #f9f9f9
- Header border-bottom: #e5e7eb
- **Service rows:**
- Service name (left, flex-grow)
- Unit price (right-aligned, tabular nums, "€X,XX")
- Checkbox per tier (centered)
- Style: standard `<input type="checkbox">`
- Color: #1A463C when checked
- Interaction: Click to toggle; live total updates immediately
- Row height: ~40px
- Border-bottom: #e5e7eb
- Hover: subtle background #f9f9f9 on the entire row
- **Services filtered by category:** Only show services whose `services.category` matches the offer's selected category (Categoria tag)
- **Row order:** Configurable (drag-to-reorder via @dnd-kit optional; planner decides)
**Totals rows** (sticky to bottom of table):
1. **Totale Servizi** (auto-calculated):
- Row label: "Totale Servizi" (bold, 14px/600)
- Per tier: SUM of unit_price for all services with checkbox ☑ in that tier
- Format: "€X,XXX.XX"
- Updates **live** as checkboxes are toggled (no page reload)
- Color: bold text, #1a1a1a
2. **Prezzo Pubblico** (manual input):
- Row label: "Prezzo Pubblico" (bold, 14px/600)
- Per tier: EditableCell (number type)
- Placeholder: "€0,00" or empty
- Format: "€X,XXX.XX"
- Interaction: Click to edit, blur/Enter to save, Esc to cancel
- Independent of "Totale Servizi" — user can set any value
- Validation: numeric, ≥ 0, max 2 decimals
**Empty state (no services matching category):**
- Placeholder text: "Nessun servizio disponibile per questa categoria" (#71717a)
- Table still visible but with placeholder row
---
#### E. Transformation Promise Block
**Section label:** "Promessa di Trasformazione" (h3)
**Structure:** 5 editable fields, each a key-value pair:
```
Aiuto: [Client Ideal — input text, 60 chars]
A ottenere: [Result — input text, 60 chars]
In: [Time — input text, 30 chars, e.g. "3 mesi"]
Senza: [Pain — input text, 60 chars]
Grazie a: [Method — input text, 60 chars]
```
**Implementation:**
- Each field is an EditableCell (text type, optional)
- Field label (dim): 12px/400, color #71717a
- Input placeholder: "Aggiungi [field name]" (muted)
- No asterisk (all optional)
- Layout: horizontal label-value pairs, or stacked label-over-input depending on space
- On desktop: consider 2-column or full-width labels
- Tablet/mobile: stack vertically
- Spacing between fields: 16px (md)
- Background: optional subtle bg #f9f9f9, or transparent (planner decides)
- Border: none, or bottom-border only on each field
- Focus ring: #1A463C when in edit mode (EditableCell pattern)
---
#### F. Action Buttons
**Bottom of editor:**
- **[Salva Offerta]** (primary CTA)
- Button: bg #1A463C, text white, 14px/600, padding 16px 24px
- Position: left-aligned
- On click: Submit form, validate all required fields (name + category + at least 1 service selected in 1 tier), save to DB, redirect to `/admin/offers`, show toast "Offerta salvata"
- Disabled state: if no tiers have services selected, button disabled with tooltip "Seleziona almeno un servizio"
- **[Annulla]** (secondary CTA)
- Button: border #e5e7eb, text #1a1a1a, no fill
- On click: Discard unsaved changes, redirect to `/admin/offers`
- Optional confirmation: if form is dirty, "Scartare le modifiche?" dialog
- **[Archivia]** (destructive action, visible only on edit, not on create)
- Button: text #dc2626, no fill
- On click: Toggle offer `is_archived` flag
- Confirmation required: "Sei sicuro? L'offerta non sarà visibile nella lista." [Annulla] [Archivia]
- After confirm: Save, show toast "Offerta archiviata", redirect to `/admin/offers`
**Spacing between buttons:** 8px (sm)
---
### 3. Tag Creation Inline ("Crea Tipo", "Crea Obiettivo")
**Interaction:** When user clicks "+ Crea [Dimension]", an input field appears inline (or in a small popover).
**Structure:**
- Text input: placeholder "Nome [Dimension]..."
- On Enter or click checkmark: Create tag (server action `addTagToOffer`), add to active tags list
- On Esc or click X: Cancel, close input
- On success: Input clears, new tag appears in the chip list with deterministic color
**Validation:**
- Required: tag name 1-30 chars
- Trimmed whitespace
- Case-insensitive duplicate check per dimension (warn: "Questo tag esiste già")
---
## Copywriting Contract
| Element | Copy | Context |
|---------|------|---------|
| **Offer list heading** | "Offerte" | Page title |
| **Create offer CTA** | "+ Nuova Offerta" | Primary action on list |
| **Back link** | "← Indietro" | Breadcrumb/nav on detail |
| **Archive toggle label** | "Mostra offerte archiviate" | Filter checkbox |
| **Edit page heading** | "Modifica Offerta" | Detail page title (or "Crea Offerta" if new) |
| **Tags section label** | "Tag" | Section heading |
| **Category dimension** | "Categoria" | Single-select label |
| **Ticket dimension** | "Ticket" | Single-select label |
| **Type dimension** | "Tipo" | Multi-select label |
| **Objective dimension** | "Obiettivo" | Multi-select label |
| **Create type link** | "+ Crea Tipo" | Inline tag creation |
| **Create objective link** | "+ Crea Obiettivo" | Inline tag creation |
| **Services section label** | "Servizi Inclusi" | Table heading |
| **Service column header** | "Servizio" | Table column |
| **Price column header** | "Prezzo" | Table column |
| **Tier headers** | "A", "B", "C" | Table columns (single letters) |
| **Services total label** | "Totale Servizi" | Calculated row |
| **Public price label** | "Prezzo Pubblico" | Manual input row |
| **Transformation block label** | "Promessa di Trasformazione" | Section heading |
| **Ideal client label** | "Aiuto" | Field label |
| **Result label** | "A ottenere" | Field label |
| **Duration label** | "In" | Field label (e.g., "3 mesi") |
| **Pain label** | "Senza" | Field label |
| **Method label** | "Grazie a" | Field label |
| **Save CTA** | "Salva Offerta" | Primary action (verb + noun) |
| **Cancel CTA** | "Annulla" | Secondary action |
| **Archive CTA** | "Archivia" | Destructive action (visible on edit only) |
| **Archive confirmation** | "Sei sicuro? L'offerta non sarà visibile nella lista." | Destructive confirmation copy + context |
| **Empty services message** | "Nessun servizio disponibile per questa categoria" | Conditional placeholder |
| **Empty offers message** | "Nessuna offerta" | List page empty state heading |
| **Empty offers hint** | "Inizia creando la tua prima offerta" | List page empty state body |
| **Save success toast** | "Offerta salvata" | Success feedback |
| **Archive success toast** | "Offerta archiviata" | Success feedback |
| **Save error toast** | "Errore nel salvataggio. Verifica i campi." | Error feedback |
| **Tag name placeholder** | "Nome tag..." | Create tag input |
| **Duplicate tag warning** | "Questo tag esiste già" | Validation error |
| **Disabled save tooltip** | "Seleziona almeno un servizio" | Constraint message |
---
## Interaction & State Management
### Checkbox Matrix Behavior
- **On checkbox toggle:** Live recalculation of "Totale Servizi" for that tier (no network call)
- **Debounce:** None — updates instant as user clicks
- **Persistence:** On "Salva Offerta" click, POST all tier compositions to backend
- **Validation on save:** At least 1 tier must have ≥1 service selected (backend confirms)
### Tag Dimensions
- **Categoria & Ticket:** Single-select (user can only have one value per offer)
- UI: Radio-button-like chips or dropdown — planner decides
- Switching to a different service category auto-filters the services matrix
- **Tipo & Obiettivo:** Multi-select (user can have 0..N values per offer)
- UI: Checkbox-like chips (inherited TagMultiSelect pattern)
- Tag creation inline: "+ Crea Tipo" / "+ Crea Obiettivo"
- Duplicate check: case-insensitive per dimension (warn and skip)
- Color: deterministic based on tag name hash (6-8 color palette, per Phase 11)
### Archive Workflow
- Archive is a toggle, not permanent deletion
- Archived offers are hidden from list by default (togglable via "Mostra offerte archiviate")
- On edit page, "Archivia" button only visible if offer is not already archived (or toggle text changes to "Ripristina")
- Confirmation required for destructive state change
### Edit State Management
- Form is dirty if any field changed since load
- On cancel with unsaved changes: confirmation dialog "Scartare le modifiche?"
- On back button navigation: same check (browser may warn too)
- Auto-save optional (planner decides) — recommend explicit "Salva" for clarity
---
## Visual States
### Offer Card States
| State | Visual |
|-------|--------|
| Default | White card, border #e5e7eb, normal shadow |
| Hover | `cursor-pointer`, shadow deepens (0 4px 12px rgba(0,0,0,0.08)) |
| Archived | Gray text, optional red "Archiviata" badge top-right |
### Checkbox States
| State | Visual |
|-------|--------|
| Unchecked | Empty square, border #e5e7eb |
| Checked | Filled square, bg #1A463C, white checkmark |
| Hover | Cursor pointer, subtle border darken |
| Disabled | Gray box, opacity-50, no cursor |
### Editable Field States (EditableCell)
| State | Visual |
|-------|--------|
| Display mode | Normal text, `cursor-pointer`, hover bg #f0f0f0 |
| Edit mode | Input with ring-1 ring-#1A463C on focus |
| Error | Ring-1 ring-#dc2626, error message below |
| Disabled | Gray text, opacity-50, no cursor |
### Tag States (TagMultiSelect)
| State | Visual |
|-------|--------|
| Display | Colored chip (6-8 colors), text inside, X icon to remove |
| Hover | Slight opacity change on X icon |
| Add mode | Input field + "+ Aggiungi" button (or Enter to confirm) |
| Create new | Temporary placeholder, then colored chip with new tag name |
### Category Filter Chip States (List Page)
| State | Visual |
|-------|--------|
| Inactive | Border #e5e7eb, text #71717a, bg transparent, `cursor-pointer` |
| Active | Border #1A463C, text #1A463C (or bg #1A463C text white — planner decides) |
| Hover | Border darken slightly |
---
## Responsive Behavior
**Desktop (≥1024px):**
- Offer list: 3-column card grid
- Filter chips: horizontal row
- Services matrix: full-width table, horizontal scroll if needed
- Editor sections: full-width, stacked vertically
**Tablet (768px1023px):**
- Offer list: 2-column card grid
- Filter chips: wrap horizontally or scroll if needed
- Services matrix: horizontal scroll (sticky left column optional)
- Tag fields: stack or 2-column layout (planner decides)
**Mobile (<768px):**
- Offer list: 1-column card stack
- Filter chips: vertical stack OR horizontal scroll in a container
- Services matrix: horizontal scroll with sticky "Servizio" + "Prezzo" columns
- Tag fields: stack vertically
- Buttons: full-width stack
- Archive toggle: move below filter chips
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | button, input, dialog, badge, table, form, checkbox | not required (official shadcn) |
| @dnd-kit | core, sortable, utilities (optional for row reordering) | not required (npm package, not registry) |
---
## Checker Sign-Off
- [ ] Dimension 1 Copywriting: All labels/CTAs use verb+noun, destructive actions clear ("Archivia" + confirmation), tag creation inline
- [ ] Dimension 2 Visuals: Checkbox matrix clear, live totals update instant, archive badge visible, tag colors consistent
- [ ] Dimension 3 Color: Accent (#1A463C) reserved for primary CTA ("Salva Offerta"), focus ring, active filter chip; destructive (#dc2626) for archive only
- [ ] Dimension 4 Typography: Body 14px/400, labels 12px/400, headings 1620px/600 — all per Phase 11 contract (2 weights only)
- [ ] Dimension 5 Spacing: All gaps/padding multiples of 4px, card padding 16px (md), section gaps 24px (lg), row height ~40px
- [ ] Dimension 6 Registry Safety: shadcn official + npm packages, no third-party registries
**Approval:** pending
---
## Notes for Planner
1. **Service filtering by category:** Implement dynamic filter on the services matrix when Categoria tag changes. Query: fetch all services WHERE `services.category = offer.categoria_tag_value`.
2. **Live total calculation:** Client-side state, no server call. Recalculate sum of unit prices for all checked services in each tier on every checkbox toggle.
3. **Three tiers (A/B/C):** Map to `offer_micros` table, one micro per tier. Store which services are assigned via new `offer_tier_services` junction table (additive, not modifying legacy `offer_micro_services`).
4. **Tag dimensions:** Use Phase 11's polymorphic `tags` table with `entity_type = "offer_macros"`. Add optional `dimension` column to distinguish Categoria/Ticket/Tipo/Obiettivo (or use tag name prefixes, planner decides). Single-select for Categoria/Ticket likely stored on `offer_macros` columns directly (simpler).
5. **Tag creation on the fly:** User can create new tag values during editing. Server action: `addTagToOffer(offerId, dimension, tagName)`. Validation + idempotency (onConflictDoNothing).
6. **Archive flag:** Add boolean `is_archived` column to `offer_macros` (additive migration). Filter in list query: WHERE is_archived = false (or include if toggle is on).
7. **Edit page routing:** Consider `/admin/offers/[id]/edit` (new page) or modal overlay. Both work; full-page simpler for first iteration.
8. **Services matrix columns:** Each tier (A/B/C) is visually a column with checkboxes. Sticky header recommended. Sticky left column (service name + price) optional on mobile/tablet.
9. **Transformation promise fields:** All optional. Can be empty string. EditableCell (text) pattern from Phase 11.
10. **Disable "Salva" button validation:**
- Rule: at least 1 tier must have ≥1 service checked
- If all tiers unchecked: button disabled with tooltip
- On submit: backend also validates (server-side check)
11. **Category filter on list:** Clicking a chip sets active filter. API: `GET /admin/offers?category=entry` (or all if "Tutti"). Auto-refresh list.
12. **Migration strategy:** Additive only. Do NOT modify existing `offer_macros`, `offer_micros`, `offer_micro_services` — extend with:
- `offer_tier_services` (new junction: tier_id → service_id)
- `offer_macros.is_archived` (boolean, default false)
- Optionally: structured promise fields on `offer_macros` (cliente_ideale, risultato, tempo, pain, metodo)
- Optionally: `services.category` → verify not already used (check Phase 11 usage first)
13. **Dry run before prod:** Test with existing offer data. Verify no data loss. Run audit script to confirm migration integrity.
---
## Requirements Traceability
| Requirement | Phase 12 Coverage |
|-------------|------------------|
| OFFER-11 | Offer editor: checkbox matrix, 3 tiers (A/B/C), live totals ✓ |
| OFFER-15 | 4-dimensional tags (Categoria/Ticket/Tipo/Obiettivo), creatable on-the-fly ✓ |
| OFFER-16 | Manual "Prezzo Pubblico" per tier, independent of service total ✓ |
| OFFER-17 | Structured transformation promise (5 fields) ✓ |
| OFFER-18 | List page filterable by category, archive toggle ✓ |
---
## Deferred Items
- **CSV/Notion import** (OFFER-12): Deferred to future phase per user decision (2026-06-14)
- **Drag-to-reorder services** (optional @dnd-kit): Not required for MVP; can be added in post-phase enhancement
---
*UI-SPEC regenerated: 2026-06-14*
*Design system source: Phase 11 (inherited), no new tokens*
*Scope: Offer Editor (list + detail) with 3-tier checkbox matrix, multi-dimensional tags, manual pricing, transformation promise*