monorepo per sistema di menu di ristorazione digitalizzato, con backend per creazione menu, portate, tavoli. Possibilità di creazione QR code tramite backoffice per inquadratura e gestione invio ordine a stampanti termiche
Find a file
2026-01-16 16:10:21 +01:00
.vscode feat: new scopes 2025-11-06 12:27:03 +01:00
backend feat: yarn lock 2026-01-16 16:10:21 +01:00
backoffice fix: revert backoffice refine upgrade 2025-12-19 12:10:37 +01:00
docs_img docs: add new documentations 2026-01-12 10:50:20 +01:00
frontend feat: yarn lock 2026-01-16 16:10:21 +01:00
alfred_logo.svg docs: add new documentations 2026-01-12 10:50:20 +01:00
LICENSE docs: add new documentations 2026-01-12 10:50:20 +01:00
logo.png docs: add new documentations 2026-01-12 10:50:20 +01:00
README.md docs: correct sunmi link and final note on contributing 2026-01-12 11:22:37 +01:00

alfred logo

📋 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 piatto
  • description (string) - Descrizione
  • totalPrice (decimal) - Prezzo finale
  • cover (image) - Foto piatto
  • menu_course (relation) - A quale portata appartiene
  • alwaysAvailable (boolean) - Se sempre disponibile
  • availability (integer) - Quantità disponibile
  • variations (array) - Varianti (es. taglia, cottura)
  • specification (string) - Note specifiche
  • notes (string) - Note aggiuntive utente

2. Menu Course

Categoria portata (Antipasti, Primi, Secondi, Dolci, Bevande, etc.)

Campi principali:

  • title (string) - Nome portata
  • menu_items (relation) - Piatti appartenenti

3. Table

Tavolo fisico del ristorante.

Campi principali:

  • tableId (integer/string) - Numero o nome tavolo
  • orders (relation) - Ordini effettuati da questo tavolo

4. Order

Ordine inviato da un tavolo.

Campi principali:

  • table (relation) - Tavolo che ha ordinato
  • menu_items (array) - Piatti ordinati con quantità
  • datetime (datetime) - Quando è stato creato
  • grouped_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 ristorante
  • buffer_wait_time (integer) - Tempo di aggregazione ordini (secondi)
  • cloud_printers_sn (string) - SN stampanti Sunmi (comma-separated)
  • menu_course_ordering (array) - Ordine di visualizzazione portate
  • show_after_checkout_poster (boolean) - Mostra poster dopo checkout
  • after_checkout_poster (image) - Immagine congratulazioni

Controller Principale: Order

File: order.ts

Funzionalità Chiave

  1. create(ctx) — Endpoint POST per ricevere ordini

    • Valida disponibilità piatti
    • Decrementa availability se necessario
    • Accoda l'ordine in una coda in-memory per aggregazione
  2. 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
  3. finalizeOrder(tableId) — Finalizzazione e stampa

    • Salva ordine definitivo in Strapi
    • Genera ricevuta in formato ESC/POS
    • Invia hex-encoded ricevuta a Sunmi Cloud API
  4. 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
  5. 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

Backend api tokens  page

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 Backend users page

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

dopo occorre recarsi su Roles da Impostazioni Backend roles  page

dentro authenticated, per le seguenti entità:

  • Global
  • Menu-course
  • Menu-item
  • Order
  • Table
  • Tag

spuntare tutte le voci di permessi disponibili

Backend roles  page

questo permetterà alle utenze con questo ruolo che accedono al backoffice di editare tutte le entità


🖨️ Integrazione Sunmi

Setup Stampanti

  1. Registrarsi su Sunmi Cloud (https://partner.sunmi.com/)
  2. Creare applicazione e ottenere:
    • SUNMI_APPID
    • SUNMI_APPKEY
  3. Associare stampanti allo store (via SN seriale)
  4. Compilare env nel backend:
    SUNMI_APPID=xxx
    SUNMI_APPKEY=xxx
    
  5. Configurare Global in Strapi:
    • Campo cloud_printers_sn: valori SN comma-separated
    • Esempio: "SPT001,SPT002,SPT003"

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

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-i18next o next-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


📝 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