Skip to main content

🍇 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 da AuthGuard + CompanyGuard a 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.
  • IWineContainerModel descrive 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 (con items raggruppati e modelQuantity).
  • 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

  1. OnInit:
    • Carica location via CompanyLocationService e popola locationOptions$.
    • Se l’utente ha una location sull’access → seleziona quella e disabilita il dropdown; altrimenti seleziona la prima.
  2. Subscrive a locationControl.valueChanges → ad ogni cambio:
    • esegue getCantinaVintages(selectedLocation).
// 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_bag vs bottle + capacità).
    • Costruisce items per la griglia:
      • openedSmartbag: Smartbag aperte con quantityLeft, openedAt → mostrate come righe figlie con contatore bicchieri e giorni dalla apertura.
      • groupedItems: gruppi per modello (ad es. “bottle 750ml”, “smart_bag”), con subItems corrispondenti.

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 (o userVintage se presente).
  • wineryName → nome della cantina (o userVintage.winery).
  • quantitiesbadge per ciascun modello (es. 3 Smartbag, 5 x 750ml).
  • parentalControlicona 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) visualizza quantityLeft con 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.
  • 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> nella parentalControl della 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> dalla parentalControl corrente.

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 (specificLocationId in header).
  • Trasformazione dati: non usare direttamente la risposta grezza — raggruppare per IWineContainerModel e calcolare modelQuantity.
  • Parental control è per location: aggiornare lo stato locale (locationOptions$) oltre all’API.
  • Smartbag aperte vs gruppi:
    • openedSmartbag → righe figlie con contatore, DELETE singolo ID.
    • groupedItemsreset counters per il gruppo (POST /counters { count: 0 }).
  • Ricerca è client-side su fullName annata (o campi userVintage).
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]