Files
simone 640f986967 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.
2026-06-14 21:00:32 +02:00

579 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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*