| .vscode | ||
| backend | ||
| backoffice | ||
| docs_img | ||
| frontend | ||
| alfred_logo.svg | ||
| LICENSE | ||
| logo.png | ||
| README.md | ||
📋 Panoramica Generale
Alfred è una monorepo full-stack per la gestione di menu interattivi e ordini da tavolo in ambienti ristorativi. Il sistema consente ai clienti di consultare il menu via QR code/link, selezionare piatti e inviare ordini che vengono stampati automaticamente su stampanti termiche Sunmi in cucina.
Stack Tecnologico
- Backend: Strapi (CMS headless)
- Backoffice: Refine.js (admin panel) (per gestione e creazione QR code dei tavoli)
- Frontend: Next.js 15+ (App Router) + Ant Design
- Database: PostgreSQL (via Strapi) + Redis
- Integrazioni: Sunmi Cloud API (stampa termica)
📁 Struttura della Monorepo
alfred/
├── backend/ # Strapi CMS
│ ├── src/
│ │ ├── api/
│ │ │ ├── menu-item/ # Entità piatti
│ │ │ ├── menu-course/ # Entità portate (Antipasti, Primi, etc.)
│ │ │ ├── table/ # Entità tavoli
│ │ │ ├── order/ # Entità ordini + controller (logica stampa)
│ │ │ └── global/ # Entità config globale
│ │ └── middlewares/
│ └── package.json
│
├── backoffice/ # Admin panel Refine.js
│ ├── src/
│ │ ├── pages/ # CRUD per entità Strapi
│ │ ├── components/
│ └── package.json
│
└── frontend/ # Next.js app cliente
├── src/
│ ├── app/ # App Router
│ ├── components/ # React components
│ │ ├── BottomBar.tsx # Barra fissa con bottone ordine
│ │ └── CartDrawer.tsx # Drawer recap ordine
│ ├── hooks/
│ ├── redux/ # State gestione carrello
│ └── lib/
└── package.json
🖥️ Backend (Strapi)
Configurazione e Setup
Recarsi dentro
backend
Strapi offre la possibilità di avere vari database relazionali come da documentazione
Se non specificate nulla, di default creerà un database dentro tmp/data.db in sqlite
creare un file .env
SUNMI_APPID=custom_sunmi_app_id
SUNMI_APPKEY=custom_sunmi_app_key
REDIS_URL=redis://localhost:6379/0 // verrà ignorato se REDIS_ENABLED è false
REDIS_ENABLED=false // se volete abilitare mettere a true e indicare un valido REDIS_URL
CORS_ORIGIN=http://localhost:3001,http://localhost://3000,http://localhost:5174 // ed eventuali altre origini se cambiate percorsi del frontend e/o del backoffice
lanciare yarn o npm install
Seguire informazioni dentro backend/README.md per lanciare Strapi
🎯 Entità Principali (Data Model)
1. Menu Item
Singolo piatto/prodotto nel menu.
Campi principali:
title(string) - Nome piattodescription(string) - DescrizionetotalPrice(decimal) - Prezzo finalecover(image) - Foto piattomenu_course(relation) - A quale portata appartienealwaysAvailable(boolean) - Se sempre disponibileavailability(integer) - Quantità disponibilevariations(array) - Varianti (es. taglia, cottura)specification(string) - Note specifichenotes(string) - Note aggiuntive utente
2. Menu Course
Categoria portata (Antipasti, Primi, Secondi, Dolci, Bevande, etc.)
Campi principali:
title(string) - Nome portatamenu_items(relation) - Piatti appartenenti
3. Table
Tavolo fisico del ristorante.
Campi principali:
tableId(integer/string) - Numero o nome tavoloorders(relation) - Ordini effettuati da questo tavolo
4. Order
Ordine inviato da un tavolo.
Campi principali:
table(relation) - Tavolo che ha ordinatomenu_items(array) - Piatti ordinati con quantitàdatetime(datetime) - Quando è stato creatogrouped_data(object) - Ordine raggruppato per portata (per stampa)status(enum) - Stato (pending, confirmed, completed, etc.)
5. Global
Configurazioni globali del ristorante.
Campi principali:
title(string) - Nome ristorantebuffer_wait_time(integer) - Tempo di aggregazione ordini (secondi)cloud_printers_sn(string) - SN stampanti Sunmi (comma-separated)menu_course_ordering(array) - Ordine di visualizzazione portateshow_after_checkout_poster(boolean) - Mostra poster dopo checkoutafter_checkout_poster(image) - Immagine congratulazioni
Controller Principale: Order
File: order.ts
Funzionalità Chiave
-
create(ctx)— Endpoint POST per ricevere ordini- Valida disponibilità piatti
- Decrementa
availabilityse necessario - Accoda l'ordine in una coda in-memory per aggregazione
-
enqueueOrder(tableId, newItems, grouped_data)— Logica di aggregazione- Merge ordini multipli dello stesso tavolo entro
buffer_wait_time - Deduplicazione e aggregazione quantità tramite
arrayConcatCustomizer - Timer automatico per finalizzare dopo timeout
- Merge ordini multipli dello stesso tavolo entro
-
finalizeOrder(tableId)— Finalizzazione e stampa- Salva ordine definitivo in Strapi
- Genera ricevuta in formato ESC/POS
- Invia hex-encoded ricevuta a Sunmi Cloud API
-
generateReceipt(order)— Generazione ricevuta termica- Comandi ESC/POS per formattazione (bold, line breaks)
- Raggruppamento piatti per portata
- Stampa varianti, specifiche, note
- Conversione UTF-8 → hex per Sunmi
-
getIncomeDataByDateRange(ctx)— Report income- Query ordini in range date
- Aggregazione fatturato per giorno
Flow Ordine Lato Backend
Client POST /orders
↓
[Validazione disponibilità]
↓
enqueueOrder() → in-memory queue
↓
[Timer: buffer_wait_time secondi]
↓
finalizeOrder()
├─→ Salva in DB (Strapi)
├─→ generateReceipt()
└─→ sendToSunmi()
└─→ Sunmi Cloud API
└─→ Stampante fisica
📱 Frontend (Next.js + Ant Design)
Setup
dal pannello di Strapi su Impostazioni recarsi su Api tokens
Cliccare su Create, chiamate la chiave "Create Order" aggiungere per le seguenti entità le spunte su "find" e "findOne":
- Global
- Menu-item
- Menu-course
- Tag e che abbia tutti i permessi eccetto per la delete per:
- Order
Poi recarsi sempre dalla lista degli Api tokens e recuperare il token per "Read-only"
recarsi dentro frontend
creare un file .env con dentro i seguenti valori:
NEXT_PUBLIC_BACKEND_URL=http://localhost:1337
NEXT_PUBLIC_API_KEY=<api key "Read only" creata in precedenza>
NEXT_PUBLIC_CREATE_ORDER_API_KEY=<api key di "Create Order" in precedenza>
NEXT_PUBLIC_SITE_URL=http://localhost:3000
Struttura Componenti
Layout Generale
App (Page Router)
├── Navigation/Header
├── MenuGrid
│ └── [Per ogni Menu Course]
│ └── MenuItemCard (click → aggiunge al carrello)
└── BottomBar (sticky)
└── CartDrawer (modale ordine)
Componenti Chiave
BottomBar.tsx
- Barra fissa in basso
- Bottone "Conferma e Invia"
- Trigger CartDrawer on click
CartDrawer.tsx
- Mostra recap ordine raggruppato per portata
- Pulsanti +/- per quantità
- Subtotali per portata
- Bottone "Invia Ordine"
- Mostra poster congratulazioni dopo checkout
State Management (Redux)
Store: cartReducer
{
items: CartLine[] // Piatti nel carrello
tableId: string // ID tavolo corrente
menuCourseOrdering: Array // Ordine portate
}
Azioni:
addToCart(menuItem, amount)removeFromCart(uniqueVariationId)removeQuantity(uniqueVariationId, amount)emptyCart()setMenuCourseOrdering(menuCourses)
Flow Ordine Lato Frontend
Utente seleziona piatti
↓
addToCart() → Redux store
↓
BottomBar visibile (se non in /menu)
↓
Click "Conferma e Invia"
↓
CartDrawer opens
↓
User reviews order
↓
Click "Invia"
├─→ POST /api/orders (body con grouped_data)
├─→ setSuccess(true)
└─→ Mostra toast "Ordine inviato"
↓
Mostra poster congratulazioni (3s)
↓
emptyCart()
Routing
/[lang]/menu— Menu principale (visualizza piatti per portata)/[lang]/menu/[courseId]— Filtro per portata specifica (opzionale)
Note: Il routing utilizza [lang] per i-18n ma multilingua non è ancora implementato completamente.
🎨 Backoffice (Refine.js)
Stato: ⚠️ Incompleto. È consigliato usare direttamente l'interfaccia Strapi.
Funzionalità Disponibili
- CRUD base per Menu Items, Menu Courses, Tables
- Gestione ordini con visualizzazione recap
- Realizzazione QR dinamici per i tavoli disponibili, per poi inquadrarli e far partire ordini da tavolo designato
Limitazioni Attuali
- Setup multilingua mancante
- UI non completamente raffinata
- Alcuni campi avanzati non mappati correttamente
Alternativa: Accedere a http://localhost:1337/admin per la UI nativa Strapi.
Setup
tldr; il backoffice non è obbligatorio, è solo un layer UI-friendly se non si vuole passare per la gestione del contenuto tramite Strapi
Una volta che il backend è su, recarsi su schermata utenti

cliccare su "Create new entry", assicurarsi di inserire "confirmed" come true e "role" su "Authenticated"

dopo occorre recarsi su Roles da Impostazioni

dentro authenticated, per le seguenti entità:
- Global
- Menu-course
- Menu-item
- Order
- Table
- Tag
spuntare tutte le voci di permessi disponibili
questo permetterà alle utenze con questo ruolo che accedono al backoffice di editare tutte le entità
🖨️ Integrazione Sunmi
Setup Stampanti
- Registrarsi su Sunmi Cloud (https://partner.sunmi.com/)
- Creare applicazione e ottenere:
SUNMI_APPIDSUNMI_APPKEY
- Associare stampanti allo store (via SN seriale)
- Compilare env nel backend:
SUNMI_APPID=xxx SUNMI_APPKEY=xxx - Configurare Global in Strapi:
- Campo
cloud_printers_sn: valori SN comma-separated - Esempio:
"SPT001,SPT002,SPT003"
- Campo
Flusso Stampa
generateReceipt(order)
├─→ Formato ESC/POS (comandi stampante)
└─→ Buffer UTF-8
Buffer → Hex (Sunmi API wants hex-encoded UTF-8)
↓
sendToSunmi()
├─→ Build request (appid, sign, trade_no)
├─→ POST https://openapi.sunmi.com/v2/printer/open/open/device/pushContent
└─→ Per ogni SN in cloud_printers_sn
└─→ Stampa in coda
Comandi ESC/POS Utilizzati
| Comando | Hex | Significato |
|---|---|---|
| Bold on | 1B 21 10 |
Attiva grassetto |
| Bold off | 1B 21 00 |
Disattiva grassetto |
| Line break | 0A |
Nuova riga |
| Cut | 1B 69 |
Taglia carta |
🔄 Flow Completo Ordine (End-to-End)
Scenario: Cliente al tavolo 3 ordina 2 Carbonare + 1 Tiramisu
[FRONTEND]
┌─────────────────────────────────────────────────────────────┐
│ 1. Client accede https://alfred.eriacinquepetali.it/it/menu │
│ (QR code da tavolo o link diretto) │
│ │
│ 2. Visualizza menu per portata │
│ - Antipasti │
│ - Primi (seleziona 2x Carbonara) │
│ - Dolci (seleziona 1x Tiramisu) │
│ │
│ 3. BottomBar visibile → Click "Conferma e Invia" │
│ │
│ 4. CartDrawer aperto → Recap: │
│ Primi: │
│ 2x Carbonara (€15 × 2 = €30) │
│ Dolci: │
│ 1x Tiramisu (€8) │
│ Totale: €38 │
│ │
│ 5. Click "Invia Ordine" │
└─────────────────────────────────────────────────────────────┘
↓ POST /api/orders
[BACKEND - Order Controller]
┌─────────────────────────────────────────────────────────────┐
│ 6. create(ctx) riceve request │
│ body = { │
│ table: { connect: { documentId: "table_3" } }, │
│ menu_items: [ │
│ { menu_item: {...}, amount: 2, ... }, │
│ { menu_item: {...}, amount: 1, ... } │
│ ], │
│ grouped_data: { │
│ "Primi": [{menuItem: {...}, amount: 2}], │
│ "Dolci": [{menuItem: {...}, amount: 1}] │
│ } │
│ } │
│ │
│ 7. Valida disponibilità ✓ │
│ │
│ 8. enqueueOrder("table_3", items, grouped) │
│ → Aggrega in memoria (se altri ordini da table_3) │
│ → Timer: 15s (buffer_wait_time) │
│ │
│ 9. After 15s → finalizeOrder("table_3") │
│ a) Salva ordine in DB (Strapi) │
│ b) generateReceipt() → ESC/POS string │
│ Output: │
│ ┌────────────────────────┐ │
│ │ RISTORANTE ALFREDO │ │
│ │ Tavolo: 3 │ │
│ │ Ora: 13:45:22 │ │
│ │ ════════════════════ │ │
│ │ /// Primi /// │ │
│ │ 2x Carbonara €30.00│ │
│ │ /// Dolci /// │ │
│ │ 1x Tiramisu €8.00│ │
│ │ ════════════════════ │ │
│ │ Totale pezzi: 3 │ │
│ │ Totale EUR: €38.00 │ │
│ │ [CUT PAPER] │ │
│ └────────────────────────┘ │
│ │
│ c) strToUtf8Hex(receiptString) → hex-encoded │
│ d) sendToSunmi() │
└─────────────────────────────────────────────────────────────┘
↓ HTTP POST
[SUNMI CLOUD API]
┌─────────────────────────────────────────────────────────────┐
│ 10. Receive hex-encoded receipt │
│ POST https://openapi.sunmi.com/.../pushContent │
│ Headers: Authorization, Signature (HMAC-SHA256) │
│ Body: { sn: "SPT001", trade_no: "...", content: "..." } │
│ │
│ 11. Route to printer SN=SPT001 (tavolo 3) │
└─────────────────────────────────────────────────────────────┘
↓
[SUNMI THERMAL PRINTER - Tavolo 3]
┌─────────────────────────────────────────────────────────────┐
│ 12. Riceve dati e stampa immediato: │
│ [CARTA FISICAMENTE STAMPATA IN CUCINA] │
│ │
│ 13. Cucina vede ordine e inizia preparazione │
└─────────────────────────────────────────────────────────────┘
[FRONTEND - Feedback]
┌─────────────────────────────────────────────────────────────┐
│ 14. Toast: "Ordine inviato con successo" (3s) │
│ │
│ 15. CartDrawer mostra poster congratulazioni (se abilitato) │
│ │
│ 16. Utente chiude drawer → carrello svuotato │
└─────────────────────────────────────────────────────────────┘
🚀 Deploy e Ambienti
Produzione
- Frontend: https://alfred.eriacinquepetali.it/it/menu
- Backoffice: (non pubblico - rete interna)
- Backend Strapi: (non pubblico - rete interna)
Variabili d'Ambiente Richieste
Backend (.env):
SUNMI_APPID=<Sunmi App ID>
SUNMI_APPKEY=<Sunmi App Secret>
NEXT_PUBLIC_CREATE_ORDER_API_KEY=<API Key per endpoint POST /orders>
DATABASE_*=<PostgreSQL credentials>
JWT_SECRET=<Random secure string>
Frontend (.env.local):
NEXT_PUBLIC_API_URL=<Backend Strapi URL>
NEXT_PUBLIC_UPLOAD_URL=<Strapi Upload URL>
🐛 Known Issues
1. Backoffice Incompleto
- Problema: Refine.js non copre tutti i CRUD completamente
- Soluzione: Usare direttamente Strapi admin (
/admin) - Priority: Media (backoffice è supplementare)
2. Multilingua Non Implementato
- Problema: Struttura routing
[lang]ma i18n non setup - Soluzione: Integrare
next-i18nextonext-intl - Scope: Tradurre stringhe UI e data Strapi per più lingue
- Priority: Bassa (attualmente solo italiano)
3. Aggregazione Ordini in Memoria
- Problema: Se backend crashes, ordini in coda perduti
- Soluzione: Persistere pending orders in DB o Redis
- Priority: Media (perdita dati rara ma possibile)
4. Generazione Ricevuta Non Modulare
- Problema:
generateReceipt()hardcoded per ESC/POS + Sunmi - Soluzione: Refactor con formatter pattern (vedi sezione precedente)
- Priority: Bassa (funziona, ma manutenzione difficile per altri printer)
📚 Setup Locale
Prerequisiti
- Node.js 18+
- PostgreSQL 12+
- Docker (opzionale)
Installazione
# Clone
git clone <repo>
cd Alfred
# Backend
cd backend
cp .env.example .env
npm install
npm run develop # Strapi admin @ http://localhost:1337
# Backoffice (in altro terminal)
cd ../backoffice
npm install
npm run dev # @ http://localhost:3000
# Frontend (in altro terminal)
cd ../frontend
npm install
npm run dev # @ http://localhost:3001
Database Seed (opzionale)
Strapi fornisce UI per creare:
- Menu Courses (Antipasti, Primi, Secondi, Dolci, Bevande)
- Menu Items (piatti per categoria)
- Tables (tavoli)
- Global config
🔗 Link Utili
- Strapi Docs: https://docs.strapi.io/
- Refine Docs: https://refine.dev/
- Next.js Docs: https://nextjs.org/docs
- Ant Design: https://ant.design/
- Sunmi Cloud API: https://cloudapi.sunmi.com/
- Demo App: https://alfred.eriacinquepetali.it/it/menu
📝 Note Finali
- Sistema production-ready ma con spazi di miglioramento
- Focus attuale: stabilità stampa e user experience menu
- Roadmap: i18n multilingue, migliore backoffice, dashboard analytics
- Supporto Sunmi solido; facile aggiungere altri printer type (vedi refactor formatter)
Supporto e contributing
Questo progetto è nato nel tempo libero per aiutare un amico a liberarsi da certi servizi che caricavano fees inaccettabili per le necessità del ristorante. Non è chiaramente pronto per la larga distribuzione, ma se qualcuno avesse voglia e interesse a contribuire migliorando qualsiasi aspetto è il benvenuto!
License
This project is licensed under the GNU General Public License v3.0 - see the LICENSE file for details.
What this means: ✅ You can use this software for any purpose ✅ You can modify and distribute it ✅ You can use it commercially ⚠️ If you distribute modified versions, they must also be GPL v3 ⚠️ You must include the original license and copyright notices

