# PromoGest — Gestionale Promozioni & Volantini

## Panoramica Progetto

**PromoGest** è una Progressive Web App (PWA) per gestire i piani promozionali di aziende alimentari. L'utente principale (Valentina) importa file Excel da 3 aziende fornitrici, ciascuna con formato diverso. L'app normalizza i dati, unifica i nomi dei punti vendita (insegne) tramite alias configurabili, e presenta tutto in un calendario interattivo.

**Live demo target**: hosting Apache + PHP 8.1+ su server web. L'app è mobile-first ma responsiva per desktop.

---

## Stack Tecnologico

| Layer | Tecnologia |
|-------|-----------|
| Frontend | HTML5 + Bootstrap 5.3 + Vanilla JS (ES6+) |
| Parsing Excel | SheetJS (xlsx.full.min.js) — lato client |
| Backend API | PHP 8.1+ puro (no framework, no Composer) |
| Storage | File JSON su filesystem server |
| PWA | Service Worker + manifest.json + cache JSON offline |
| Icone | Generare favicon + icone PWA (192x192, 512x512) |

---

## Struttura File e Directory

```
promogest/
├── index.html                  # Entry point (login + SPA)
├── manifest.json               # PWA manifest
├── sw.js                       # Service Worker
├── favicon.ico
├── icons/
│   ├── icon-192.png
│   └── icon-512.png
├── css/
│   └── app.css                 # Stili custom (Bootstrap via CDN)
├── js/
│   ├── app.js                  # Main app logic, routing, init
│   ├── auth.js                 # Login, logout, cookie management
│   ├── calendar.js             # Vista calendario (mensile)
│   ├── list.js                 # Vista lista/promemoria
│   ├── import.js               # Import Excel + mapping colonne
│   ├── settings.js             # Impostazioni (alias, aziende, password)
│   ├── analytics.js            # Pagina statistiche
│   └── api.js                  # Wrapper chiamate PHP API
├── lib/
│   └── xlsx.full.min.js        # SheetJS (download da CDN o locale)
├── api/
│   ├── index.php               # Router API
│   ├── auth.php                # Autenticazione
│   ├── events.php              # CRUD eventi
│   ├── companies.php           # CRUD aziende
│   ├── aliases.php             # CRUD alias insegne
│   └── import.php              # Ricezione dati importati da client
└── data/
    ├── users.json
    ├── companies.json
    ├── aliases.json
    └── events.json
```

---

## File JSON — Struttura Dati

### users.json

```json
{
  "users": [
    {
      "id": "u1",
      "username": "valentina",
      "password_hash": "<bcrypt hash di 'valentina'>",
      "created_at": "2026-03-01T00:00:00Z"
    }
  ]
}
```

- Password di default: `valentina` (hash bcrypt)
- Possibilità di cambio password nelle impostazioni

### companies.json

Le 3 aziende preconfigurate con il mapping delle colonne Excel. Il mapping usa **indici di colonna 0-based** (come li legge SheetJS).

```json
{
  "companies": [
    {
      "id": "caseifici_gt",
      "name": "Caseifici GT",
      "color": "#2196F3",
      "mapping": {
        "sellout_start": 9,
        "sellout_end": 10,
        "insegna": 3,
        "product": 6,
        "extra_info": 13,
        "extra_info_label": "Meccanica",
        "header_row": 0
      }
    },
    {
      "id": "salumifici_gt",
      "name": "Salumifici GT",
      "color": "#4CAF50",
      "mapping": {
        "sellout_start": 12,
        "sellout_end": 13,
        "insegna": 7,
        "product": 15,
        "extra_info": 8,
        "extra_info_label": "Tema",
        "header_row": 0
      }
    },
    {
      "id": "parmacotto",
      "name": "Parmacotto",
      "color": "#FF9800",
      "mapping": {
        "sellout_start": 7,
        "sellout_end": 8,
        "insegna": 1,
        "product": 4,
        "extra_info": 11,
        "extra_info_label": "Volantino",
        "header_row": 0
      }
    }
  ]
}
```

**Note sui mapping:**
- `header_row`: indice della riga intestazione (0 = prima riga). Le righe dati partono dalla successiva.
- `extra_info`: colonna con informazione accessoria. Per Caseifici GT è la "Meccanica" (valori: A, B, C, S). Per Salumifici GT è il "Tema" (Volantini, TaglioPrezzo, Collection, ecc.). Per Parmacotto è "Volantino" (X se presente, vuoto altrimenti).
- L'utente può modificare i mapping dall'interfaccia impostazioni se la struttura Excel cambia.

### aliases.json

Sistema per unificare i nomi delle insegne tra aziende diverse. Ogni alias ha un `canonical_name` (nome unificato) e una lista di `variants` (come appare nei vari file Excel).

```json
{
  "aliases": [
    {
      "id": "a1",
      "canonical_name": "BENNET",
      "variants": ["BENNET SPA", "BENNET BRAND", "BENNET"]
    },
    {
      "id": "a2",
      "canonical_name": "ESSELUNGA",
      "variants": ["ESSELUNGA SPA", "ESSELUNGA"]
    },
    {
      "id": "a3",
      "canonical_name": "DIMAR",
      "variants": ["DIMAR SPA", "DIMAR"]
    }
  ]
}
```

- Il confronto tra variante e nome nel file Excel è **case-insensitive** e **trim**.
- Se un nome nel file non corrisponde a nessuna variante, viene usato così com'è (originale).
- Nella pagina impostazioni l'utente può aggiungere/modificare alias e varianti.
- Suggerimento UX: dopo un import, mostrare le insegne non ancora mappate ad un alias così l'utente può decidere se aggiungerle.

### events.json

Contiene gli eventi **già raggruppati** (un evento = stessa insegna + stesse date inizio/fine da una specifica azienda).

```json
{
  "events": [
    {
      "id": "evt_001",
      "company_id": "caseifici_gt",
      "insegna_raw": "ESSELUNGA SPA",
      "insegna": "ESSELUNGA",
      "sellout_start": "2026-03-13",
      "sellout_end": "2026-03-22",
      "products": [
        {
          "name": "PARMAREGGIO SNACK PR.RE. 20gx5 x10",
          "extra_info": "A"
        },
        {
          "name": "BURRO PARMAREGGIO 400g x12",
          "extra_info": "A"
        }
      ],
      "imported_at": "2026-03-01T10:30:00Z"
    }
  ]
}
```

**Campi chiave:**
- `insegna_raw`: nome originale dal file Excel (per debug/tracciabilità)
- `insegna`: nome canonico dopo applicazione alias (usato per raggruppamento cross-azienda nel calendario)
- `company_id`: identifica l'azienda di provenienza
- `products`: array di prodotti raggruppati sotto lo stesso evento
- `imported_at`: timestamp dell'ultimo import

---

## Logica di Import Excel

### Flusso

1. L'utente seleziona l'azienda di destinazione (dropdown con le 3 aziende)
2. L'utente carica il file Excel (.xlsx)
3. SheetJS (lato client) legge il file e restituisce un array di righe
4. Il JS applica il mapping colonne dell'azienda selezionata per estrarre: `sellout_start`, `sellout_end`, `insegna`, `product`, `extra_info`
5. Righe con date mancanti o invalide vengono scartate (mostrare contatore)
6. Applicazione alias: ogni `insegna` viene confrontata (case-insensitive, trimmed) con le varianti in aliases.json. Se trova match → usa `canonical_name`, altrimenti usa il valore originale
7. **Raggruppamento**: le righe vengono raggruppate per chiave `(insegna_normalizzata, sellout_start, sellout_end)`. I prodotti vengono aggregati in array
8. **Sovrascrittura**: TUTTI gli eventi precedenti della stessa `company_id` vengono eliminati prima dell'inserimento
9. I nuovi eventi vengono inviati al backend PHP che li salva in events.json
10. Mostrare riepilogo: X eventi creati, Y prodotti totali, Z insegne non mappate

### Validazione Date

Le date nei file Excel possono essere:
- Oggetti Date JS (SheetJS con `cellDates: true`)
- Numeri seriali Excel
- Stringhe (vari formati)

Usare SheetJS con opzione `cellDates: true` e poi convertire in formato `YYYY-MM-DD`.

### Flusso Import a Step (con Modal di Anteprima)

L'import è un processo guidato a **3 step**, gestito tramite un modal Bootstrap a schermo intero su mobile:

#### Step 1 — Selezione
- Dropdown selezione azienda
- Upload file Excel (drag & drop + bottone classico)
- Bottone "Avanti" → parte il parsing SheetJS lato client con spinner di attesa

#### Step 2 — Anteprima (OBBLIGATORIA prima dell'import)
Modal che mostra:
- **Riepilogo numerico in alto**: X righe totali trovate, Y righe valide, Z righe scartate
- **Tabella anteprima**: le prime 20 righe parsate in formato tabella con colonne: Insegna | Prodotto | Data Inizio | Data Fine | Extra Info. Le colonne devono riflettere il mapping applicato, così l'utente verifica che i dati siano stati letti correttamente
- **Righe scartate** (collassabile): lista delle righe ignorate con motivo (data mancante, data invalida, insegna vuota)
- **Insegne nuove** (non ancora mappate ad alias): lista evidenziata in giallo. L'utente ne prende nota e potrà configurare gli alias dopo l'import
- **Contatore eventi raggruppati**: "Queste X righe verranno raggruppate in Y eventi" (dopo il group by insegna+date)
- **Avviso sovrascrittura**: banner warning "Attenzione: l'import sostituirà tutti i XXX eventi attualmente presenti per [Nome Azienda]"
- Bottoni: "← Indietro" (torna a step 1) | "Conferma Import →" (procede)

#### Step 3 — Risultato
- Spinner durante l'invio al server
- Riepilogo finale: "Import completato! X eventi creati, Y prodotti totali, Z insegne"
- Bottone "Vai al Calendario" o "Chiudi"

---

## Interfaccia Utente — Pagine e Componenti

### Navigazione

Bottom navbar (mobile-first) con 4 tab:
1. 📅 **Calendario** (home/default)
2. 📋 **Lista**
3. 📊 **Analytics**
4. ⚙️ **Impostazioni**

Top bar con: logo/nome app + indicatore utente + logout

### 1. Pagina Calendario

**Vista mensile** con griglia giorni. Ogni giorno mostra:
- Pallini/badge colorati per ogni evento attivo in quel giorno (colore = azienda)
- Numero eventi se sono troppi per essere visualizzati

**Navigazione**: frecce ← → per cambiare mese, bottone "Oggi" per tornare al mese corrente.

**Click su un giorno**: apre un pannello/modal che mostra tutti gli eventi attivi in quella data, raggruppati per insegna. Per ogni insegna:
- Nome canonico insegna
- Badge colorato per ogni azienda che ha prodotti in promo
- Lista prodotti con info extra

**Click su un evento/span nel calendario**: apre modal dettaglio con:
- Nome insegna
- Periodo sellout (dal - al)
- Azienda di provenienza (con badge colorato)
- Elenco prodotti in offerta con extra info
- Eventuali prodotti di altre aziende per la stessa insegna e periodo (cross-reference)

### 2. Pagina Lista

Elenco cronologico degli eventi (ordinato per data inizio sellout). Filtri:
- Per azienda (checkbox multiple)
- Per insegna (search/autocomplete)
- Per range date (date picker from-to)
- Per testo prodotto (ricerca libera)

Ogni card evento mostra:
- Insegna (grande)
- Periodo sellout
- Badge azienda
- Numero prodotti (espandibile per vedere la lista)

### 3. Pagina Analytics

Dashboard con:
- **Contatori**: totale eventi, totale prodotti, totale insegne attive
- **Grafico a barre**: numero eventi per mese (raggruppato per azienda, colori diversi)
- **Top 10 insegne**: per numero di promozioni
- **Prossimi eventi**: lista dei prossimi 5 eventi in arrivo
- **Distribuzione extra_info**: per Caseifici (A/B/C), per Salumifici (Volantini/TaglioPrezzo), per Parmacotto (con/senza Volantino)

Per i grafici usare **Chart.js** via CDN.

### 4. Pagina Impostazioni

Tabs o accordion con sezioni:

#### 4a. Profilo Utente
- Cambio password (vecchia password + nuova + conferma)

#### 4b. Aziende
- Lista delle 3 aziende con colore e mapping
- Click su azienda → mostra/modifica mapping colonne (con helper: "Seleziona colonna per data inizio", ecc.)
- Possibilità di aggiungere nuova azienda
- Per ogni azienda: pulsante "Elimina tutti gli eventi" (con conferma)

#### 4c. Alias Insegne
- Lista alias con canonical_name e varianti
- Aggiunta nuovo alias: campo canonical_name + campo varianti (textarea, una per riga)
- Modifica/eliminazione alias esistenti
- **Sezione "Insegne non mappate"**: mostra tutte le insegne presenti negli eventi che non hanno un alias. L'utente può selezionarle e assegnarle a un alias esistente o crearne uno nuovo.

#### 4d. Import Excel
- Flusso a 3 step tramite modal (vedi sezione "Flusso Import a Step" sopra)
- Storico import (ultima data import per ogni azienda, visibile in questa sezione)

#### 4e. Gestione Dati
- Export completo JSON (backup)
- Import JSON (restore)
- Reset dati (con tripla conferma)

### 5. Login Page

- Form semplice: username + password
- Checkbox "Ricordami" (cookie persistente 30 giorni)
- Design centrato, logo app in evidenza
- Se il cookie è presente e valido, auto-redirect alla home

---

## Backend PHP — API REST

### Endpoint

Tutte le API sotto `/api/`. Formato JSON in request/response. Autenticazione via session PHP + cookie.

```
POST   /api/auth.php?action=login        { username, password, remember }
POST   /api/auth.php?action=logout
POST   /api/auth.php?action=change_password  { old_password, new_password }
GET    /api/auth.php?action=check         → { authenticated: true/false, user }

GET    /api/events.php                    → { events: [...] }
POST   /api/events.php?action=import      { company_id, events: [...] }
DELETE /api/events.php?action=clear        { company_id }

GET    /api/companies.php                 → { companies: [...] }
PUT    /api/companies.php                 { company } (update mapping/color)
POST   /api/companies.php                 { company } (add new)

GET    /api/aliases.php                   → { aliases: [...] }
POST   /api/aliases.php                   { alias } (add new)
PUT    /api/aliases.php                   { alias } (update)
DELETE /api/aliases.php?id=xxx            (delete)

GET    /api/export.php                    → full JSON backup
POST   /api/import-backup.php             { data } (restore from backup)
```

### Sicurezza PHP

- Password hash con `password_hash()` / `password_verify()` (bcrypt)
- Session PHP con `session_start()`, `session_regenerate_id()` al login
- Cookie "remember me": token random salvato in users.json, cookie `HttpOnly`, `Secure`, `SameSite=Strict`, durata 30 giorni
- CORS headers per API se necessario (ma probabilmente same-origin)
- Validazione input su ogni endpoint
- File JSON nella directory `data/` con `.htaccess` che nega accesso diretto:
  ```apache
  <Files "*.json">
    Require all denied
  </Files>
  ```
- File lock (`flock`) per scritture concorrenti sui JSON

---

## PWA — Progressive Web App

### manifest.json

```json
{
  "name": "PromoGest",
  "short_name": "PromoGest",
  "description": "Gestionale Promozioni & Volantini",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#1976D2",
  "orientation": "portrait-primary",
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ]
}
```

### Service Worker (sw.js)

Strategia: **Network First con fallback a cache** per le API, **Cache First** per gli asset statici.

- Cache degli asset statici (HTML, CSS, JS, Bootstrap, SheetJS, Chart.js)
- Cache dell'ultimo JSON eventi ricevuto → consultabile offline
- Offline: l'app funziona in sola lettura (calendario + lista + analytics) con i dati in cache
- Online: fetch fresh data e aggiorna cache
- Import Excel: solo online (richiede salvataggio server)
- Mostrare indicatore online/offline nella top bar

---

## Dati Preconfigurati dalle Analisi dei File Excel

### Statistiche attuali dei file

| Azienda | Righe | Insegne uniche | Range date |
|---------|-------|---------------|------------|
| Caseifici GT | 1.203 | 100 | Feb 2026 — Nov 2026 |
| Salumifici GT | 4.319 | 104 | Feb 2026 — Gen 2027 |
| Parmacotto | 239 | 33 | Gen 2026 — Gen 2027 |

### Mapping colonne dettagliato

#### Caseifici GT (`Caseifici_GT_Piani_Promo_24_02.xlsx`)

| Campo | Colonna Excel | Indice 0-based | Nome colonna nel file |
|-------|:------------:|:--------------:|----------------------|
| Insegna | D | 3 | Cliente L6 - Descrizione Cliente Fatturazione |
| Prodotto | G | 6 | Descrizione Articolo |
| Sellout inizio | J | 9 | Sellout da |
| Sellout fine | K | 10 | a (colonna dopo "Sellout da") |
| Extra info | N | 13 | Meccanica (valori: A, B, C, S) |

#### Salumifici GT (`Salumifici_GT_Piani_Promo_agg_24_02.xlsx`)

| Campo | Colonna Excel | Indice 0-based | Nome colonna nel file |
|-------|:------------:|:--------------:|----------------------|
| Insegna | H | 7 | Des_Target |
| Prodotto | P | 15 | DESCR_ART |
| Sellout inizio | M | 12 | sell out inizio |
| Sellout fine | N | 13 | sell out fine |
| Extra info | I | 8 | Tema (valori: Volantini, TaglioPrezzo, Collection, Occasione Acquisto) |

#### Parmacotto (`Parmacotto_Tabellone_Promozionale_dm_italia.xlsx`)

| Campo | Colonna Excel | Indice 0-based | Nome colonna nel file |
|-------|:------------:|:--------------:|----------------------|
| Insegna | B | 1 | CLIENTE |
| Prodotto | E | 4 | PRODOTTO |
| Sellout inizio | H | 7 | SELL - OUT DAL |
| Sellout fine | I | 8 | SELL - OUT AL |
| Extra info | L | 11 | VOLANTINO (valori: "X" se volantino, vuoto altrimenti) |

### Alias insegne da preconfigurare

Basandosi sull'analisi dei 3 file, queste insegne appaiono con nomi diversi e dovrebbero essere preconfigurate come alias iniziali. Inserirle come dati di partenza in aliases.json:

```json
[
  { "canonical_name": "DIMAR", "variants": ["DIMAR SPA", "DIMAR"] },
  { "canonical_name": "ALFI", "variants": ["ALFI SRL", "ALFI"] },
  { "canonical_name": "BENNET", "variants": ["BENNET SPA", "BENNET BRAND", "BENNET"] },
  { "canonical_name": "ESSELUNGA", "variants": ["ESSELUNGA SPA", "ESSELUNGA"] },
  { "canonical_name": "MIGROSS", "variants": ["MIGROSS SPA", "MIGROSS SpA"] },
  { "canonical_name": "UNICOOP FIRENZE", "variants": ["UNICOOP FIRENZE"] },
  { "canonical_name": "L'ABBONDANZA", "variants": ["L'ABBONDANZA SRL", "L'ABBONDANZA"] },
  { "canonical_name": "TATO' PARIDE", "variants": ["TATO' PARIDE SPA", "TATO' PARIDE"] },
  { "canonical_name": "UNICOMM", "variants": ["UNICOMM SRL", "UNICOMM"] },
  { "canonical_name": "RETAILPRO", "variants": ["RETAILPRO SPA", "RETAILPRO"] },
  { "canonical_name": "ASPIAG", "variants": ["ASPIAG SERVICE S.R.L.", "ASPIAG SERVICE SRL", "ASPIAG CEDI"] },
  { "canonical_name": "CE.DI SIGMA CAMPANIA", "variants": ["CE.DI.SIGMA CAMPANIA S.p.A.", "CE.DI SIGMA CAMPANIA SPA"] },
  { "canonical_name": "CE.DI MARCHE", "variants": ["CE.DI MARCHE SOC. COOPERATIVA", "CE.DI.MARCHE SOCIETA' COOPERATIVA"] },
  { "canonical_name": "MULTICEDI", "variants": ["MULTICEDI S.R.L.", "MULTICEDI SRL"] },
  { "canonical_name": "MAIORA", "variants": ["MAIORA SPA", "MAIORA SRL"] }
]
```

L'utente Valentina configurerà poi ulteriori alias dall'interfaccia man mano che importa i file.

---

## Design e UX

### Palette colori

- **Primary**: #1976D2 (blu Material)
- **Aziende**: Caseifici GT → #2196F3 (blu), Salumifici GT → #4CAF50 (verde), Parmacotto → #FF9800 (arancione)
- **Background**: #f8f9fa (grigio chiaro)
- **Card/modal**: #ffffff con shadow leggera

### Font

- **Bootstrap default** (system font stack) — ottimo per performance e leggibilità mobile

### Icona App / Favicon

Creare un'icona con:
- Sfondo circolare blu (#1976D2)
- Icona stilizzata di un calendario con un carrello/tag promo sovrapposto
- Lettera "P" stilizzata come alternativa semplice
- Formato: PNG 512x512 per PWA, ridimensionato a 192x192 e favicon 32x32

### Mobile First

- Bottom nav bar fissa (Bootstrap)
- Card-based layout per eventi
- Touch-friendly: bottoni almeno 44px, spazi adeguati
- Swipe per navigare mesi nel calendario (opzionale, nice to have)
- Modal full-screen su mobile per dettagli evento

---

## Priorità di Sviluppo (Fasi)

### Fase 1 — Core MVP
1. Struttura progetto e file base
2. Backend PHP: API auth + CRUD JSON
3. Login page + gestione sessione/cookie
4. Pagina Import Excel (selezione azienda + upload + parsing + preview + salvataggio)
5. Pagina Calendario (vista mensile + click giorno + modal dettagli)
6. Dati preconfigurati (3 aziende + alias iniziali)

### Fase 2 — Funzionalità Complete
7. Pagina Lista con filtri
8. Sistema alias insegne completo (CRUD + suggerimenti post-import)
9. Pagina Impostazioni (cambio password, gestione aziende, mapping)
10. Pagina Analytics con grafici

### Fase 3 — PWA e Polish
11. Service Worker + manifest + icone
12. Cache offline
13. Export/import backup JSON
14. Rifinitura UX, animazioni, transizioni

---

## Note Tecniche Importanti

### SheetJS — Parsing Excel lato client

```javascript
// Lettura file
const workbook = XLSX.read(data, { type: 'array', cellDates: true });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' });
// rows è un array di array, rows[0] = header, rows[1+] = dati
```

- Usare `cellDates: true` per ottenere oggetti Date JS
- Usare `header: 1` per avere array di array (indici numerici, non nomi colonna)
- Gestire date come oggetti Date e poi formattare come `YYYY-MM-DD`

### Raggruppamento eventi (logica client-side)

```javascript
// Dopo parsing, raggruppare per: insegna_normalizzata + sellout_start + sellout_end
const key = `${normalizedInsegna}|${startDate}|${endDate}`;
const groups = {};
rows.forEach(row => {
  // ... extract fields using mapping ...
  if (!groups[key]) {
    groups[key] = { insegna, insegna_raw, sellout_start, sellout_end, products: [] };
  }
  groups[key].products.push({ name: productName, extra_info: extraInfo });
});
```

### Concorrenza file JSON

Usare `flock()` in PHP per evitare race condition:

```php
function writeJson($file, $data) {
    $fp = fopen($file, 'w');
    flock($fp, LOCK_EX);
    fwrite($fp, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
    flock($fp, LOCK_UN);
    fclose($fp);
}
```

### .htaccess per directory data/

```apache
# data/.htaccess
<Files "*.json">
    Require all denied
</Files>
```

---

## Criteri di Qualità

- Zero errori console JS
- Responsive: testare su viewport 375px (iPhone SE), 414px (iPhone), 768px (tablet), 1024px+ (desktop)
- Performance: caricamento iniziale < 3s su 3G
- Accessibilità: label su tutti gli input, contrasto colori WCAG AA
- Il calendario deve gestire senza problemi 5.000+ eventi (i 3 file attuali combinati producono circa 5.700 righe)
- Import di file Excel fino a 5.000 righe deve completarsi in < 5 secondi
