🍇 Cantina — panoramica
La feature Cantina mostra l’inventario per location (indirizzo) della company attiva: annate con relativi contenitori (Smartbag/Bottle), quantità disponibili e apertura/consumo delle Smartbag.
È accessibile sia a Cantina (WINERY) sia a Professional, con funzioni coerenti al ruolo.
🧭 Routing & Modulo
// src/dashboards/cantina/cantina.module.ts
@NgModule({
declarations: [CantinaComponent],
imports: [
SharedModule,
CommonModule,
RouterModule.forChild([{ path: '', component: CantinaComponent }])
]
})
export class CantinaModule {}
- Rotta lazy:
/dashboard/cantina(protetta daAuthGuard+CompanyGuarda livello Dashboard). CantinaComponentè la shell della feature (nessun figlio: vista unica).
🧩 Modello dati (semplificato)
// CantinaComponent (estratti tipizzati)
type CantinaResponseType = {
_id: string,
items: ((Omit<ISmartBag, 'type'> | IBottle) & { type: 'smartbag' | 'bottle' })[],
vintage?: (IWineVintage & { wine: IWine & { winery: ICompany } }),
userVintage?: IUserVintage
};
type GroupedModelsType = IWineContainerModel & {
subItems: CantinaResponseType['items'],
itemType: 'openedSmartbag' | 'groupedItems'
};
type CantinaVintageItemType =
| (Omit<ISmartBag, 'type'> & { type: 'smartbag' | 'bottle', itemType: 'openedSmartbag' })
| (GroupedModelsType & { itemType: 'groupedItems' });
CantinaResponseTypeè la risposta per ogni annata visualizzata.items: elenco di Smartbag e/o Bottiglie legati a quell’annata.IWineContainerModeldescrive il modello contenitore (es.type: 'smart_bag'o'bottle', capacità, ecc.).
🧠 Stato locale & servizi
Osservabili principali
locationOptions$ : (AlbiOption & { parentalControl: ICompanyAddress['vintagesParentalControl'] })[]
Opzioni location (indirizzi) della company + elenco annate soggette a parental control in quella location.locationControl : FormControl<AlbiOption & { parentalControl: ... }>
Se l’utente ha una location vincolata nel proprio access, il controllo è pre-selezionato e disabilitato.cantinaItems$→ elenco delle annate trasformate per la tabella (conitemsraggruppati emodelQuantity).filteredCantinaItems$→ come sopra, filtrate dal testo di ricerca.
Servizi
CompanyLocationService.getCompanyLocations(companyId, role)→ carica le location disponibili per l’utente corrente.BackendService→ tutte le API della sezione (inventario, counters, toggle parental, ecc.).TranslateService→ colonne/tab ed etichette localizzate.
🔄 Flusso dati end-to-end
1) Inizializzazione & scelta location
- OnInit:
- Carica location via
CompanyLocationServicee popolalocationOptions$. - Se l’utente ha una location sull’access → seleziona quella e disabilita il dropdown; altrimenti seleziona la prima.
- Carica location via
- Subscrive a
locationControl.valueChanges→ ad ogni cambio:- esegue
getCantinaVintages(selectedLocation).
- esegue
// valore emesso dal dropdown
locationControl.valueChanges
.pipe(tap(loc => this.getCantinaVintages(loc)))
.subscribe();
2) Caricamento inventario per location
// getCantinaVintages (estratto)
forkJoin({
cantinaVintages: this._backendService.get<{page:number, vintages: CantinaResponseType[]}>(
`cantina/cantina_vintages?expand=all`,
{ useCompanyHeader: true, specificLocationId: selectedLocation.key }
),
wineContainerModels: this._backendService.get<{ models: IWineContainerModel[] }>(
`wine/wine_container_models`
)
}).pipe(
tap(({ cantinaVintages, wineContainerModels }) => {
// mappa dei modelli per raggruppare items e calcolare quantità per tipo
const modelsMap = new Map(wineContainerModels.models.map(m => [m._id, m]));
// trasforma ogni annata in righe tabellari + quantità aggregate per modello
// → vedi "Raggruppamento e quantità" sotto
this.cantinaItems$.next(transformedVintages);
})
).subscribe();
Raggruppamento e quantità
- Per ogni annata:
- Costruisce
modelQuantity[]: conteggio per modello (es.smart_bagvsbottle+ capacità). - Costruisce
itemsper la griglia:openedSmartbag: Smartbag aperte conquantityLeft,openedAt→ mostrate come righe figlie con contatore bicchieri e giorni dalla apertura.groupedItems: gruppi per modello (ad es. “bottle 750ml”, “smart_bag”), consubItemscorrispondenti.
- Costruisce
3) Ricerca client-side
combineLatest([
cantinaItems$,
searchControl.valueChanges.pipe(startWith(searchControl.value))
]).pipe(
tap(([rows, q]) => filteredCantinaItems$.next(!q ? rows : rows.filter(v => {
// match su fullName dell’annata (o campi userVintage)
})))
).subscribe();
🖥️ UI e componenti
Tabella principale (albi-table)
Colonne (configurate all’avvio):
wineName→ immagine etichetta +WineName VintageYear(ouserVintagese presente).wineryName→ nome della cantina (ouserVintage.winery).quantities→ badge per ciascun modello (es.3 Smartbag,5 x 750ml).parentalControl→ icona lock/unlock per annata corrente rispetto alla location selezionata.
La tabella usa righe gerarchiche: i gruppi (
groupedItems) si espandono in righe figlie; le Smartbag aperte (openedSmartbag) mostrano contatore e dati di scadenza.
Template figlio — Smartbag aperte
<glass-counter>(già presente nello shared) visualizzaquantityLeftcon colore vino.expiryDaysLeftPipe+resolveDateStringPipe→ testo “aperto da X giorni / aperto il …”.- Azioni: Delete (icona) per rimuovere quella Smartbag.
Template figlio — Gruppi per modello
- Per Smartbag chiuse / Bottiglie:
- azioni di reset contatori (vedi “Azioni & API”).
- badge quantità per modello.
Header
- Titolo/descrizione localizzati.
- Dropdown location (
albi-dropdown). - Search (input con debounce; vedi
searchControl).
🛠️ Azioni & API
Parental Control (per annata e location)
-
Blocca (lock) annata:
POST users/me/add_parental_control_on_vintage
body: { wine: <vintageId> }
headers: { useCompanyHeader: true, specificLocationId: locationControl.value.key }Aggiorna
locationOptions$→ aggiunge<vintageId>nellaparentalControldella location corrente. -
Sblocca (unlock) annata:
POST users/me/remove_parental_control_on_vintage/<vintageId>
headers: { useCompanyHeader: true, specificLocationId: locationControl.value.key }Aggiorna
locationOptions$→ rimuove<vintageId>dallaparentalControlcorrente.
Eliminazione / Reset elementi inventario
Metodo unico deleteVintageItem(item: CantinaVintageItemType):
-
openedSmartbag(rimuove la singola smartbag aperta)DELETE cantina/smart_bags/<smartbagId>
headers: { useCompanyHeader: true, specificLocationId: locationControl.value.key } -
groupedItems
Resetta i contatori del primo elemento del gruppo (semantica: azione rappresentativa del gruppo):// Smartbag chiuse
POST cantina/smart_bags/<id>/counters body: { count: 0 }
// Bottiglie
POST cantina/bottles/<id>/counters body: { count: 0 }
Dopo ogni azione → refresh inventario chiamando getCantinaVintages(...).
🔎 Sequenze chiave (mermaid)
Caricamento e trasformazione inventario
sequenceDiagram
autonumber
participant UI as CantinaComponent
participant Svc as BackendService
participant Loc as CompanyLocationService
UI->>Loc: getCompanyLocations(companyId, role)
Loc-->>UI: locations (+ parentalControl per location)
UI->>UI: set locationControl (disable se vincolata)
UI->>Svc: GET cantina/cantina_vintages?expand=all (specificLocationId)
UI->>Svc: GET wine/wine_container_models
Svc-->>UI: vintages[], models[]
UI->>UI: group per modello + compute quantities
UI->>UI: build rows (openedSmartbag + groupedItems)
UI-->>UI: cantinaItems$ → filteredCantinaItems$
Parental control & delete/reset
flowchart TD
A[Click lock/unlock] --> B{Action}
B -- add --> C[POST add_parental_control_on_vintage]
B -- remove --> D[POST remove_parental_control_on_vintage/:id]
C --> E[Update locationOptions$.parentalControl]
D --> E[Update locationOptions$.parentalControl]
F[Delete item] --> G{itemType}
G -- openedSmartbag --> H[DELETE smart_bags/:id]
G -- groupedItems (smartbag) --> I[POST smart_bags/:id/counters {0}]
G -- groupedItems (bottle) --> J[POST bottles/:id/counters {0}]
H --> K[Refresh getCantinaVintages]
I --> K
J --> K
🗂️ Colonne tabella (config)
this.cantinaTableColumns = [
{ name: 'wineName', label: t('cantina.homePage.columnName.wineName') },
{ name: 'wineryName', label: t('cantina.homePage.columnName.winery') },
{ name: 'quantities', label: t('cantina.homePage.columnName.quantity') },
{ name: 'parentalControl', label: '' } // icone lock/unlock
];
✅ Note operative per chi subentra
- Location first: ogni chiamata dipende dalla location selezionata (
specificLocationIdin header). - Trasformazione dati: non usare direttamente la risposta grezza — raggruppare per
IWineContainerModele calcolaremodelQuantity. - Parental control è per location: aggiornare lo stato locale (
locationOptions$) oltre all’API. - Smartbag aperte vs gruppi:
openedSmartbag→ righe figlie con contatore, DELETE singolo ID.groupedItems→ reset counters per il gruppo (POST/counters { count: 0 }).
- Ricerca è client-side su
fullNameannata (o campiuserVintage).
flowchart LR
subgraph Context
UA[Selected User Access] --> L[Location options]
L --> LC[locationControl]
end
LC --> INV[getCantinaVintages]
INV --> Rows[Rows + Quantities]
Rows --> Table[albi-table]
Table --> Actions[lock/unlock/delete]
Actions --> Refresh[getCantinaVintages]