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:
@@ -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 (768–1023px), 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 (768px–1023px):**
|
||||
- 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 16–20px/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*
|
||||
Reference in New Issue
Block a user