Projet Flutter : application mobile Le Délice pour un restaurant avec SQLite
-
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 RESTRICTpour protéger les plats,ON DELETE CASCADEpour supprimer une commande avec ses lignes ; - écrire des modèles avec
factory fromMap(),toMap()etcopyWith(), 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,showTimePickeret la fonction SQLitedate(), et empêcher une double réservation avec une contrainteUNIQUE; - 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.mdet 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,FutureBuilderetNavigator.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
adbfourni avec Android Studio (vérifier avecadb devices).
- un compte GitHub gratuit et Git installé (vérifier avec
- Attention :
sqflitene fonctionne pas sur Flutter Web. Travaillez sur Android, iOS, macOS, Windows ou Linux, jamais dans Chrome.
-
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 |
-
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_deliceet supprimer le code du compteur dansmain.dart. Le nom du paquet Android seracom.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/etcaptures/et les fichiersreponses.mdetsql/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 exclutbuild/et.dart_tool/, puis ajouter les lignes ci-dessus (à droite). Répondre dansreponses.md: pourquoi versionne-t-onschema.sqlmais jamais les fichiers.db? - Q0.6. Dans
main.dart, créer unMaterialApp(titre « Le Délice »,useMaterial3: true,colorSchemeSeedde votre choix, par exemple un rouge brique) avec un simpleScaffoldet uneAppBar« 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-COMPTEpar 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/etpubspec.yamlapparaissent, 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
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.
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 » ?
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
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_tablepeut-il êtreNULLdanscommandesmais pas dansreservations? - pourquoi stocke-t-on
prix_unitairedanslignes_commandealors que le prix existe déjà dansplats? (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)?
- pourquoi
- 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.sqlavec un commentaire-- Q1.4.x:- le menu complet : nom du plat, prix et nom de la catégorie, trié par
c.ordrepuis par nom de plat ; - le détail de la commande 1 : nom du plat, quantité, prix unitaire et sous-total (
quantite * prix_unitaire AS sous_total) ; - le total recalculé de chaque commande avec
SUM(quantite * prix_unitaire)etGROUP BY commande_id: correspond-il à la colonnetotal? - les réservations du 25 octobre 2026 avec
WHERE date(date_heure) = '2026-10-25'.
- le menu complet : nom du plat, prix et nom de la catégorie, trié par
- 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
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
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
sqfliteet é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(intlservira à afficher les dates). En desktop Windows ou Linux, ajouter aussisqflite_common_ffiet initialiser SQLite dansmain()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 classePlatà partir du code ci-dessous, puis écrire vous-mêmetoMap()etcopyWith().
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'enumStatutCommandeet la classeCommande. 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) etReservation(id,nomClient,telephone,dateHeurede typeDateTime,nbCouverts,numeroTable), chacune avecfromMapettoMap. - Q2.5. Dans
models/ligne_panier.dart, créer la classe du panier. Elle n'a nifromMapnitoMap: 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
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
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
DatabaseHelperqui 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. LesCREATE TABLEsont ceux deschema.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é dansonCreate? Combien de fois sera-t-il inséré dans la vie de l'application ? Pourquoi lePRAGMA foreign_keys = ONest-il dansonConfigure? - 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
onCreateet les nombres affichés. Relancer l'application : le messageonCreateréapparaît-il ? - Q3.5. Récupérer le fichier
le_delice.dbet 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.dbadb 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
categoriescontient 4 lignes etplats9 lignes, et que les tablescommandes,lignes_commandeetreservationssont vides. Capture :captures/03_structure.png.
-
Résultat attendu à la fin de la partie 03
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
Étape 2 – DAO : écrire la méthode Dart correspondante.
Étape 3 – Test : l'appeler depuis un bouton de
É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.
É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.ordreetc.nom AS categorie_nom. Pourquoi l'aliascategorie_nomest-il indispensable ici ? - Q4.2 (Étape 2 – DAO). Dans
database/plat_dao.dart, créer la classePlatDaoavec 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, écrireFuture<List<Categorie>> lireCategories()triée parordre. - 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 sanscategorieId. - Q4.5 (Étape 5 – Interface). Créer
MenuScreen(StatefulWidget) qui charge le menu dansinitStateet l'affiche avec unFutureBuilder(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 format14.000 DTentrailing; - un plat indisponible est grisé et affiche une petite étiquette « Épuisé ».
- les plats sont regroupés par catégorie, avec un titre de section (« Entrées », « Plats »…) ; regrouper les lignes en Dart dans une
- 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 relancelireMenu(categorieId: …). Ajouter unRefreshIndicator. -
Ajouter un plat
- Q4.7 (Étapes 1 à 4). Écrire l'
INSERTd'un nouveau plat « Lablabi » (bol de pois chiches, 5,000 DT, catégorie Entrées) dans DB Browser, puis la méthodeFuture<int> insererPlat(Plat plat)dansPlatDao. La tester avec un bouton, puis vérifier dans DB Browser que le plat a l'id10. - Q4.8. Ajouter un bouton qui insère un plat avec un prix de
0, puis un autre aveccategorieId: 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
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
PlatFormScreenavec un paramètre optionnelPlat? plat(null = ajout, sinon modification). LeFormcontient : nom (obligatoire), description (maxLines: 3), prix (nombre strictement positif, virgule acceptée), catégorie (DropdownButtonFormField<int>rempli depuis la base) et unSwitchListTile« Disponible ». - Q5.2. Dans
MenuScreen, unFloatingActionButtonouvre 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'
UPDATEqui passe le prix de l'escalope à 17,500 DT. Écrire ensuite dansPlatDao:Future<int> modifierPlat(Plat plat)qui met à jour toutes les colonnes ;Future<int> changerDisponibilite(int id, bool disponible)qui ne modifie que la colonnedisponible.
- Les tester depuis
TestsScreenet 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
showModalBottomSheetavec trois actions :- « Modifier » : ouvre
PlatFormScreenpré-rempli, puis enregistre aveccopyWithetmodifierPlat; - « Marquer comme épuisé » / « Remettre au menu » selon l'état du plat : appelle
changerDisponibilite; - « Supprimer » : voir Q5.5.
- « Modifier » : ouvre
- Q5.5 (Delete). Écrire
Future<int> supprimerPlat(int id). L'action « Supprimer » affiche unAlertDialogde confirmation, puis supprime le plat. Entourer l'appel d'untry / catch: si uneDatabaseExceptioncontientFOREIGN KEY, afficher leSnackBar« 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.
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
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 variablefinal Map<int, LignePanier> _panier = {};(clé =iddu plat). Sur chaque plat disponible, unIconButton(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 widgetBadgeindiquant le nombre d'articles. Elle ouvrePanierScreenen lui passant le panier. - Q6.3.
PanierScreenaffiche chaque ligne (nom, boutons « − » / « + », sous-total), le total, unDropdownButtonFormField<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
INSERTdanscommandes, puis deuxINSERTdanslignes_commande. Quelle valeur faut-il connaître après le premierINSERTpour é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 danslignes_commandequi portent sonid. - 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
SnackBarde 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
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 JOINet non unINNER JOIN? PourquoiCOALESCE?
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 facultatifWHERE c.statut = ?;lireLignes(int commandeId)qui retourne le nom du plat, la quantité, le prix unitaire et le sous-total (jointure avecplats) ;changerStatut(int id, StatutCommande statut);supprimerCommande(int id).
- Q7.3 (Étape 5). Créer
CommandesScreen: une rangée deFilterChip(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 packageintl), le nombre d'articles, le total et une étiquette colorée selon le statut (orange, bleu, vert, gris). - Q7.4. Créer
CommandeDetailScreenqui reçoit l'idde 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_commandesans 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
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 deWHERE date(date_heure) = '2026-10-25'. Lequel préférez-vous et pourquoi ? Que retourneSELECT date('2026-10-25T20:00:00'), time('2026-10-25T20:00:00');? - Q8.2 (Étape 2 – DAO). Dans
database/reservation_dao.dart, écrireinsererReservation,lireReservationsDuJour(DateTime jour)(triées par heure) etsupprimerReservation. Pour la date du jour, utiliserDateFormat('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 ouvreshowTimePicker; 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).
- nom du client (obligatoire) et téléphone (8 chiffres,
- 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). UnDismissiblesupprime 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
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
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.sqlles requêtes suivantes, en ne comptant que les commandes servies du jour (date(date_commande) = date('now', 'localtime')) :- le chiffre d'affaires du jour (
COALESCE(SUM(total), 0)) ; - le nombre de commandes servies et le panier moyen (
AVG(total)) ; - le nombre de réservations du jour ;
- le top 5 des plats les plus vendus (toutes dates, commandes non annulées) :
- le chiffre d'affaires du jour (
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, utiliserSqflite.firstIntValue(); pour une somme,(resultat.first['total'] as num?)?.toDouble() ?? 0. - Q9.3 (Étape 5). Créer
DashboardScreen: quatreCarden 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
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 TEXTdans leCREATE TABLE commandesde_creerTableset 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 ajouteronUpgrade: _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
NULLdans la nouvelle colonne) et User Version vaut 2. Pourquoi écrit-onif (ancienne < 2)et nonif (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 ?
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
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
HomePageavec uneNavigationBarà quatre destinations : « Menu » (Icons.restaurant_menu), « Commandes » (Icons.receipt_long), « Réservations » (Icons.event_seat) et « Tableau de bord » (Icons.insights). Supprimertests_screen.dartet tous ses appels. - Q11.2. Dans l'
AppBardu tableau de bord, ajouter un menu avec « Réinitialiser la base » : après confirmation, fermer la base, la supprimer avecdeleteDatabase(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_commanded'une commande, User Version = 2). -
Finaliser le dépôt GitHub
- Q11.5. Rédiger le
README.md(modèle ci-dessous), vérifier quereponses.md,sql/schema.sqletsql/requetes.sqlsont complets, et qu'aucun fichier.dbni aucun secret n'est versionné (git ls-files | findstr .dbsous Windows,git ls-files | grep .dbsous Linux/macOS). - Q11.6. Faire le dernier commit, poser l'étiquette
v1.0et 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


## 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
v1.0. -
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
ChangeNotifieravecProvider, 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 colonnephoto. - 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à chaquegit push, avec le badge dans leREADME.md.
-
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'étiquettev1.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) etsql/requetes.sql(toutes les requêtes testées dans DB Browser, commentées) ; - dans le dépôt, le
README.mdet les captures du dossiercaptures/, dont celles de DB Browser.
- le lien de votre dépôt GitHub
- 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 (
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.