Stack e confini
| Componente | Tecnologia | Responsabilità |
|---|---|---|
| Backend | Laravel 12 / PHP 8.2 | REST API, autenticazione, pannello admin con shell premium e regole operative. |
| Database server | MySQL | Dati multi-tenant e transazioni di consegna/stock. |
| Mobile | Flutter | Applicazione agente unica esposta come Consegne Panificio su Android e iOS, bundle it.appteam.panificio. |
| Storage locale | SQLite + secure storage | Snapshot, outbox e storico DDT offline per tenant; token custodito dal dispositivo. |
| Stampa | Bluetooth LE / ESC/POS | Selezione stampante dall’app e invio documento valorizzato alle termiche BLE, incluse PT210 generiche, senza rendering PDF o immagini. |
Multitenancy SaaS
Il modello utilizza uno schema condiviso con chiave bakery_id. L’entità bakeries identifica l’ambiente; gli account piattaforma non appartengono a un panificio, mentre amministratori tenant e agenti sì.
| Dominio dati | Isolamento |
|---|---|
| Anagrafiche | customers, products, vans, agents filtrati per panificio. |
| Configurazione | customer_product_prices, agent_customer, routes, route_stops appartengono al tenant. |
| Operatività | turns, van_inventories, orders, deliveries e righe includono bakery_id. |
| Unicità | Codici operativi, numeri ordine e DDT sono unici nel relativo panificio. |
| Ciclo di vita | Il platform admin può sospendere il tenant o eliminarlo; l’eliminazione rimuove dati operativi, utenti tenant e token mobile. |
API mobile
Autenticazione agente
GET /api/v1/config espone la configurazione pubblica mobile, compreso lo stato OneSignal e l’eventuale App ID condiviso. POST /api/v1/auth/login accetta email, password e identificazione dispositivo. Risponde con token Sanctum, utente, panificio e identità push OneSignal dell’agente. Sono accettati soltanto agenti attivi associati a tenant attivi.
Snapshot mattutino
GET /api/v1/agent/sync?date=YYYY-MM-DD richiede Bearer token e restituisce il turno dell’agente, i clienti esplicitamente assegnati e il listino valido specifico per ciascuno. Le righe degli ordini pianificati includono sia la quantità ordinata sia l’eventuale reso pianificato.
{
"bakery": {"id": 1, "name": "Forno Rossi", "slug": "forno-rossi", "vat_number": "IT12345678901", "address": "Via del Pane 1", "city": "Roma", "postal_code": "00100"},
"agent": {"id": 7, "name": "Marco Alba"},
"turn": {"route": {}, "van": {}, "ddt_numbering": {"next_sequence": 501}},
"assigned_customers": [{"id": 14, "business_name": "Market Centro", "vat_number": "IT98765432109", "price_list": [{"product_id": 3, "unit_price": "1.2500"}]}],
"stops": [{"customer": {}, "price_list": [], "orders": []}],
"van_inventory": []
}
Outbox consegne
POST /api/v1/agent/deliveries/sync riceve massimo 100 bolle per richiesta. Ogni bolla usa un client_uuid idempotente e include le righe con sale_type planned oppure van_sale. Se l’ordine nasce nell’app, il server crea lo storico ordine in stato chiuso e lo collega al DDT.
La transazione server verifica appartenenza al tenant, assegnazione del cliente all’agente, ordine opzionale, intervallo DDT assegnato, listino valido e stock disponibile prima di inserire il documento e aggiornare quantità consegnate e rese. Ogni bolla è trattata separatamente: conflitti DDT, stock insufficiente, riferimenti scaduti o altre validazioni operative vengono restituiti in rejected con il relativo client_uuid, senza impedire la registrazione delle altre bolle valide del batch.
Storico DDT agente
GET /api/v1/agent/deliveries restituisce gli ultimi DDT emessi dai turni dell’agente autenticato, con cliente, righe, valori di consegnato/reso e data di eventuale stampa. La risposta è sempre filtrata per tenant e agente.
POST /api/v1/agent/deliveries/print-sync conferma in modo idempotente la prima stampa riuscita di un DDT già sincronizzato, coprendo anche la ristampa eseguita dopo un precedente errore della stampante.
Offline Flutter
- Il token restituito dal login è memorizzato in
flutter_secure_storage. - SQLite mantiene snapshot, consegne pendenti, conferme stampa pendenti e l’elenco ordini effettuati usando lo slug del panificio come scope locale.
- L’area admin web usa una shell Blade condivisa con navigazione raggruppata per domini, header contestuali di pagina, card metriche e viste operative riorganizzate per dashboard, anagrafiche, turni, ordini e agenti.
- Il premium pass 2 introduce ricerca server-side nelle anagrafiche clienti e nel listino cliente, oltre a una pagina dettaglio agente separata dalla lista per assegnazioni filtrabili e contesto operativo.
- Il premium pass 3 aggiunge breadcrumb contestuali nelle viste figlie, modifica inline della scheda agente e ordini pianificati multi-riga con righe prodotto dinamiche e validazione listino per ogni riga.
- Il premium pass 6 introduce un’area impostazioni tenant con modifica dell’intestazione del panificio attivo, anteprima operativa della testata e accesso diretto dalla navigazione admin.
- Le anagrafiche clienti, prodotti, furgoni e giri usano una gestione tabellare compatta con ricerca, modifica rapida in riga, ordinamento più naturale per identità anagrafica e indicatori di completezza operativa in stile backoffice, per evitare eccessivo scroll su dataset estesi.
- L’anagrafica clienti include ora il campo `email`, usato come destinatario dei tracciati Excel inviati dal backoffice quando un ordine ha già generato il relativo DDT.
- L’area `admin/operations/orders` espone filtri GET per periodo consegna, cliente, stato operativo e ricerca libera su ordine, DDT, cliente o codice; la tabella filtrata alimenta la selezione singola o totale degli ordini esportabili, export singolo `.xls`, export TeamSystem testuale tramite `format=teamsystem`, export massivo `.zip`, invio email singolo e invio/rinvio email massivo dei tracciati cliente. Il file Excel è generato a partire dai `delivery_items` del DDT collegato e produce sempre due blocchi logici nello stesso foglio: prima le righe consegnate e poi le righe di reso con quantità e totale negativi. Il file TeamSystem usa record fixed-width da 400 caratteri: una riga `TESTA` per il documento e righe `CORPO` per consegnati e resi. Negli ZIP massivi i nomi file vengono resi univoci quando più DDT dello stesso cliente generano la stessa intestazione.
- La configurazione OneSignal è centralizzata in `platform_settings`: il super admin salva App ID e REST API Key globali, mentre ogni tenant può solo riusare tale configurazione condivisa per l’invio ai propri agenti.
- L’area `admin/settings/push-notifications` consente invio push a un singolo agente o a un set di agenti selezionati; il backend invia verso OneSignal usando alias `external_id` derivato dall’utente agente, include sempre un fallback `en` oltre al contenuto `it` richiesto dal provider, accetta sia un URL immagine opzionale sia un upload diretto dal backoffice e inoltra il media come `big_picture` Android e `ios_attachments`.
- Il log push del panificio viene automaticamente potato alle ultime 50 righe dopo ogni nuovo invio, oltre al pulsante manuale di svuotamento completo; ogni riga puo conservare URL pubblico e path storage dell’immagine inviata, così i file uploadati vengono rimossi quando una riga esce dallo storico o quando il log viene svuotato.
- La dashboard tenant usa una veduta selezionabile (`deliveries` o `products`) con filtri GET per intervallo date, cliente, agente, mezzo e giro; il rendering separa cabina di regia, metriche principali, controlli operativi, classifica clienti top e vista dati principale, ma raccoglie i filtri in un drawer espandibile per migliorare la resa responsive su schermi piccoli. L’export Excel riproduce la sola veduta attiva con gli stessi filtri applicati e, nella veduta `deliveries`, genera una riga per ogni articolo del DDT con quantità di reso esposta sempre in negativo.
- L’app Flutter agente usa una home di giro con ricerca clienti, filtri rapidi per serviti/da servire/criticità, contatore dei clienti visibili, metriche compatte in griglia 2x2, contenuto inferiore scrollabile, priorità visive sulle tappe, badge sync più chiari, riepilogo del carico residuo del van con prodotti in esaurimento e un composer ordine con ricerca prodotti e riepilogo economico progressivo.
- L’app Flutter inizializza OneSignal leggendo l’App ID dal backend, effettua `login` OneSignal con un `external_id` stabile dell’agente e lo rimuove al logout, così i push restano agganciati all’identità utente e non a un singolo token memorizzato lato server.
- Lo storico notifiche agente è locale al dispositivo: il client intercetta le notifiche OneSignal in foreground o al click, le salva in SQLite con stato letto/non letto, immagine e payload essenziale, mostra il contatore in alto a destra e consente di rivederle anche dopo la chiusura della notifica di sistema.
- Su Android l’app dichiara solo i permessi coerenti con le funzioni esposte: rete, notifiche push OneSignal, Bluetooth LE per la stampante e compatibilità legacy con localizzazione limitata ad Android 11 e precedenti per la scansione BLE. Su iOS espone la descrizione privacy Bluetooth per collegare la stampante termica e stampare DDT e documenti di consegna.
- La release Android usa ora un keystore dedicato letto da `android/key.properties`; le credenziali restano in file privati esclusi dal versionamento. La cartella iOS include anche il `Podfile` standard e il target `OneSignalNotificationServiceExtension` per agevolare il lavoro successivo su Xcode e CocoaPods e abilitare il rich media delle push.
- Per le immagini push il progetto usa il disco Laravel `public`, ma su questo hosting il path statico `/storage/...` puo essere intercettato dal symlink pubblico prima di Laravel. Per questo i nuovi upload usano l'URL canonico `/push-media/{file}` servito da route controllata, mentre gli eventuali link legacy `/storage/push-notifications/...` vengono riscritti internamente verso la stessa route senza esporre l'intera cartella storage.
- Check operativo deploy/restore: se un'immagine push restituisce `403`, verificare prima di tutto che `public/storage`, `storage/app/public` e i file in `storage/app/public/push-notifications/` non risultino con owner/gruppo errati dopo operazioni eseguite come `root`. In questo hosting devono restare servibili da `appteamit:appteamit`.
- Un logout azzera la sessione; l’accesso successivo non legge dati offline di un tenant differente.
- Il servizio di sincronizzazione scarica lo snapshot e invia l’outbox quando la connettività è disponibile durante le azioni operative dell’utente, come sincronizzazione manuale, chiusura ordine o ristampa; inoltre la schermata giro ascolta il ritorno rete e tenta automaticamente il flush delle consegne e delle conferme stampa pendenti. Gli errori applicativi vengono mostrati con il messaggio restituito dalle API e non più come solo codice HTTP.
- Il composer ordine mobile espone solo clienti assegnati e prodotti del rispettivo listino; prima della chiusura raccoglie quantità consegnata e reso.
- Il composer usa anche l’inventario del furgone ricevuto nello snapshot e le bolle ancora pendenti sul dispositivo per bloccare quantità superiori al residuo disponibile per prodotto.
- La stampante scelta viene memorizzata nello storage sicuro tramite identificatore Bluetooth del dispositivo.
- Il servizio BLE esegue scansione e connessione, privilegia le characteristic di stampa comunemente usate dalle termiche generiche e invia ESC/POS in chunk da massimo 20 byte con pausa breve per evitare perdite silenziose di dati sulla PT210.
- La chiusura ordine memorizza e tenta di stampare il DDT prima dell’upload; la mancanza di rete non blocca la ricevuta e lo storico indica gli elementi ancora da sincronizzare.
- La sequenza DDT successiva è calcolata dal massimo numero già registrato sul server e dai documenti conservati localmente, per non riutilizzare un numero dopo il riavvio dell’app.
- Il documento DDT riporta numero progressivo assegnato dal blocco del turno, data ordine, intestazioni di panificio e cliente, righe valorizzate, totali consegnato/reso/netto e spazio firma/timbro; l’ultimo ordine può essere ristampato.
- Le righe consegnato/reso e i totali sono formattati in massimo 32 caratteri per riga, compatibili con la stampa stretta della PT210.
- Le schermate operative usano gli inset di sistema in modo coerente su login, home giro, notifiche, composer ordine e storico ordini, evitando che liste, stati vuoti, loading o pulsanti di chiusura ordine vengano coperti dalla barra di stato o di navigazione Android.
Sicurezza operativa
- L’area amministrativa usa sessione Laravel e middleware distinti per amministratore piattaforma e tenant.
- Un tenant con stato sospeso non può essere utilizzato né dal backoffice del panificio né dalle API mobile, inclusi token già emessi.
- L’API rifiuta una consegna per un cliente non presente nell’assegnazione dell’agente autenticato.
- Le API app sono protette da Sanctum; il login emette un token dedicato con abilità di sincronizzazione agente.
- Il document root protegge file applicativi e configurazioni server tramite regole Apache.
- Questa documentazione pubblica non contiene password né valori di configurazione riservati.