# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What This Is
PWA per gestione piani promozionali alimentari (utente: Valentina). Importa file Excel di piani
promo da 3 aziende, li normalizza in "eventi" e li mostra su calendario / lista / analytics.
Radice app: `promogest/` — servita da Apache su `http://localhost/valentina/promogest/`.

## Tech Stack — NO build tools
HTML5 + Bootstrap 5.3 (CDN) + Vanilla JS ES6 globals + SheetJS (locale, `lib/`) + Chart.js (CDN).
Backend PHP 8.1+ senza framework, storage in file JSON flat sotto `data/`.
**Nessun npm, Node, Composer, database, bundler, test runner.** I file editati sono i file eseguiti.

### Comandi / verifica
Non esiste build né suite di test. Per validare una modifica:
```bash
php -l promogest/api/<file>.php                 # syntax check PHP
node --check promogest/js/<file>.js             # syntax check JS (solo parsing)
python3 -m json.tool promogest/data/<f>.json    # valida un JSON dopo modifiche a mano
curl -s http://localhost/valentina/promogest/api/events.php   # deve dare 401 senza sessione
```
Il test reale è aprire l'app nel browser (hard refresh) e guardare la console.

## Architettura

### Frontend: moduli come oggetti globali, ordine di caricamento vincolante
Nessun `import`/`export`. Ogni file `js/*.js` dichiara **un solo `const` globale** (`API`, `Auth`,
`Import`, `Calendar`, `List`, `Analytics`, `Settings`, `App`) e si affida al fatto che gli altri
esistano già a runtime. `index.html` li carica in quest'ordine, con **`app.js` per ultimo** perché
è lui a fare `App.init()` su `DOMContentLoaded`. Aggiungendo un modulo, inserire il `<script>`
prima di `app.js` (e in `sw.js` → `STATIC_ASSETS`).

`App` è l'unico stato: `App.events / companies / aliases / ignoredInsegne`, caricati in un solo
`Promise.all` da `App.loadData()`. Tutti i render leggono da lì, non rifanno fetch. Dopo una
mutazione che tocca il server, ricaricare la fetta interessata di `App.*` prima di ri-renderizzare.
`App.showPage(name)` mostra `#page-<name>` e chiama `<Modulo>.render()` — è il solo punto di routing.

### Il `mapping` per azienda è il cuore del sistema
Ogni azienda in `data/companies.json` ha un `mapping` con **indici di colonna Excel 0-based**:
```json
"mapping": { "sellout_start": 9, "sellout_end": 10, "insegna": 3, "product": 6,
             "extra_info": 13, "extra_info_label": "Meccanica", "header_row": 0 }
```
I 3 fornitori mandano fogli con layout diversi: il codice di parsing è unico, cambiano solo questi
indici. `extra_info_label` è l'etichetta UI di quella colonna (varia per azienda: "Meccanica",
"Tema", "Volantino"). Gli indici sono editabili da Settings (`Settings._saveMappingField`) —
un import che scarta tutte le righe è quasi sempre un mapping sbagliato, non un bug del parser.

### Pipeline di import (client-side, `js/import.js` + `App._processFile`)
1. `readSheetNames()` — se il workbook ha >1 foglio, `App._showSheetPicker()` fa scegliere quale.
2. `parseFile(file, company, sheetIndex)` — legge con `cellDates:true`, salta `header_row + 1`
   righe, e per ogni riga: `parseDate` su start/end (accetta Date, seriale Excel, `YYYY-MM-DD`,
   `DD/MM/YYYY`), risolve l'insegna via alias, scarta le righe incomplete con motivo
   (`discarded[]`, numerate `i+2` = numero riga Excel) e filtra le insegne ignorate (`ignoredRows[]`).
3. `groupRows()` — raggruppa per chiave `insegna|sellout_start|sellout_end` → un evento con N prodotti.
4. `mergeConflicts(groups, maxDays=2)` — stessa insegna, **una** data uguale e l'altra diversa di
   ≤2 giorni ⇒ fonde i due gruppi. Vince chi ha più prodotti (a parità, il range più largo).
   Serve perché gli Excel di origine hanno date sfalsate per lo stesso volantino.
5. `findUnmapped()` — insegne mai viste negli alias, mostrate come warning per invitare a mapparle.
6. `API.importEvents()` → `events.php?action=import`: **cancella tutti gli eventi di quella azienda
   e reinserisce**. L'import è una sostituzione totale per azienda, mai un merge incrementale.
   Le altre aziende non vengono toccate. Il server ri-valida date/insegna/products e assegna
   `id`, `company_id`, `imported_at`.

Il modale di import ha 3 step; la visibilità è pilotata da `data-step="N"` su **6** elementi
`.import-step` (3 nel body + 3 nel footer) — `App._showImportStep(n)` li toggla insieme.
Le regole CSS di `.import-step` in `app.css` usano `!important` per battere il `d-flex` di Bootstrap:
non rimuoverlo o i pulsanti del footer restano tutti visibili.

### Backend: 6 endpoint, tutti stesso scheletro
Ogni `api/*.php` fa `require utils.php` → `requireAuth()` → switch su `$_SERVER['REQUEST_METHOD']`
e `$_GET['action']` → `jsonResponse()`. Non c'è router.

Contratto uniforme: `{"success":true,"data":…}` oppure HTTP 400 + `{"success":false,"error":"…"}`.
`API._fetch` in `js/api.js` scarta l'envelope e fa `throw new Error(error)` — nel frontend si
lavora sempre con `try/catch` + `App.toast(msg,'danger')`, mai con status code. Il 401 è speciale:
`_fetch` chiama `App.showLogin()` da solo prima di lanciare.

Auth: sessione PHP + cookie `remember_token` HttpOnly, confrontato con `hash_equals` contro
lo SHA-256 salvato in `users.json`. Password con `password_hash` bcrypt.

`data/.htaccess` nega l'accesso diretto ai `*.json`; `promogest/.htaccess` fa fallback SPA su
`index.html`. Il DB è il filesystem: ogni scrittura passa da `writeJson()` (encode → tmp file con
suffisso random → `rename()` atomico).

## Regole critiche
1. **Mai `new Date(dateString)`** su `YYYY-MM-DD` — sempre `new Date(s + 'T00:00:00')`, altrimenti
   il parsing UTC sposta la data di un giorno. Vale ovunque: calendar, list, analytics, import.
2. **`#app` parte `display:none`** e `showPage()` non lo mostra — dopo il check auth serve una
   chiamata esplicita a `App.hideLogin()`. Ometterla = pagina bianca dopo hard refresh.
3. **Scritture JSON solo via `writeJson()`** — mai `file_put_contents` diretto sui file di `data/`.
4. **XSS**: `escapeHtml()` (app.js) o `_esc()` (calendar.js) su ogni stringa proveniente dagli
   Excel o dall'utente prima di metterla in `innerHTML`.
5. **`aliases.json` ha due chiavi**, `"aliases"` e `"ignored_insegne"` — `readJson`/`writeJson`
   lavorano sull'intero documento: leggere, modificare la chiave, riscrivere tutto.
6. **`companies.json` custodisce i `mapping`** e `last_import`: non riscriverlo da zero.

## Service Worker
Registrazione **disattivata** in `index.html`: al suo posto c'è uno script che fa `unregister()`
di tutte le registrazioni e svuota le cache (evita di servire versioni stale durante lo sviluppo).
`sw.js` resta in repo funzionante (`CACHE_NAME: promogest-v2`, Network First per gli asset locali,
Cache First per i CDN). Per riattivarlo: rimettere `navigator.serviceWorker.register('./sw.js')`,
bumpare `CACHE_NAME` e allineare `STATIC_ASSETS`.

## Endpoint
| File | Metodi |
|---|---|
| `api/auth.php` | POST `?action=login` / `logout` / `change_password`, GET `?action=check` |
| `api/events.php` | GET lista, POST `?action=import` (sostituisce tutti gli eventi dell'azienda), DELETE `?action=clear` |
| `api/companies.php` | GET / POST / PUT |
| `api/aliases.php` | GET/POST/PUT/DELETE alias, + `?action=ignored` GET/POST |
| `api/export.php` | GET → download **JSON** di backup completo (non Excel) |
| `api/import-backup.php` | POST ripristino backup, con validazione strutturale |

## Dati
- Utente default: `valentina` / `valentina`
- Aziende: `caseifici_gt` (blu #2196F3), `salumifici_gt` (verde #4CAF50), `parmacotto` (arancio #FF9800)
- Scala attuale: ~1.500 eventi, `events.json` ~1,3 MB. Nessun problema; il pretty-print costa ~47%.
  Serve a Valentina solo il mese corrente e i futuri: se cresce, scartare in import le righe con
  `sellout_end < today` prima di archiviare o filtrare lato server.

## Deploy
Dopo ogni modifica ai file dell'app va fatto il deploy FTP su `ftp.duebytes.it` → `htdocs/app/vale/`
(credenziali nella memoria di progetto). Locale e produzione sono lo stesso albero di file.
