Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Flutter

Projet Flutter développement d'une application mobile pour un restaurant

Un cours complet pour maîtriser Projet Flutter développement d'une application mobile pour un restaurant.

0 chapitres 0 QCM Accès gratuit
Dans cet article

Projet Flutter : application mobile Le Délice pour un restaurant avec SQLite

  1. Objectifs

  • Ce projet de 6 heures clôt le chapitre Maîtriser les opérations CRUD avec SQLite dans Flutter. En le réalisant, vous allez apprendre à :
    • concevoir une base relationnelle de cinq tables (menu, commandes, lignes de commande, réservations) et la tester dans DB Browser for SQLite avant d'écrire du Dart ;
    • choisir le bon comportement des clés étrangères : ON DELETE RESTRICT pour protéger les plats, ON DELETE CASCADE pour supprimer une commande avec ses lignes ;
    • écrire des modèles avec factory fromMap(), toMap() et copyWith(), et convertir un enum Dart (statut de commande) en texte SQLite ;
    • réaliser les opérations CRUD une par une avec le cycle SQL dans DB Browser → méthode DAO → test → vérification dans DB Browser → interface;
    • enregistrer une commande et ses lignes dans une transaction, pour qu'une commande ne soit jamais enregistrée à moitié ;
    • gérer des dates et des heures (réservations) avec showDatePicker, showTimePicker et la fonction SQLite date(), et empêcher une double réservation avec une contrainte UNIQUE;
    • calculer un tableau de bord avec des requêtes d'agrégation (SUM, COUNT, AVG, GROUP BY, jointures) ;
    • faire évoluer la base sans perdre les données (onUpgrade, ALTER TABLE) pour ajouter un programme de fidélité ;
    • versionner son travail avec Git et le publier sur GitHub dès le départ : dépôt, commit à chaque étape, push, README.md et version finale étiquetée.
  • Prérequis : Flutter 3.22 ou plus récent ; avoir suivi les leçons des chapitres « Base de données SQLite » et « Maîtriser les opérations CRUD avec SQLite » ; connaître StatefulWidget, Form, FutureBuilder et Navigator.push. Le TP StockFacile est conseillé mais pas obligatoire.
  • Avant la séance :
    • un compte GitHub gratuit et Git installé (vérifier avec git --version) ;
    • DB Browser for SQLite installé (version PortableApp sur les postes sans droits administrateur) ;
    • avec un émulateur Android : l'outil adb fourni avec Android Studio (vérifier avec adb devices).
  • Attention : sqflite ne fonctionne pas sur Flutter Web. Travaillez sur Android, iOS, macOS, Windows ou Linux, jamais dans Chrome.
  1. Description

  • Le restaurant tunisois « Le Délice » veut remplacer ses carnets de commandes et son cahier de réservations par une application mobile utilisée par le personnel de salle. L'application doit permettre de gérer le menu, de prendre les commandes à table ou à emporter, de suivre leur préparation, d'enregistrer les réservations et de connaître chaque jour le chiffre d'affaires et les plats les plus vendus. Les données restent sur la tablette du restaurant, même sans Internet : on utilise une base SQLite locale.
  • La base contient cinq tables:
Table Colonnes principales Rôle
categories id, nom (unique), ordre Entrées, Plats, Desserts, Boissons (dans l'ordre d'affichage du menu)
plats id, nom, description, prix, disponible, categorie_id La carte du restaurant ; un plat appartient à une catégorie
commandes id, date_commande, numero_table (vide = à emporter), statut, total Une commande passée en salle ou à emporter
lignes_commande id, commande_id, plat_id, quantite, prix_unitaire Le détail d'une commande : quel plat, combien, à quel prix
reservations id, nom_client, telephone, date_heure, nb_couverts, numero_table Les réservations de tables ; une table ne peut pas être réservée deux fois au même créneau
  • L'application finale comporte quatre onglets (NavigationBar) et quatre écrans secondaires:
Écran Fichier Flutter Opérations SQLite utilisées
Onglet « Menu » menu_screen.dart rawQuery avec jointure, filtre par catégorie, update (disponibilité), delete
Écran « Formulaire plat » plat_form_screen.dart insert (ajout) et update (modification)
Écran « Panier » panier_screen.dart transaction: une commande + ses lignes
Onglet « Commandes » et écran « Détail commande » commandes_screen.dart, commande_detail_screen.dart LEFT JOIN + GROUP BY, update du statut, delete en cascade
Onglet « Réservations » et écran « Formulaire réservation » reservations_screen.dart, reservation_form_screen.dart insert avec contrainte UNIQUE, filtre par jour avec date()
Onglet « Tableau de bord » dashboard_screen.dart SUM, COUNT, AVG, GROUP BY, fidélité (version 2 de la base)
  • Résultats attendus : chaque partie se termine par une capture « Résultat attendu ». Comparez votre écran avec cette capture avant de faire votre commit : si le résultat diffère, corrigez avant de passer à la partie suivante.
  • Répartition du temps :
Partie Notions travaillées Durée
Partie 00 – Mise en place du projet, de DB Browser et du dépôt GitHub Git, GitHub, DB Browser 30 min
Partie 01 – Concevoir les cinq tables dans DB Browser CREATE TABLE, clés étrangères, CASCADE / RESTRICT, UNIQUE 35 min
Partie 02 – Packages et classes modèles fromMap, toMap, enum StatutCommande 25 min
Partie 03 – Ouvrir et créer la base depuis Flutter DatabaseHelper, onCreate, données de démonstration 25 min
Partie 04 – Le menu : lire et insérer des plats rawQuery, jointure, insert, FutureBuilder 40 min
Partie 05 – Le menu : modifier et supprimer des plats update, delete, showModalBottomSheet 35 min
Partie 06 – Prendre une commande (transaction) panier, transaction, prix figé 45 min
Partie 07 – Suivre les commandes LEFT JOIN, GROUP BY, statut, CASCADE 30 min
Partie 08 – Les réservations dates et heures, UNIQUE, date() 30 min
Partie 09 – Le tableau de bord agrégats, top des plats 25 min
Partie 10 – Migration : le programme de fidélité onUpgrade, ALTER TABLE 20 min
Partie 11 – Intégration finale et version 1.0 sur GitHub tous + git tag 20 min
Total   6 h
  1. Travail à réaliser

  • Partie 00 Mise en place du projet, de DB Browser et du dépôt GitHub
  • DB Browser for SQLite
  • Q0.1. Si DB Browser for SQLite n'est pas encore installé, le télécharger sur sqlitebrowser.org/dl (Windows : Standard installer ou PortableApp; macOS : .dmg; Linux : sudo apt install sqlitebrowser), le lancer et le passer en français (Édition → Préférences → Général → Langue).
  • Q0.2. Repérer les quatre onglets que vous utiliserez tout au long du projet : Structure de la base de données, Parcourir les données, Éditer les pragmas et Exécuter le SQL, ainsi que les boutons « Écrire les modifications » (Ctrl + S) et « Annuler les modifications ».
Rappel : dans DB Browser, rien n'est enregistré dans le fichier .db tant que vous n'avez pas cliqué sur « Écrire les modifications ». Vous pouvez donc tester une requête dangereuse (DELETE, UPDATE sans WHERE) puis revenir en arrière avec « Annuler les modifications ».
  • Créer le projet Flutter
  • Q0.3. Créer le projet avec flutter create le_delice et supprimer le code du compteur dans main.dart. Le nom du paquet Android sera com.example.le_delice: notez-le pour la partie 03.
  • Q0.4. Organiser le projet selon l'arborescence ci-dessous. Créer dès maintenant les dossiers sql/ et captures/ et les fichiers reponses.md et sql/requetes.sql (vides).
le_delice/
├── .gitignore                (modifié en Q0.5)
├── README.md                 (Partie 11)
├── reponses.md               (au fil du TP)
├── captures/                 (au fil du TP)
├── sql/
│   ├── brouillon.db          (Partie 01, non versionné)
│   ├── schema.sql            (Parties 01 et 10)
│   └── requetes.sql          (Parties 01 à 10)
lib/
├── models/
│   ├── categorie.dart
│   ├── plat.dart
│   ├── commande.dart         (avec l'enum StatutCommande)
│   ├── ligne_commande.dart
│   ├── ligne_panier.dart     (pas en base)
│   └── reservation.dart
├── database/
│   ├── database_helper.dart
│   ├── categorie_dao.dart
│   ├── plat_dao.dart
│   ├── commande_dao.dart
│   ├── reservation_dao.dart
│   └── stats_dao.dart
├── screens/
│   ├── tests_screen.dart     (supprimé en partie 11)
│   ├── menu_screen.dart
│   ├── plat_form_screen.dart
│   ├── panier_screen.dart
│   ├── commandes_screen.dart
│   ├── commande_detail_screen.dart
│   ├── reservations_screen.dart
│   ├── reservation_form_screen.dart
│   └── dashboard_screen.dart
└── main.dart
# À ajouter à la fin de .gitignore

# Bases SQLite locales (jamais sur GitHub)
*.db
*.db-journal
*.db-wal
*.db-shm
Un DAO par table : chaque fichier *_dao.dart regroupe les requêtes d'une table. Les écrans n'écrivent jamais de SQL : ils appellent par exemple PlatDao().lirePlats(). Le fichier stats_dao.dart regroupe les requêtes du tableau de bord, qui portent sur plusieurs tables.
  • Q0.5. Ouvrir .gitignore, vérifier qu'il exclut build/ et .dart_tool/, puis ajouter les lignes ci-dessus (à droite). Répondre dans reponses.md: pourquoi versionne-t-on schema.sql mais jamais les fichiers .db?
  • Q0.6. Dans main.dart, créer un MaterialApp (titre « Le Délice », useMaterial3: true, colorSchemeSeed de votre choix, par exemple un rouge brique) avec un simple Scaffold et une AppBar « Le Délice ». Lancer l'application.
  • Publier le projet sur GitHub dès le départ
  • Q0.7. Sur github.com/new, créer un dépôt le-delice, sans cocher « Add a README », « .gitignore » ni « license ». Si le dépôt est privé, inviter l'enseignant comme collaborateur (Settings → Collaborators).
  • Q0.8. Si ce n'est pas déjà fait sur ce poste, configurer votre identité Git (commandes 1 et 2), puis envoyer le projet sur GitHub (commandes 3 à 8) en remplaçant VOTRE-COMPTE par votre nom d'utilisateur.
# 1-2. Identité (une seule fois par poste)
git config --global user.name "Prénom Nom"
git config --global user.email "adresse@exemple.com"

# 3-8. Premier envoi du projet
cd le_delice
git init
git add .
git commit -m "Partie 00 : création du projet Le Délice"
git branch -M main
git remote add origin https://github.com/VOTRE-COMPTE/le-delice.git
git push -u origin main
# Vérifications utiles
git status
git log --oneline
git remote -v
git check-ignore -v sql/brouillon.db
  • Q0.9. Vérifier sur GitHub que lib/, android/, sql/ et pubspec.yaml apparaissent, puis envoyer le lien du dépôt à l'enseignant avant de commencer la partie 01.
Authentification et mots de passe : GitHub n'accepte plus le mot de passe du compte lors d'un git push. Connectez-vous par le navigateur quand Git le propose (Git Credential Manager, VS Code ou GitHub Desktop), ou utilisez un Personal Access Token (Settings → Developer settings → Personal access tokens → Tokens (classic), portée repo) à la place du mot de passe. Copiez-le immédiatement : il n'est affiché qu'une fois.
Règle de sécurité : aucun mot de passe, jeton ou clé (par exemple une clé de paiement en ligne) ne doit être écrit dans le code ou dans un fichier envoyé sur GitHub.
Règle du TP : un commit et un push à la fin de chaque partie.
Chaque partie se termine par un encadré « Git » avec le message de commit attendu. Un dépôt qui ne contient qu'un seul commit déposé en fin de séance n'obtient aucun point sur le critère Git.
Question de réflexion : en équipe, pourquoi des messages de commit clairs (« Partie 06 : commande enregistrée en transaction ») sont-ils plus utiles que « modif » ou « ça marche » ?
  • Résultat attendu à la fin de la partie 00
Résultat attendu à la fin de la partie 00 du projet Le Délice
Application lancée, dépôt le-delice publié sur GitHub avec un premier commit, DB Browser for SQLite prêt.
  • Partie 01 Concevoir les cinq tables dans DB Browser
  • Objectif : construire et tester toute la base à la main avant d'écrire du Dart, en particulier le comportement des clés étrangères lors des suppressions.
  • Q1.1. Dans DB Browser, créer une nouvelle base sql/brouillon.db (cliquer sur Annuler dans la fenêtre « Éditer la définition de la table »). Dans l'onglet « Exécuter le SQL », exécuter le script suivant, puis cliquer sur « Écrire les modifications ».
CREATE TABLE categories (
  id    INTEGER PRIMARY KEY AUTOINCREMENT,
  nom   TEXT    NOT NULL UNIQUE,
  ordre INTEGER NOT NULL DEFAULT 0
);

CREATE TABLE plats (
  id           INTEGER PRIMARY KEY AUTOINCREMENT,
  nom          TEXT    NOT NULL,
  description  TEXT,
  prix         REAL    NOT NULL CHECK (prix > 0),
  disponible   INTEGER NOT NULL DEFAULT 1,
  categorie_id INTEGER NOT NULL,
  FOREIGN KEY (categorie_id) REFERENCES categories(id) ON DELETE RESTRICT
);

CREATE TABLE commandes (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  date_commande TEXT    NOT NULL,
  numero_table  INTEGER,                        -- NULL = à emporter
  statut        TEXT    NOT NULL DEFAULT 'en_attente'
                CHECK (statut IN ('en_attente', 'en_preparation', 'servie', 'annulee')),
  total         REAL    NOT NULL DEFAULT 0
);

CREATE TABLE lignes_commande (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  commande_id   INTEGER NOT NULL,
  plat_id       INTEGER NOT NULL,
  quantite      INTEGER NOT NULL CHECK (quantite > 0),
  prix_unitaire REAL    NOT NULL,
  FOREIGN KEY (commande_id) REFERENCES commandes(id) ON DELETE CASCADE,
  FOREIGN KEY (plat_id)     REFERENCES plats(id)     ON DELETE RESTRICT
);

CREATE TABLE reservations (
  id           INTEGER PRIMARY KEY AUTOINCREMENT,
  nom_client   TEXT    NOT NULL,
  telephone    TEXT    NOT NULL,
  date_heure   TEXT    NOT NULL,                -- ISO 8601 : 2026-10-25T20:00:00
  nb_couverts  INTEGER NOT NULL CHECK (nb_couverts BETWEEN 1 AND 12),
  numero_table INTEGER NOT NULL,
  UNIQUE (numero_table, date_heure)
);
  • Q1.2. Dans l'onglet « Structure de la base de données », vérifier les cinq tables. Répondre dans reponses.md:
    • pourquoi numero_table peut-il être NULL dans commandes mais pas dans reservations?
    • pourquoi stocke-t-on prix_unitaire dans lignes_commande alors que le prix existe déjà dans plats? (Indice : que se passe-t-il pour les anciennes commandes si le prix du couscous augmente ?)
    • que garantit la contrainte UNIQUE (numero_table, date_heure)?
  • Q1.3. Insérer les données de départ avec le script ci-dessous, puis « Écrire les modifications ». Afficher chaque table dans l'onglet « Parcourir les données ».
INSERT INTO categories (nom, ordre) VALUES
  ('Entrées', 1), ('Plats', 2), ('Desserts', 3), ('Boissons', 4);

INSERT INTO plats (nom, description, prix, disponible, categorie_id) VALUES
  ('Brik à l''œuf',          'Feuille de malsouka, œuf, thon, persil', 4.500, 1, 1),
  ('Salade méchouia',        'Poivrons et tomates grillés',            6.000, 1, 1),
  ('Couscous au poisson',    'Mérou, légumes de saison',              22.000, 1, 2),
  ('Ojja merguez',           'Œufs, sauce tomate pimentée',           14.000, 1, 2),
  ('Escalope panée',         'Frites maison et salade',               16.500, 1, 2),
  ('Makroudh',               'Semoule, pâte de dattes, miel',          3.500, 1, 3),
  ('Crème caramel',          'Recette de la maison',                   4.000, 1, 3),
  ('Citronnade',             'Citron pressé, menthe',                  3.000, 1, 4),
  ('Thé à la menthe',        'Thé vert, pignons',                      2.500, 1, 4);

INSERT INTO commandes (date_commande, numero_table, statut, total) VALUES
  ('2026-10-24T12:30:00', 3,    'servie',     35.000),
  ('2026-10-24T12:45:00', NULL, 'en_attente', 18.500);

INSERT INTO lignes_commande (commande_id, plat_id, quantite, prix_unitaire) VALUES
  (1, 1, 2, 4.500), (1, 3, 1, 22.000), (1, 8, 1, 3.000),
  (2, 5, 1, 16.500), (2, 9, 1, 2.500);

INSERT INTO reservations (nom_client, telephone, date_heure, nb_couverts, numero_table) VALUES
  ('Mme Ben Salah', '22123456', '2026-10-25T20:00:00', 4, 4),
  ('M. Trabelsi',   '98765432', '2026-10-25T20:30:00', 2, 1);
  • Q1.4. Écrire et exécuter une par une les requêtes suivantes, puis les copier dans sql/requetes.sql avec un commentaire -- Q1.4.x:
    1. le menu complet : nom du plat, prix et nom de la catégorie, trié par c.ordre puis par nom de plat ;
    2. le détail de la commande 1 : nom du plat, quantité, prix unitaire et sous-total (quantite * prix_unitaire AS sous_total) ;
    3. le total recalculé de chaque commande avec SUM(quantite * prix_unitaire) et GROUP BY commande_id: correspond-il à la colonne total?
    4. les réservations du 25 octobre 2026 avec WHERE date(date_heure) = '2026-10-25'.
  • Q1.5. Tester les suppressions. Exécuter les requêtes suivantes une par une, noter le résultat de chacune, puis cliquer sur « Annuler les modifications »:
-- a) supprimer un plat qui figure dans une commande
DELETE FROM plats WHERE id = 3;

-- b) supprimer une commande, puis compter ses lignes
DELETE FROM commandes WHERE id = 1;
SELECT COUNT(*) FROM lignes_commande WHERE commande_id = 1;

-- c) réserver la table 4 au même créneau que Mme Ben Salah
INSERT INTO reservations (nom_client, telephone, date_heure, nb_couverts, numero_table)
VALUES ('M. Gharbi', '55111222', '2026-10-25T20:00:00', 3, 4);

-- d) un statut qui n'existe pas
UPDATE commandes SET statut = 'livree' WHERE id = 2;
Question de réflexion :
Pourquoi la suppression a) est-elle refusée alors que la b) supprime aussi les lignes de la commande ? Expliquer le choix de RESTRICT d'un côté et de CASCADE de l'autre du point de vue du restaurant.
  • Q1.6. Exporter la structure (Fichier → Exporter → Base de données vers fichier SQL…, option « Exporter uniquement le schéma ») dans sql/schema.sql, puis fermer la base dans DB Browser.
  • Résultat attendu à la fin de la partie 01
Résultat attendu à la fin de la partie 01 du projet Le Délice
Les cinq tables dans DB Browser, le détail d'une commande avec sous-totaux, puis les deux comportements des clés étrangères : suppression refusée (RESTRICT) et suppression en cascade (CASCADE).
Git – fin de la partie 01 : vérifier avec git status que brouillon.db n'apparaît pas.
git add .
git commit -m "Partie 01 : conception des cinq tables dans DB Browser"
git push
  • Partie 02 Packages et classes modèles
  • Objectif : installer sqflite et écrire une classe Dart par table, ainsi qu'une classe pour le panier, qui n'existe qu'en mémoire.
  • Q2.1. Installer les packages : flutter pub add sqflite path intl (intl servira à afficher les dates). En desktop Windows ou Linux, ajouter aussi sqflite_common_ffi et initialiser SQLite dans main() comme ci-dessous.
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  if (!kIsWeb && (Platform.isWindows || Platform.isLinux)) {
    sqfliteFfiInit();
    databaseFactory = databaseFactoryFfi;
  }
  runApp(const LeDeliceApp());
}
  • Q2.2. Dans models/plat.dart, créer la classe Plat à partir du code ci-dessous, puis écrire vous-même toMap() et copyWith().
class Plat {
  final int? id;
  final String nom;
  final String? description;
  final double prix;
  final bool disponible;
  final int categorieId;

  const Plat({
    this.id,
    required this.nom,
    this.description,
    required this.prix,
    this.disponible = true,
    required this.categorieId,
  });

  factory Plat.fromMap(Map<String, dynamic> map) {
    return Plat(
      id: map['id'] as int?,
      nom: map['nom'] as String,
      description: map['description'] as String?,
      prix: (map['prix'] as num).toDouble(),
      disponible: map['disponible'] == 1,
      categorieId: map['categorie_id'] as int,
    );
  }

  // À écrire : Map<String, dynamic> toMap()  et  Plat copyWith({...})
}
  • Q2.3. Dans models/commande.dart, recopier l'enum StatutCommande et la classe Commande. L'enum garde à la fois le code stocké en base ('en_preparation') et le libellé affiché (« En préparation »).
enum StatutCommande {
  enAttente('en_attente', 'En attente'),
  enPreparation('en_preparation', 'En préparation'),
  servie('servie', 'Servie'),
  annulee('annulee', 'Annulée');

  const StatutCommande(this.code, this.libelle);
  final String code;     // valeur stockée dans SQLite
  final String libelle;  // texte affiché à l'écran

  static StatutCommande depuisCode(String code) =>
      StatutCommande.values.firstWhere((s) => s.code == code);
}

class Commande {
  final int? id;
  final DateTime dateCommande;
  final int? numeroTable;          // null = à emporter
  final StatutCommande statut;
  final double total;

  const Commande({
    this.id,
    required this.dateCommande,
    this.numeroTable,
    this.statut = StatutCommande.enAttente,
    this.total = 0,
  });

  factory Commande.fromMap(Map<String, dynamic> map) {
    return Commande(
      id: map['id'] as int?,
      dateCommande: DateTime.parse(map['date_commande'] as String),
      numeroTable: map['numero_table'] as int?,
      statut: StatutCommande.depuisCode(map['statut'] as String),
      total: (map['total'] as num).toDouble(),
    );
  }

  Map<String, dynamic> toMap() => {
        if (id != null) 'id': id,
        'date_commande': dateCommande.toIso8601String(),
        'numero_table': numeroTable,
        'statut': statut.code,
        'total': total,
      };
}
  • Q2.4. Sur le même modèle, écrire vous-même les classes Categorie (id, nom, ordre), LigneCommande (id, commandeId, platId, quantite, prixUnitaire) et Reservation (id, nomClient, telephone, dateHeure de type DateTime, nbCouverts, numeroTable), chacune avec fromMap et toMap.
  • Q2.5. Dans models/ligne_panier.dart, créer la classe du panier. Elle n'a ni fromMap ni toMap: pourquoi ?
import 'plat.dart';

/// Une ligne du panier en cours (n'existe qu'en mémoire, jamais en base).
class LignePanier {
  final Plat plat;
  int quantite;

  LignePanier(this.plat, {this.quantite = 1});

  double get sousTotal => plat.prix * quantite;
}
Question de réflexion :
Pourquoi stocke-t-on statut.code ('en_preparation') et non statut.index (le chiffre 1) ? Que deviendraient les commandes déjà enregistrées si l'on ajoutait plus tard un statut « Prête » entre « En préparation » et « Servie » ?
  • Résultat attendu à la fin de la partie 02
Résultat attendu à la fin de la partie 02 du projet Le Délice
Packages installés et modèles créés ; l'enum StatutCommande garde le code stocké en base et le libellé affiché.
Git – fin de la partie 02 :
git add .
git commit -m "Partie 02 : packages et modèles (Plat, Commande, Reservation...)"
git push
  • Partie 03 Ouvrir et créer la base depuis Flutter
  • Objectif : créer le DatabaseHelper qui ouvre la base une seule fois, crée les cinq tables et insère le menu de démonstration, puis le vérifier dans DB Browser.
  • Q3.1. Dans database/database_helper.dart, recopier le code suivant. Les CREATE TABLE sont ceux de schema.sql: il suffit de les copier depuis ce fichier.
import 'package:flutter/foundation.dart';
import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

class DatabaseHelper {
  static const String nomBase = 'le_delice.db';
  static const int versionBase = 1;

  DatabaseHelper._interne();
  static final DatabaseHelper _instance = DatabaseHelper._interne();
  factory DatabaseHelper() => _instance;

  static Database? _db;

  Future<Database> get database async {
    _db ??= await _ouvrirBase();
    return _db!;
  }

  Future<Database> _ouvrirBase() async {
    final chemin = join(await getDatabasesPath(), nomBase);
    debugPrint('Base SQLite : $chemin');
    return openDatabase(
      chemin,
      version: versionBase,
      onConfigure: (db) async => db.execute('PRAGMA foreign_keys = ON'),
      onCreate: _creerTables,
    );
  }

  Future<void> _creerTables(Database db, int version) async {
    debugPrint('onCreate : création des tables (version $version)');

    await db.execute('''CREATE TABLE categories ( ... )''');      // copier depuis schema.sql
    await db.execute('''CREATE TABLE plats ( ... )''');
    await db.execute('''CREATE TABLE commandes ( ... )''');
    await db.execute('''CREATE TABLE lignes_commande ( ... )''');
    await db.execute('''CREATE TABLE reservations ( ... )''');

    await _insererMenuDemo(db);
  }

  Future<void> _insererMenuDemo(Database db) async {
    final categories = ['Entrées', 'Plats', 'Desserts', 'Boissons'];
    for (var i = 0; i < categories.length; i++) {
      await db.insert('categories', {'nom': categories[i], 'ordre': i + 1});
    }
    const plats = [
      ['Brik à l\'œuf', 'Feuille de malsouka, œuf, thon, persil', 4.5, 1],
      ['Salade méchouia', 'Poivrons et tomates grillés', 6.0, 1],
      ['Couscous au poisson', 'Mérou, légumes de saison', 22.0, 2],
      ['Ojja merguez', 'Œufs, sauce tomate pimentée', 14.0, 2],
      ['Escalope panée', 'Frites maison et salade', 16.5, 2],
      ['Makroudh', 'Semoule, pâte de dattes, miel', 3.5, 3],
      ['Crème caramel', 'Recette de la maison', 4.0, 3],
      ['Citronnade', 'Citron pressé, menthe', 3.0, 4],
      ['Thé à la menthe', 'Thé vert, pignons', 2.5, 4],
    ];
    for (final p in plats) {
      await db.insert('plats', {
        'nom': p[0], 'description': p[1], 'prix': p[2], 'categorie_id': p[3],
      });
    }
  }

  Future<void> fermer() async {
    await _db?.close();
    _db = null;
  }
}
  • Q3.2. Répondre dans reponses.md: pourquoi le menu de démonstration est-il inséré dans onCreate? Combien de fois sera-t-il inséré dans la vie de l'application ? Pourquoi le PRAGMA foreign_keys = ON est-il dans onConfigure?
  • Q3.3. Créer un écran TestsScreen (banc d'essai) qui devient provisoirement la page d'accueil. Ajouter un bouton « Ouvrir la base » qui affiche dans la console le nombre de catégories et de plats :
final db = await DatabaseHelper().database;
final nbCategories = Sqflite.firstIntValue(await db.rawQuery('SELECT COUNT(*) FROM categories'));
final nbPlats = Sqflite.firstIntValue(await db.rawQuery('SELECT COUNT(*) FROM plats'));
debugPrint('Catégories : $nbCategories · Plats : $nbPlats');
  • Q3.4. Lancer l'application, cliquer sur le bouton et relever dans la console le chemin de la base, le message onCreate et les nombres affichés. Relancer l'application : le message onCreate réapparaît-il ?
  • Q3.5. Récupérer le fichier le_delice.db et l'ouvrir dans DB Browser en lecture seule:
Environnement Méthode
Desktop Ouvrir directement le chemin affiché dans la console.
Émulateur, Android Studio View → Tool Windows → Device Explorer, dossier data/data/com.example.le_delice/databases/, clic droit → Save As sur tout le dossier.
Émulateur ou téléphone, adb Dans cmd (pas PowerShell) :
adb exec-out run-as com.example.le_delice cat databases/le_delice.db > le_delice.db
adb exec-out run-as com.example.le_delice cat databases/le_delice.db-wal > le_delice.db-wal
Pièges fréquents : PowerShell corrompt le fichier redirigé avec > (utilisez cmd) ; copiez toujours le fichier -wal avec le .db, sinon les dernières lignes ajoutées manquent ; sur émulateur, vous travaillez sur une copie qu'il faut refaire après chaque test.
  • Q3.6. Dans DB Browser, vérifier que les cinq tables existent, que categories contient 4 lignes et plats 9 lignes, et que les tables commandes, lignes_commande et reservations sont vides. Capture : captures/03_structure.png.
  • Résultat attendu à la fin de la partie 03
Résultat attendu à la fin de la partie 03 du projet Le Délice
Banc d'essai, messages de la console (chemin, onCreate, menu de démonstration) et contenu des cinq tables vérifié dans DB Browser.
Git – fin de la partie 03 :
git add .
git commit -m "Partie 03 : DatabaseHelper, cinq tables et menu de démonstration"
git push
Méthode à suivre pour CHAQUE opération des parties 04 à 10 : le cycle en 5 étapes
Étape 1 – SQL : écrire et tester la requête dans DB Browser sur brouillon.db, puis la copier dans requetes.sql.
Étape 2 – DAO : écrire la méthode Dart correspondante.
Étape 3 – Test : l'appeler depuis un bouton de TestsScreen et lire la console.
Étape 4 – Vérification : ouvrir la base de l'application dans DB Browser et constater le changement.
Étape 5 – Interface : seulement ensuite, brancher la méthode sur le vrai écran, puis faire un commit.
  • Partie 04 Le menu : lire et insérer des plats
  • Objectif : afficher la carte du restaurant, classée par catégorie, et ajouter un plat.
  • Lire le menu
  • Q4.1 (Étape 1 – SQL). Reprendre la requête Q1.4.1 et y ajouter c.ordre et c.nom AS categorie_nom. Pourquoi l'alias categorie_nom est-il indispensable ici ?
  • Q4.2 (Étape 2 – DAO). Dans database/plat_dao.dart, créer la classe PlatDao avec la méthode suivante, qui accepte un filtre facultatif par catégorie :
Future<List<Map<String, Object?>>> lireMenu({int? categorieId}) async {
  final db = await DatabaseHelper().database;
  return db.rawQuery('''
    SELECT p.*, c.nom AS categorie_nom, c.ordre
    FROM plats p
    INNER JOIN categories c ON c.id = p.categorie_id
    ${categorieId != null ? 'WHERE p.categorie_id = ?' : ''}
    ORDER BY c.ordre, p.nom
  ''', categorieId != null ? [categorieId] : []);
}
  • Q4.3. Dans database/categorie_dao.dart, écrire Future<List<Categorie>> lireCategories() triée par ordre.
  • Q4.4 (Étape 3 – Test). Ajouter un bouton « Test : lire le menu » qui affiche chaque ligne dans la console sous la forme Plats · Ojja merguez · 14.000 DT. Le tester avec et sans categorieId.
  • Q4.5 (Étape 5 – Interface). Créer MenuScreen (StatefulWidget) qui charge le menu dans initState et l'affiche avec un FutureBuilder (chargement, erreur, liste vide, données) :
    • les plats sont regroupés par catégorie, avec un titre de section (« Entrées », « Plats »…) ; regrouper les lignes en Dart dans une Map<String, List<Plat>>;
    • chaque plat est un ListTile: nom en titre, description en sous-titre (une ligne, overflow: TextOverflow.ellipsis), prix au format 14.000 DT en trailing;
    • un plat indisponible est grisé et affiche une petite étiquette « Épuisé ».
  • Q4.6. Au-dessus de la liste, ajouter une rangée de ChoiceChip (« Tout », puis une puce par catégorie lue en base). Choisir une puce relance lireMenu(categorieId: …). Ajouter un RefreshIndicator.
  • Ajouter un plat
  • Q4.7 (Étapes 1 à 4). Écrire l'INSERT d'un nouveau plat « Lablabi » (bol de pois chiches, 5,000 DT, catégorie Entrées) dans DB Browser, puis la méthode Future<int> insererPlat(Plat plat) dans PlatDao. La tester avec un bouton, puis vérifier dans DB Browser que le plat a l'id 10.
  • Q4.8. Ajouter un bouton qui insère un plat avec un prix de 0, puis un autre avec categorieId: 99. Relever les deux messages d'erreur et indiquer quelle contrainte de la partie 01 bloque chacun.
  • Résultat attendu à la fin de la partie 04
Résultat attendu à la fin de la partie 04 du projet Le Délice
Menu groupé par catégorie, filtre par catégorie, tests dans la console et plat inséré vérifié dans DB Browser.
Git – fin de la partie 04 :
git add .
git commit -m "Partie 04 : menu par catégorie (jointure) et insertion d'un plat"
git push
  • Partie 05 Le menu : modifier et supprimer des plats
  • Objectif : gérer la carte depuis l'application : ajouter, modifier, rendre indisponible et supprimer un plat.
  • Q5.1. Créer PlatFormScreen avec un paramètre optionnel Plat? plat (null = ajout, sinon modification). Le Form contient : nom (obligatoire), description (maxLines: 3), prix (nombre strictement positif, virgule acceptée), catégorie (DropdownButtonFormField<int> rempli depuis la base) et un SwitchListTile « Disponible ».
  • Q5.2. Dans MenuScreen, un FloatingActionButton ouvre le formulaire en mode ajout. Au retour (Navigator.pop(context, true)), rafraîchir le menu et afficher « Plat ajouté ».
  • Q5.3 (Étapes 1 à 4 – Update). Dans DB Browser, écrire l'UPDATE qui passe le prix de l'escalope à 17,500 DT. Écrire ensuite dans PlatDao:
    • Future<int> modifierPlat(Plat plat) qui met à jour toutes les colonnes ;
    • Future<int> changerDisponibilite(int id, bool disponible) qui ne modifie que la colonne disponible.
  • Les tester depuis TestsScreen et vérifier dans DB Browser que seule la ligne visée a changé.
  • Q5.4 (Étape 5). Un appui long sur un plat du menu ouvre un showModalBottomSheet avec trois actions :
    • « Modifier » : ouvre PlatFormScreen pré-rempli, puis enregistre avec copyWith et modifierPlat;
    • « Marquer comme épuisé » / « Remettre au menu » selon l'état du plat : appelle changerDisponibilite;
    • « Supprimer » : voir Q5.5.
  • Q5.5 (Delete). Écrire Future<int> supprimerPlat(int id). L'action « Supprimer » affiche un AlertDialog de confirmation, puis supprime le plat. Entourer l'appel d'un try / catch: si une DatabaseException contient FOREIGN KEY, afficher le SnackBar « Ce plat figure dans des commandes : marquez-le plutôt comme épuisé ».
  • Q5.6. À ce stade, la base de l'application ne contient encore aucune commande : supprimer « Lablabi » doit donc fonctionner. Sur brouillon.db, rappeler ce qui se passe pour le couscous (Q1.5 a). Vous referez ce test dans l'application en partie 06.
Question de réflexion :
Pour un restaurant, pourquoi est-il plus sûr de marquer un plat comme « épuisé » que de le supprimer ? Pensez aux statistiques de vente de la partie 09.
  • Résultat attendu à la fin de la partie 05
Résultat attendu à la fin de la partie 05 du projet Le Délice
Actions sur un plat (appui long), formulaire en mode modification, puis menu avec le prix modifié et un plat épuisé.
Git – fin de la partie 05 :
git add .
git commit -m "Partie 05 : formulaire plat, disponibilité et suppression protégée"
git push
  • Partie 06 Prendre une commande (transaction)
  • Objectif : composer un panier dans le menu, puis enregistrer la commande et toutes ses lignes en une seule opération : soit tout est enregistré, soit rien.
  • Le panier (en mémoire)
  • Q6.1. Dans MenuScreen, ajouter une variable final Map<int, LignePanier> _panier = {}; (clé = id du plat). Sur chaque plat disponible, un IconButton (Icons.add_shopping_cart) ajoute le plat ou augmente sa quantité.
  • Q6.2. Dans l'AppBar, afficher une icône panier entourée d'un widget Badge indiquant le nombre d'articles. Elle ouvre PanierScreen en lui passant le panier.
  • Q6.3. PanierScreen affiche chaque ligne (nom, boutons « − » / « + », sous-total), le total, un DropdownButtonFormField<int?> pour la table (« À emporter » = null, puis tables 1 à 10) et un bouton « Valider la commande ». Une quantité ramenée à 0 retire la ligne.
  • Enregistrer la commande dans une transaction
  • Q6.4 (Étape 1 – SQL). Dans DB Browser, écrire à la main les trois requêtes qu'il faudrait pour enregistrer une commande de 2 thés et 1 makroudh à la table 6 : un INSERT dans commandes, puis deux INSERT dans lignes_commande. Quelle valeur faut-il connaître après le premier INSERT pour écrire les deux suivants ?
  • Q6.5 (Étape 2 – DAO). Dans database/commande_dao.dart, recopier la méthode suivante et commenter chaque bloc :
Future<int> creerCommande({int? numeroTable, required List<LignePanier> lignes}) async {
  final db = await DatabaseHelper().database;

  return db.transaction((txn) async {
    final total = lignes.fold<double>(0, (somme, l) => somme + l.sousTotal);

    final commandeId = await txn.insert(
      'commandes',
      Commande(dateCommande: DateTime.now(), numeroTable: numeroTable, total: total).toMap(),
    );

    for (final l in lignes) {
      await txn.insert('lignes_commande', {
        'commande_id': commandeId,
        'plat_id': l.plat.id,
        'quantite': l.quantite,
        'prix_unitaire': l.plat.prix,   // prix figé au moment de la commande
      });
    }
    return commandeId;
  });
}
  • Q6.6 (Étapes 3 et 4). Ajouter un bouton de test qui crée une commande de 2 thés et 1 makroudh à la table 6, puis vérifier dans DB Browser : une ligne dans commandes (total 8,500 DT) et deux lignes dans lignes_commande qui portent son id.
  • Q6.7 (Test d'atomicité). Ajouter un second bouton qui crée une commande dont la deuxième ligne a une quantité de 0. Quelle contrainte échoue ? Vérifier dans DB Browser que ni la commande ni sa première ligne n'ont été enregistrées. Que se serait-il passé sans transaction ?
  • Q6.8 (Étape 5). Brancher le bouton « Valider la commande » : appeler creerCommande, vider le panier, revenir au menu et afficher « Commande n° X enregistrée ». Le bouton est désactivé si le panier est vide.
  • Q6.9. Refaire maintenant le test de la Q5.6 dans l'application : essayer de supprimer un plat qui vient d'être commandé. Le SnackBar de la Q5.5 doit apparaître.
  • Q6.10. Modifier le prix du thé à la menthe (Q5.4), puis vérifier dans DB Browser que la ligne de la commande de la Q6.6 a gardé l'ancien prix. Quel problème cela évite-t-il ?
Piège : à l'intérieur de db.transaction(...), il faut utiliser txn et jamais db. Un appel à db attendrait la fin de la transaction, qui attend elle-même cet appel : l'application se bloque.
  • Résultat attendu à la fin de la partie 06
Résultat attendu à la fin de la partie 06 du projet Le Délice
Ajout au panier, panier avec choix de la table, puis commande enregistrée en transaction et test d'atomicité.
Git – fin de la partie 06 :
git add .
git commit -m "Partie 06 : panier et commande enregistrée en transaction"
git push
  • Partie 07 Suivre les commandes
  • Objectif : lister les commandes avec leur nombre d'articles, afficher leur détail et faire avancer leur statut jusqu'au service.
  • Q7.1 (Étape 1 – SQL). Mettre au point dans DB Browser la requête de la liste des commandes. Pourquoi faut-il un LEFT JOIN et non un INNER JOIN? Pourquoi COALESCE?
SELECT c.*, COALESCE(SUM(l.quantite), 0) AS nb_articles
FROM commandes c
LEFT JOIN lignes_commande l ON l.commande_id = c.id
GROUP BY c.id
ORDER BY c.date_commande DESC;
  • Q7.2 (Étapes 2 à 4). Dans CommandeDao, écrire :
    • lireCommandes({StatutCommande? statut}) avec la requête ci-dessus et un filtre facultatif WHERE c.statut = ?;
    • lireLignes(int commandeId) qui retourne le nom du plat, la quantité, le prix unitaire et le sous-total (jointure avec plats) ;
    • changerStatut(int id, StatutCommande statut);
    • supprimerCommande(int id).
  • Q7.3 (Étape 5). Créer CommandesScreen: une rangée de FilterChip (Toutes, En attente, En préparation, Servie, Annulée) puis la liste. Chaque commande affiche « n° 12 · Table 3 » (ou « À emporter »), l'heure (DateFormat.Hm() du package intl), le nombre d'articles, le total et une étiquette colorée selon le statut (orange, bleu, vert, gris).
  • Q7.4. Créer CommandeDetailScreen qui reçoit l'id de la commande et affiche ses lignes et son total. Selon le statut, afficher le bouton de l'étape suivante :
    • « En attente » → bouton « Lancer la préparation » ;
    • « En préparation » → bouton « Marquer comme servie » ;
    • tant que la commande n'est pas servie : bouton « Annuler la commande » (avec confirmation) ;
    • « Servie » ou « Annulée » : plus aucun bouton, la commande est terminée.
  • Q7.5. Sur une commande annulée, ajouter un bouton « Supprimer ». Après la suppression, vérifier dans DB Browser que ses lignes ont disparu de lignes_commande sans aucun code supplémentaire. Quelle clause de la partie 01 en est responsable ?
  • Q7.6. Au retour du détail, la liste doit se rafraîchir (statut et filtre à jour). Quelle technique de la partie 05 réutilisez-vous ?
  • Résultat attendu à la fin de la partie 07
Résultat attendu à la fin de la partie 07 du projet Le Délice
Liste des commandes avec statut coloré, détail avec l'étape suivante, requête LEFT JOIN + GROUP BY et suppression en cascade vérifiée.
Git – fin de la partie 07 :
git add .
git commit -m "Partie 07 : liste, détail et statut des commandes"
git push
  • Partie 08 Les réservations
  • Objectif : enregistrer les réservations jour par jour, en empêchant de réserver deux fois la même table au même créneau.
  • Q8.1 (Étape 1 – SQL). Dans DB Browser, comparer les résultats de WHERE date_heure LIKE '2026-10-25%' et de WHERE date(date_heure) = '2026-10-25'. Lequel préférez-vous et pourquoi ? Que retourne SELECT date('2026-10-25T20:00:00'), time('2026-10-25T20:00:00');?
  • Q8.2 (Étape 2 – DAO). Dans database/reservation_dao.dart, écrire insererReservation, lireReservationsDuJour(DateTime jour) (triées par heure) et supprimerReservation. Pour la date du jour, utiliser DateFormat('yyyy-MM-dd').format(jour).
  • Q8.3 (Étapes 3 et 4). Tester l'insertion de la réservation de Mme Ben Salah, puis d'une seconde réservation de la table 4 au même créneau. Intercepter l'erreur avec e.isUniqueConstraintError() et afficher « La table 4 est déjà réservée à 20:00 ».
  • Q8.4 (Étape 5). Créer ReservationFormScreen:
    • nom du client (obligatoire) et téléphone (8 chiffres, keyboardType: TextInputType.phone) ;
    • un bouton qui ouvre showDatePicker (pas de date passée) et un bouton qui ouvre showTimePicker; n'accepter que les créneaux d'ouverture, de 12:00 à 14:30 et de 19:00 à 22:30 ;
    • la date et l'heure sont combinées en un seul DateTime(annee, mois, jour, heure, minute), sans secondes ;
    • le nombre de couverts (1 à 12) et le numéro de table (1 à 10).
  • Q8.5. Créer ReservationsScreen: en haut, un sélecteur de jour (flèches « précédent » / « suivant » et la date au format « samedi 25 octobre ») ; en dessous, les réservations de ce jour (heure, nom, couverts, table). Un Dismissible supprime une réservation après confirmation. Le bouton « + » ouvre le formulaire, pré-rempli avec le jour affiché.
Question de réflexion :
Pourquoi est-il important de construire la date de réservation sans secondes ni millisecondes ? Que deviendrait la contrainte UNIQUE (numero_table, date_heure) si l'on enregistrait DateTime.now()?
  • Résultat attendu à la fin de la partie 08
Résultat attendu à la fin de la partie 08 du projet Le Délice
Réservations du jour, choix de l'heure avec showTimePicker et double réservation refusée par la contrainte UNIQUE.
Git – fin de la partie 08 :
git add .
git commit -m "Partie 08 : réservations par jour et contrainte UNIQUE"
git push
  • Partie 09 Le tableau de bord
  • Objectif : donner au gérant les chiffres de la journée, calculés par SQLite.
  • Q9.1 (Étape 1 – SQL). Dans DB Browser, mettre au point et copier dans requetes.sql les requêtes suivantes, en ne comptant que les commandes servies du jour (date(date_commande) = date('now', 'localtime')) :
    1. le chiffre d'affaires du jour (COALESCE(SUM(total), 0)) ;
    2. le nombre de commandes servies et le panier moyen (AVG(total)) ;
    3. le nombre de réservations du jour ;
    4. le top 5 des plats les plus vendus (toutes dates, commandes non annulées) :
SELECT p.nom, SUM(l.quantite) AS quantite_vendue
FROM lignes_commande l
INNER JOIN plats p     ON p.id = l.plat_id
INNER JOIN commandes c ON c.id = l.commande_id
WHERE c.statut <> 'annulee'
GROUP BY p.id
ORDER BY quantite_vendue DESC
LIMIT 5;
  • Q9.2 (Étape 2). Dans database/stats_dao.dart, écrire une méthode par requête. Pour une valeur unique entière, utiliser Sqflite.firstIntValue(); pour une somme, (resultat.first['total'] as num?)?.toDouble() ?? 0.
  • Q9.3 (Étape 5). Créer DashboardScreen: quatre Card en grille (chiffre d'affaires, commandes servies, panier moyen, réservations du jour), puis le top 5 des plats, chacun avec une barre proportionnelle (LinearProgressIndicator(value: quantite / maximum)).
  • Q9.4. Servir une commande dans l'onglet Commandes, revenir au tableau de bord : les chiffres doivent changer. Si ce n'est pas le cas, expliquer pourquoi et corriger.
  • Q9.5. Pourquoi la requête du chiffre d'affaires ne doit-elle compter que les commandes servies ? Quelle erreur ferait le gérant si les commandes annulées étaient comptées ?
  • Résultat attendu à la fin de la partie 09
Résultat attendu à la fin de la partie 09 du projet Le Délice
Tableau de bord du jour et requêtes d'agrégation testées dans DB Browser (chiffre d'affaires, top 5 des plats).
Git – fin de la partie 09 :
git add .
git commit -m "Partie 09 : tableau de bord (chiffre d'affaires, top des plats)"
git push
  • Partie 10 Migration : le programme de fidélité
  • Objectif : le restaurant veut récompenser ses clients fidèles : 1 point pour 10 DT dépensés. Il faut enregistrer le téléphone du client avec chaque commande, sans perdre les commandes déjà saisies sur la tablette.
  • Q10.1 (La mauvaise méthode). Ajouter seulement la colonne telephone_client TEXT dans le CREATE TABLE commandes de _creerTables et le champ dans le modèle, puis passer une commande. Recopier l'erreur obtenue. Dans DB Browser, relever la valeur de User Version (onglet « Éditer les pragmas »).
  • Q10.2 (La bonne méthode). Passer versionBase à 2, garder la colonne dans _creerTables (nouvelles installations) et ajouter onUpgrade: _migrer:
Future<void> _migrer(Database db, int ancienne, int nouvelle) async {
  debugPrint('onUpgrade : $ancienne → $nouvelle');
  if (ancienne < 2) {
    await db.execute('ALTER TABLE commandes ADD COLUMN telephone_client TEXT');
  }
}
  • Q10.3. Relancer l'application et vérifier dans DB Browser : la colonne existe, les anciennes commandes sont toujours là (avec NULL dans la nouvelle colonne) et User Version vaut 2. Pourquoi écrit-on if (ancienne < 2) et non if (ancienne == 1)?
  • Q10.4. Dans PanierScreen, ajouter un champ facultatif « Téléphone du client (fidélité) » transmis à creerCommande.
  • Q10.5. Mettre au point dans DB Browser, puis afficher dans le tableau de bord, la liste des clients fidèles :
SELECT telephone_client,
       COUNT(*)                         AS nb_commandes,
       SUM(total)                       AS montant,
       CAST(SUM(total) / 10 AS INTEGER) AS points
FROM commandes
WHERE telephone_client IS NOT NULL AND statut = 'servie'
GROUP BY telephone_client
ORDER BY points DESC;
  • Q10.6. Ré-exporter le schéma de la base de l'application (version 2) dans sql/schema.sql.
Question de réflexion :
Désinstaller l'application fait aussi disparaître l'erreur de la Q10.1. Pourquoi est-ce inacceptable une fois l'application utilisée par le restaurant ?
  • Résultat attendu à la fin de la partie 10
Résultat attendu à la fin de la partie 10 du projet Le Délice
Erreur sans migration puis onUpgrade : 1 → 2, User Version = 2, nouvelle colonne telephone_client et clients fidèles avec leurs points.
Git – fin de la partie 10 :
git add .
git commit -m "Partie 10 : migration version 2, programme de fidélité"
git push
  • Partie 11 Intégration finale et version 1.0 sur GitHub
  • Objectif : assembler les écrans et finaliser le dépôt.
  • Q11.1. Créer une HomePage avec une NavigationBar à quatre destinations : « Menu » (Icons.restaurant_menu), « Commandes » (Icons.receipt_long), « Réservations » (Icons.event_seat) et « Tableau de bord » (Icons.insights). Supprimer tests_screen.dart et tous ses appels.
  • Q11.2. Dans l'AppBar du tableau de bord, ajouter un menu avec « Réinitialiser la base » : après confirmation, fermer la base, la supprimer avec deleteDatabase(chemin) et remettre _db à null. Que se passe-t-il au prochain accès à la base ?
  • Q11.3. Scénario de recette complet, à dérouler sans erreur : ajouter un plat, prendre une commande de trois plats à la table 2 avec un numéro de fidélité, la faire passer en préparation puis servie, enregistrer une réservation pour ce soir, vérifier le tableau de bord, puis redémarrer l'application et vérifier que tout est conservé.
  • Q11.4. Faire les captures finales dans captures/: menu, panier, liste et détail des commandes, réservations, tableau de bord, ainsi que trois captures de DB Browser (structure, lignes_commande d'une commande, User Version = 2).
  • Finaliser le dépôt GitHub
  • Q11.5. Rédiger le README.md (modèle ci-dessous), vérifier que reponses.md, sql/schema.sql et sql/requetes.sql sont complets, et qu'aucun fichier .db ni aucun secret n'est versionné (git ls-files | findstr .db sous Windows, git ls-files | grep .db sous Linux/macOS).
  • Q11.6. Faire le dernier commit, poser l'étiquette v1.0 et l'envoyer sur GitHub. Vérifier que le README s'affiche avec les captures et que l'étiquette apparaît dans Tags.
# Le Délice

Application Flutter de gestion de restaurant réalisée
dans le cadre du projet « CRUD avec SQLite ».

## Fonctionnalités
- Menu par catégorie : ajout, modification, plats épuisés
- Prise de commande avec panier (transaction SQLite)
- Suivi des commandes : en attente, en préparation, servie
- Réservations de tables sans double réservation
- Tableau de bord : chiffre d'affaires, top des plats
- Programme de fidélité (migration version 2)

## Base de données
categories, plats, commandes, lignes_commande, reservations
Schéma complet : sql/schema.sql

## Captures d'écran
![Menu](captures/11_menu.png)
![Commandes](captures/11_commandes.png)

## Lancer le projet
flutter pub get
flutter run

## Auteur
Prénom Nom – Groupe
git add .
git commit -m "Partie 11 : intégration finale, README et captures"
git push

git tag v1.0
git push origin v1.0
  • Résultat attendu à la fin de la partie 11
Résultat attendu à la fin de la partie 11 du projet Le Délice
Application finale à quatre onglets et dépôt GitHub finalisé : un commit par partie, README et étiquette v1.0.
  1. Exercices complémentaires (bonus)

  • Paiement simulé : ajouter une colonne payee (migration version 3) et un écran « Encaisser » qui choisit le mode de paiement (espèces, carte). Un vrai paiement en ligne (Stripe, PayPal) demande un serveur : ne jamais placer de clé secrète dans l'application.
  • Notifications : avec le package flutter_local_notifications, afficher une notification locale quand une commande passe à « Servie ».
  • Architecture : déplacer l'état du panier et des commandes dans un ChangeNotifier avec Provider, ou utiliser le pattern BLoC, pour que tous les onglets se mettent à jour automatiquement.
  • Photos des plats : avec image_picker, prendre une photo du plat, la copier dans le dossier de l'application (path_provider) et enregistrer son chemin dans une nouvelle colonne photo.
  • Ticket de caisse : générer un PDF de la commande avec le package pdf.
  • Intégration continue : ajouter un workflow GitHub Actions qui lance flutter analyze à chaque git push, avec le badge dans le README.md.
  1. Compte rendu

  • À rendre :
    • le lien de votre dépôt GitHub le-delice, envoyé dès la partie 00 : c'est ce dépôt qui est corrigé, dans l'état de l'étiquette v1.0;
    • dans le dépôt, le fichier reponses.md: réponses aux questions Q0.5, Q1.2, Q1.4, Q1.5, Q2.5, Q3.2, Q3.4, Q4.1, Q4.8, Q5.6, Q6.4, Q6.7, Q6.10, Q7.1, Q7.5, Q7.6, Q8.1, Q9.4, Q9.5, Q10.1, Q10.3, Q11.2 et aux questions de réflexion ;
    • dans le dépôt, les fichiers sql/schema.sql (version 2) et sql/requetes.sql (toutes les requêtes testées dans DB Browser, commentées) ;
    • dans le dépôt, le README.md et les captures du dossier captures/, dont celles de DB Browser.
  • Barème indicatif (/20) :
Critère Points
Partie 00 – mise en place, .gitignore, dépôt GitHub 1
Partie 01 – cinq tables, données, tests RESTRICT / CASCADE / UNIQUE 1,5
Partie 02 – modèles, conversions, enum StatutCommande 1,5
Partie 03 – DatabaseHelper et vérification dans DB Browser 1
Partie 04 – menu par catégorie (jointure, filtre) et insertion 2
Partie 05 – formulaire, modification, disponibilité, suppression protégée 2
Partie 06 – panier et commande en transaction, test d'atomicité 2,5
Partie 07 – liste (LEFT JOIN, GROUP BY), détail, statut, cascade 2
Partie 08 – réservations, dates et heures, contrainte UNIQUE 2
Partie 09 – tableau de bord (agrégats, top des plats) 1,5
Partie 10 – migration sans perte de données, fidélité 1
Partie 11 – intégration finale et scénario de recette 0,5
Git / GitHub – un commit par partie avec un message clair, aucun .db ni secret versionné, README.md, étiquette v1.0 1,5
Total 20
Bonus +2 max
Question de réflexion (à rédiger en 5 lignes) :
Ce projet utilise trois mécanismes de SQLite pour protéger les données du restaurant : les clés étrangères (RESTRICT et CASCADE), la contrainte UNIQUE et les transactions. Pour chacun, citer la situation de l'application où il a évité une erreur.