🍷 Wine-List — panoramica
La feature Wine-List consente di creare, gestire e pubblicare le liste vini di una location aziendale, con:
- selezione di vini/annate (anche user vintages);
- definizione dei modelli di vendita (calice/bottiglia/caraffa + capacità) e prezzi;
- opzioni di template e sezioni/extra informazioni da mostrare;
- attivazione/disattivazione di una lista per la location;
- QR code per accesso Winelist e AI Sommelier (se sottoscritti);
- gestione del menu piatti associato (categorie e voci).
L’intera feature è lazy sotto dashboard/wine-list e usa un piccolo servizio locale per ricordare la location selezionata.
🧭 Routing & Moduli
// wine-list.module.ts (semplificato)
const WINE_LIST_ROUTES: Route[] = [
{
path: '',
component: WineListComponent, // shell con <router-outlet>
children: [
{ path: '', loadChildren: () => import('./wine-list-homepage/wine-list-homepage.module').then(m => m.WineListHomepageModule) },
{ path: 'create', loadChildren: () => import('./create-wine-list/create-wine-list.module').then(m => m.CreateWineListModule) },
{ path: 'menu/:id', loadChildren: () => import('./edit-list-menu/edit-list-menu.module').then(m => m.EditListMenuModule) },
{ path: ':id', loadChildren: () => import('./edit-wine-list/edit-wine-list.module').then(m => m.EditWineListModule) },
]
}
];
- WineListComponent: shell che resetta la location salvata al
ngOnDestroy. - WineListHomepage: indice per location, QR, attiva/disattiva lista, tabella liste.
- CreateWineList/EditWineList: pagina di dettaglio/modifica riusata tramite
<wine-list-detail-component>. - EditListMenu: gestione menu piatti per la lista (
/menu/:id).
🧠 Contesto location (persistenza locale)
// wineListLocation.service.ts
class WineListLocationService {
private _selectedLocation?: AlbiOption;
getSelectedLocation(): AlbiOption { return this._selectedLocation; }
setSelectedLocation(location?: AlbiOption) {
localStorage.setItem('wineListSelectedLocation', JSON.stringify(location));
this._selectedLocation = location;
}
}
- La location si sceglie da dropdown in homepage; viene persistita in
localStorage. - Se l’utente è vincolato a una location nel suo access, il dropdown viene pre-selezionato e disabilitato.
🏠 Homepage
Dati & stato
companyAddresses$ : AlbiOption[]→ locations ottenute viaCompanyLocationService.locationSelectorControl : FormControl<AlbiOption>→ al change:GET winelists(header:useCompanyHeader,specificLocationId).- Aggiorna
wineLists$e lista attiva. - Costruisce QR options per:
- Winelist →
https://winelist{[-stage]}.albicchiere.com/<locationId> - Sommelier →
https://sommelier{[-stage]}.albicchiere.com/<locationId>(se sottoscrizioneai_sommelierattiva nella company).
- Winelist →
isAiSommelierPlanActive$→ daselectedCompany.subscriptions.
Azioni
- Crea nuova → nav
dashboard/wine-list/create. - Modifica una lista → nav
dashboard/wine-list/:id. - Vai al menu della lista attiva → nav
dashboard/wine-list/menu/:id. - Attiva/Disattiva lista:
POST winelists/:id/enable/POST winelists/:id/disablecon header location.
- Elimina lista:
DELETE winelists/:idcon conferma modale.
- Scarica QR: serializza l’SVG generato da
ngx-qrcode-stylinge scaricaimage.svg.
✍️ Create/Edit: <wine-list-detail-component>
Componente unico per creazione nuova e modifica esistente:
- in Create (route
/create): usato direttamente dal wrapperCreateWineListComponent; - in Edit (route
/:id):EditWineListComponentpassa@Input() wineListId.
Stato principale
- Selezione vini:
selectedWines$ : { vintage? | userVintage?, isSelected, isVisible, modelsPrices[] }[] - Risultati catalogo:
catalogResults$(ricerca con debounce); vistaresults/selected. - Template:
selectedTemplate$ : 'default1' | .... - Set modelli:
wineModelSet$ : Set<'glass|' | 'bottle|750' | 'jug|1000' | ...> - Colonne prezzi:
selectedWineTable$costruito dawineModelSet$
(di default: calice + bottiglia 750) - Extra info (switch):
description,grapes,philosophy,alcohol,foods. - Sezioni (switch):
albicchiereSection,lowAlcoholSection. - Form di supporto:
- Aggiungi Modello:
modelName(glass|bottle|jug) +capacity(ml, opzionale). - Aggiungi Vintage non presente (crea user_vintage).
- Aggiungi Modello:
Ricerca & aggiunta vini
- Catalogo vini/annate (server) con debounce → popola
catalogResults$. - Se non si trova, crea UserVintage:
→ aggiunge la
POST wine/user_vintages
body: {
name, wineryId, wineId, year, wineColor, temperature?, alcohol?
}
headers: { useCompanyHeader: true, specificLocationId }userVintageai selezionati. - Le vintages mostrate includono badge “selezionato” e toggle “mostra tutte le annate”.
Modelli & prezzi
- Aggiungi modello → aggiorna
wineModelSet$e quindi le colonne della tabella prezzi (glass|,bottle|750,jug|…). - Ogni riga (vintage selezionata) espone FormControl per ogni modello:
- validazione: numeri > 0; gruppi invalidi evidenziati (dot rosso, nome evidenziato).
- Il mapping per payload genera per ciascuna vintage:
modelPrices: ModelPrices[] = [
{ modelName: 'glass', capacity: undefined, price: number },
{ modelName: 'bottle', capacity: 750, price: number },
{ modelName: 'jug', capacity: 1000, price: number },
...
]
Salvataggio (ETag + preview)
- Edit (esiste
wineListId):PATCH winelists/:id // header: If-Match: <ETag>
body: {
name, template,
vintages: Array<{ vintageId | userVintageId, modelPrices[] }>,
extraWineData: string[], // es. ['description','grapes',...]
categories: string[], // es. ['albicchiereSection','lowAlcoholSection']
}
headers: { useCompanyHeader: true, specificLocationId } - Create:
POST winelists // body come sopra - Azioni post-save:
- View: genera preview token e apre nuova tab:
GET winelists/:id/preview_token
open https://winelist{[-stage]}.albicchiere.com/preview/:id?token=... - Close: ritorna alla homepage.
- View: genera preview token e apre nuova tab:
🧾 Edit List Menu (/menu/:id)
Pagina per gestire il menu piatti della Wine-List (categorie predefinite):
'appetizers' | 'first_course' | 'second_course' | 'pizza' | 'sides' | 'desserts'.
Stato & forme
winelistData : signal<IWineList>+winelistEtag : signal<string>(ottenuto viagetFullResponse).dishForm : FormGroup→name(req),description,price(req, >0),category(req).- Pipe
filterListMenuper filtrare voci per categoria.
Operazioni (tutte con header location + If-Match ETag in PATCH):
- Aggiungi/Modifica piatto: aggiorna array
menuePATCH winelists/:id { menu } - Rimuovi piatto: conferma modale → aggiorna
menu→PATCH. - Back:
navigate(['dashboard/wine-list']).
🔄 Flussi principali (Mermaid)
1) Home: selezione location, QR e liste
sequenceDiagram
autonumber
participant UI as Homepage
participant Loc as CompanyLocationService
participant Svc as BackendService
UI->>Loc: getCompanyLocations(companyId, role)
Loc-->>UI: locations[]
UI->>UI: set dropdown (persist via WineListLocationService)
UI->>Svc: GET /winelists (specificLocationId)
Svc-->>UI: winelists[]
UI-->>UI: set activeList + build QR options (sommelier/winelist)
2) Create/Edit: selezione, modelli, prezzi, salvataggio
flowchart TD
A[Apri Create/Edit] --> B[Seleziona da catalogo]
B -->|not found| C[Create user_vintage (POST)]
B -->|trovato| D[Aggiungi alla selezione]
D --> E[Aggiungi Modello (glass/bottle/jug + capacità)]
E --> F[Compila prezzi per modello]
F --> G[Salva (POST/PATCH + If-Match ETag)]
G --> H{Action}
H -- "view" --> I[GET preview_token + open preview URL]
H -- "close" --> L[Ritorno homepage]
3) Menu: CRUD piatti
sequenceDiagram
autonumber
participant UI as EditListMenu
participant Svc as BackendService
UI->>Svc: GET (full) /winelists/:id // per ETag
Svc-->>UI: winelist + ETag
UI-->>UI: add/edit/remove in menu[]
UI->>Svc: PATCH /winelists/:id { menu } (If-Match: ETag)
Svc-->>UI: 200 OK + nuovo ETag
🔗 Dipendenze & integrazioni
- albi-ui: tabelle, input, switch, dialog, tag, icone, autocomplete, paginator.
- ngx-qrcode-styling: generazione e download QR (serializzazione SVG).
- CompanyLocationService: elenco location filtrate per access/ruolo.
- WineListLocationService: memoria locale della location selezionata.
- BackendService: CRUD
winelists,wine/user_vintages, attiva/disattiva, token preview. - Stripe (ngx-stripe): apertura customer portal (gestione piano AI Sommelier) dalla homepage.
- TranslateService: testi/titoli localizzati.
✅ Note operative per chi subentra
- Location-first: tutte le chiamate a
winelistsrichiedono l’headerspecificLocationId. - La tabella prezzi è guidata dai modelli in
wineModelSet$→ non duplicare colonne manualmente. - In Edit usare sempre ETag (If-Match) sia per la lista che per il menu.
- Quando si crea un user_vintage, assicurarsi di passarne gli ID coerenti (
wineryId,wineId) e attributi minimi (colore, anno). - Per aggiungere nuovi modelli (es. bottle 375):
- abilita il dialog Add Model con
modelName='bottle'ecapacity=375; - viene automaticamente aggiunta la colonna e i FormControl nelle righe selezionate;
- il payload includerà il nuovo
ModelPrices.
- abilita il dialog Add Model con
- Le sezioni e le extra info sono liste di stringhe nel payload: aggiorna i dizionari di traduzione se cambi nomenclature.
flowchart LR
subgraph Context
SvcLoc[CompanyLocationService] --> Opts[Location options]
Opts --> Persist[WineListLocationService]
end
Persist --> Home[Homepage]
Home --> CreateEdit[Detail Component]
Home --> Menu[Edit List Menu]
CreateEdit --> Preview[Preview token flow]