last commit gestion commande
This commit is contained in:
parent
1aceb5669a
commit
435baa3b4f
@ -3,15 +3,14 @@ import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:youmazgestion/Views/HandleProduct.dart';
|
||||
import 'package:youmazgestion/Views/RoleListPage.dart';
|
||||
import 'package:youmazgestion/Views/commandManagement.dart';
|
||||
import 'package:youmazgestion/Views/historique.dart';
|
||||
import 'package:youmazgestion/Views/addProduct.dart';
|
||||
import 'package:youmazgestion/Views/bilanMois.dart';
|
||||
import 'package:youmazgestion/Views/gestionProduct.dart';
|
||||
import 'package:youmazgestion/Views/gestionStock.dart';
|
||||
import 'package:youmazgestion/Views/listUser.dart';
|
||||
import 'package:youmazgestion/Views/loginPage.dart';
|
||||
import 'package:youmazgestion/Views/newCommand.dart';
|
||||
import 'package:youmazgestion/Views/registrationPage.dart';
|
||||
import 'package:youmazgestion/Views/gestionRole.dart';
|
||||
import 'package:youmazgestion/accueil.dart';
|
||||
import 'package:youmazgestion/controller/userController.dart';
|
||||
|
||||
@ -31,201 +30,230 @@ class CustomDrawer extends StatelessWidget {
|
||||
return Drawer(
|
||||
backgroundColor: Colors.white,
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
// Header avec informations utilisateur
|
||||
GetBuilder<UserController>(
|
||||
builder: (controller) => UserAccountsDrawerHeader(
|
||||
accountEmail: Text(controller.email),
|
||||
accountName: Text(controller.name),
|
||||
currentAccountPicture: const CircleAvatar(
|
||||
backgroundImage: AssetImage("assets/youmaz2.png"),
|
||||
),
|
||||
builder: (controller) => Container(
|
||||
padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.white, Color.fromARGB(255, 4, 54, 95)],
|
||||
colors: [Color.fromARGB(255, 4, 54, 95), Colors.blue],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundImage: AssetImage("assets/youmaz2.png"),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
controller.name,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
controller.email,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
controller.role,
|
||||
style: const TextStyle(
|
||||
color: Colors.white70,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.home),
|
||||
iconColor: Colors.lightBlueAccent,
|
||||
title: const Text("Accueil"),
|
||||
onTap: () async {
|
||||
bool hasPermission = await userController.hasPermission('view', '/accueil');
|
||||
if (hasPermission) {
|
||||
Get.to(const AccueilPage());
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Accès refusé",
|
||||
"Vous n'avez pas les droits pour accéder à cette page",
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
icon: const Icon(Icons.error),
|
||||
duration: const Duration(seconds: 3),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.person_add),
|
||||
iconColor: Colors.green,
|
||||
title: const Text("Ajouter un utilisateur"),
|
||||
onTap: () async {
|
||||
bool hasPermission = await userController.hasPermission('create', '/ajouter-utilisateur');
|
||||
if (hasPermission) {
|
||||
Get.to(const RegistrationPage());
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Accès refusé",
|
||||
"Vous n'avez pas les droits pour ajouter un utilisateur",
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
icon: const Icon(Icons.error),
|
||||
duration: const Duration(seconds: 3),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.supervised_user_circle),
|
||||
iconColor: const Color.fromARGB(255, 4, 54, 95),
|
||||
title: const Text("Modifier/Supprimer un utilisateur"),
|
||||
onTap: () async {
|
||||
bool hasPermission = await userController.hasPermission('update', '/modifier-utilisateur');
|
||||
if (hasPermission) {
|
||||
Get.to(const ListUserPage());
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Accès refusé",
|
||||
"Vous n'avez pas les droits pour modifier/supprimer un utilisateur",
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
icon: const Icon(Icons.error),
|
||||
duration: const Duration(seconds: 3),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add),
|
||||
iconColor: Colors.indigoAccent,
|
||||
title: const Text("Gestion des produit"),
|
||||
onTap: () async {
|
||||
bool hasPermission = await userController.hasPermission('create', '/ajouter-produit');
|
||||
if (hasPermission) {
|
||||
Get.to(() => const ProductManagementPage());
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Accès refusé",
|
||||
"Vous n'avez pas les droits pour ajouter un produit",
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
icon: const Icon(Icons.error),
|
||||
duration: const Duration(seconds: 3),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
// Section Accueil
|
||||
_buildDrawerItem(
|
||||
icon: Icons.home,
|
||||
title: "Accueil",
|
||||
color: Colors.blue,
|
||||
permissionAction: 'view',
|
||||
permissionRoute: '/accueil',
|
||||
onTap: () => Get.to(const AccueilPage()),
|
||||
),
|
||||
|
||||
ListTile(
|
||||
leading: const Icon(Icons.bar_chart),
|
||||
title: const Text("Bilan"),
|
||||
onTap: () async {
|
||||
bool hasPermission = await userController.hasPermission('read', '/bilan');
|
||||
if (hasPermission) {
|
||||
Get.to(const BilanMois());
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Accès refusé",
|
||||
"Vous n'avez pas les droits pour accéder au bilan",
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
icon: const Icon(Icons.error_outline_outlined),
|
||||
duration: const Duration(seconds: 3),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
},
|
||||
// Section Utilisateurs
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
||||
child: Text(
|
||||
"GESTION UTILISATEURS",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.warning_amber),
|
||||
title: const Text("Gérer les rôles"),
|
||||
onTap: () async {
|
||||
bool hasPermission = await userController.hasPermission('admin', '/gerer-roles');
|
||||
if (hasPermission) {
|
||||
Get.to(const RoleListPage());
|
||||
print("permission accepted");
|
||||
} else {
|
||||
print("permission not accepted for" +userController.username);
|
||||
Get.snackbar(
|
||||
"Accès refusé ",
|
||||
"Vous n'avez pas les droits pour gérer les rôles",
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
icon: const Icon(Icons.error_outline_outlined),
|
||||
duration: const Duration(seconds: 3),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
},
|
||||
_buildDrawerItem(
|
||||
icon: Icons.person_add,
|
||||
title: "Ajouter un utilisateur",
|
||||
color: Colors.green,
|
||||
permissionAction: 'create',
|
||||
permissionRoute: '/ajouter-utilisateur',
|
||||
onTap: () => Get.to(const RegistrationPage()),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.inventory),
|
||||
iconColor: Colors.blueAccent,
|
||||
title: const Text("Gestion de stock"),
|
||||
onTap: () async {
|
||||
bool hasPermission = await userController.hasPermission('update', '/gestion-stock');
|
||||
if (hasPermission) {
|
||||
Get.to(const GestionStockPage());
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Accès refusé",
|
||||
"Vous n'avez pas les droits pour accéder à la gestion de stock",
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
icon: const Icon(Icons.error),
|
||||
duration: const Duration(seconds: 3),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
},
|
||||
_buildDrawerItem(
|
||||
icon: Icons.supervised_user_circle,
|
||||
title: "Gérer les utilisateurs",
|
||||
color: Color.fromARGB(255, 4, 54, 95),
|
||||
permissionAction: 'update',
|
||||
permissionRoute: '/modifier-utilisateur',
|
||||
onTap: () => Get.to(const ListUserPage()),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.history),
|
||||
iconColor: Colors.blue,
|
||||
title: const Text("Historique"),
|
||||
onTap: () {
|
||||
Get.to(HistoryPage());
|
||||
},
|
||||
|
||||
// Section Produits
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
||||
child: Text(
|
||||
"GESTION PRODUITS",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.logout),
|
||||
iconColor: Colors.red,
|
||||
title: const Text("Déconnexion"),
|
||||
_buildDrawerItem(
|
||||
icon: Icons.inventory,
|
||||
title: "Gestion des produits",
|
||||
color: Colors.indigoAccent,
|
||||
permissionAction: 'create',
|
||||
permissionRoute: '/ajouter-produit',
|
||||
onTap: () => Get.to(const ProductManagementPage()),
|
||||
),
|
||||
_buildDrawerItem(
|
||||
icon: Icons.storage,
|
||||
title: "Gestion de stock",
|
||||
color: Colors.blueAccent,
|
||||
permissionAction: 'update',
|
||||
permissionRoute: '/gestion-stock',
|
||||
onTap: () => Get.to(const GestionStockPage()),
|
||||
),
|
||||
|
||||
// Section Commandes
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
||||
child: Text(
|
||||
"GESTION COMMANDES",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildDrawerItem(
|
||||
icon: Icons.add_shopping_cart,
|
||||
title: "Nouvelle commande",
|
||||
color: Colors.orange,
|
||||
permissionAction: 'create',
|
||||
permissionRoute: '/nouvelle-commande',
|
||||
onTap: () => Get.to(const NouvelleCommandePage()),
|
||||
),
|
||||
_buildDrawerItem(
|
||||
icon: Icons.list_alt,
|
||||
title: "Gérer les commandes",
|
||||
color: Colors.deepPurple,
|
||||
permissionAction: 'manage',
|
||||
permissionRoute: '/gerer-commandes',
|
||||
onTap: () => Get.to(const GestionCommandesPage()),
|
||||
),
|
||||
|
||||
// Section Rapports
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
||||
child: Text(
|
||||
"RAPPORTS",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildDrawerItem(
|
||||
icon: Icons.bar_chart,
|
||||
title: "Bilan mensuel",
|
||||
color: Colors.teal,
|
||||
permissionAction: 'read',
|
||||
permissionRoute: '/bilan',
|
||||
onTap: () => Get.to(const BilanMois()),
|
||||
),
|
||||
_buildDrawerItem(
|
||||
icon: Icons.history,
|
||||
title: "Historique",
|
||||
color: Colors.blue,
|
||||
permissionAction: 'read',
|
||||
permissionRoute: '/historique',
|
||||
onTap: () => Get.to(HistoryPage()),
|
||||
),
|
||||
|
||||
// Section Administration
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
||||
child: Text(
|
||||
"ADMINISTRATION",
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildDrawerItem(
|
||||
icon: Icons.admin_panel_settings,
|
||||
title: "Gérer les rôles",
|
||||
color: Colors.redAccent,
|
||||
permissionAction: 'admin',
|
||||
permissionRoute: '/gerer-roles',
|
||||
onTap: () => Get.to(const RoleListPage()),
|
||||
),
|
||||
|
||||
// Déconnexion
|
||||
const Divider(),
|
||||
_buildDrawerItem(
|
||||
icon: Icons.logout,
|
||||
title: "Déconnexion",
|
||||
color: Colors.red,
|
||||
onTap: () {
|
||||
Get.defaultDialog(
|
||||
title: "Déconnexion",
|
||||
content: const Text("Voulez-vous vraiment vous déconnecter ?"),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("Non"),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
child: const Text("Oui"),
|
||||
onPressed: () {
|
||||
clearUserData();
|
||||
Get.offAll(const LoginPage());
|
||||
},
|
||||
),
|
||||
ElevatedButton(
|
||||
child: const Text("Non"),
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@ -234,4 +262,41 @@ class CustomDrawer extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDrawerItem({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required Color color,
|
||||
String? permissionAction,
|
||||
String? permissionRoute,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: color),
|
||||
title: Text(title),
|
||||
trailing: permissionAction != null
|
||||
? const Icon(Icons.chevron_right, color: Colors.grey)
|
||||
: null,
|
||||
onTap: () async {
|
||||
if (permissionAction != null && permissionRoute != null) {
|
||||
bool hasPermission = await userController.hasPermission(permissionAction, permissionRoute);
|
||||
if (hasPermission) {
|
||||
onTap();
|
||||
} else {
|
||||
Get.snackbar(
|
||||
"Accès refusé",
|
||||
"Vous n'avez pas les droits pour accéder à cette page",
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
icon: const Icon(Icons.error),
|
||||
duration: const Duration(seconds: 3),
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
onTap();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
191
lib/Models/Client.dart
Normal file
191
lib/Models/Client.dart
Normal file
@ -0,0 +1,191 @@
|
||||
// Models/client.dart
|
||||
class Client {
|
||||
final int? id;
|
||||
final String nom;
|
||||
final String prenom;
|
||||
final String email;
|
||||
final String telephone;
|
||||
final String? adresse;
|
||||
final DateTime dateCreation;
|
||||
final bool actif;
|
||||
|
||||
Client({
|
||||
this.id,
|
||||
required this.nom,
|
||||
required this.prenom,
|
||||
required this.email,
|
||||
required this.telephone,
|
||||
this.adresse,
|
||||
required this.dateCreation,
|
||||
this.actif = true,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'nom': nom,
|
||||
'prenom': prenom,
|
||||
'email': email,
|
||||
'telephone': telephone,
|
||||
'adresse': adresse,
|
||||
'dateCreation': dateCreation.toIso8601String(),
|
||||
'actif': actif ? 1 : 0,
|
||||
};
|
||||
}
|
||||
|
||||
factory Client.fromMap(Map<String, dynamic> map) {
|
||||
return Client(
|
||||
id: map['id'],
|
||||
nom: map['nom'],
|
||||
prenom: map['prenom'],
|
||||
email: map['email'],
|
||||
telephone: map['telephone'],
|
||||
adresse: map['adresse'],
|
||||
dateCreation: DateTime.parse(map['dateCreation']),
|
||||
actif: map['actif'] == 1,
|
||||
);
|
||||
}
|
||||
|
||||
String get nomComplet => '$prenom $nom';
|
||||
}
|
||||
|
||||
// Models/commande.dart
|
||||
enum StatutCommande {
|
||||
enAttente,
|
||||
confirmee,
|
||||
enPreparation,
|
||||
expediee,
|
||||
livree,
|
||||
annulee
|
||||
}
|
||||
|
||||
class Commande {
|
||||
final int? id;
|
||||
final int clientId;
|
||||
final DateTime dateCommande;
|
||||
final StatutCommande statut;
|
||||
final double montantTotal;
|
||||
final String? notes;
|
||||
final DateTime? dateLivraison;
|
||||
|
||||
// Données du client (pour les jointures)
|
||||
final String? clientNom;
|
||||
final String? clientPrenom;
|
||||
final String? clientEmail;
|
||||
|
||||
Commande({
|
||||
this.id,
|
||||
required this.clientId,
|
||||
required this.dateCommande,
|
||||
this.statut = StatutCommande.enAttente,
|
||||
required this.montantTotal,
|
||||
this.notes,
|
||||
this.dateLivraison,
|
||||
this.clientNom,
|
||||
this.clientPrenom,
|
||||
this.clientEmail,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'clientId': clientId,
|
||||
'dateCommande': dateCommande.toIso8601String(),
|
||||
'statut': statut.index,
|
||||
'montantTotal': montantTotal,
|
||||
'notes': notes,
|
||||
'dateLivraison': dateLivraison?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
factory Commande.fromMap(Map<String, dynamic> map) {
|
||||
return Commande(
|
||||
id: map['id'],
|
||||
clientId: map['clientId'],
|
||||
dateCommande: DateTime.parse(map['dateCommande']),
|
||||
statut: StatutCommande.values[map['statut']],
|
||||
montantTotal: map['montantTotal'].toDouble(),
|
||||
notes: map['notes'],
|
||||
dateLivraison: map['dateLivraison'] != null
|
||||
? DateTime.parse(map['dateLivraison'])
|
||||
: null,
|
||||
clientNom: map['clientNom'],
|
||||
clientPrenom: map['clientPrenom'],
|
||||
clientEmail: map['clientEmail'],
|
||||
);
|
||||
}
|
||||
|
||||
String get statutLibelle {
|
||||
switch (statut) {
|
||||
case StatutCommande.enAttente:
|
||||
return 'En attente';
|
||||
case StatutCommande.confirmee:
|
||||
return 'Confirmée';
|
||||
case StatutCommande.enPreparation:
|
||||
return 'En préparation';
|
||||
case StatutCommande.expediee:
|
||||
return 'Expédiée';
|
||||
case StatutCommande.livree:
|
||||
return 'Livrée';
|
||||
case StatutCommande.annulee:
|
||||
return 'Annulée';
|
||||
}
|
||||
}
|
||||
|
||||
String get clientNomComplet =>
|
||||
clientPrenom != null && clientNom != null
|
||||
? '$clientPrenom $clientNom'
|
||||
: 'Client inconnu';
|
||||
}
|
||||
|
||||
// Models/detail_commande.dart
|
||||
class DetailCommande {
|
||||
final int? id;
|
||||
final int commandeId;
|
||||
final int produitId;
|
||||
final int quantite;
|
||||
final double prixUnitaire;
|
||||
final double sousTotal;
|
||||
|
||||
// Données du produit (pour les jointures)
|
||||
final String? produitNom;
|
||||
final String? produitImage;
|
||||
final String? produitReference;
|
||||
|
||||
DetailCommande({
|
||||
this.id,
|
||||
required this.commandeId,
|
||||
required this.produitId,
|
||||
required this.quantite,
|
||||
required this.prixUnitaire,
|
||||
required this.sousTotal,
|
||||
this.produitNom,
|
||||
this.produitImage,
|
||||
this.produitReference,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'commandeId': commandeId,
|
||||
'produitId': produitId,
|
||||
'quantite': quantite,
|
||||
'prixUnitaire': prixUnitaire,
|
||||
'sousTotal': sousTotal,
|
||||
};
|
||||
}
|
||||
|
||||
factory DetailCommande.fromMap(Map<String, dynamic> map) {
|
||||
return DetailCommande(
|
||||
id: map['id'],
|
||||
commandeId: map['commandeId'],
|
||||
produitId: map['produitId'],
|
||||
quantite: map['quantite'],
|
||||
prixUnitaire: map['prixUnitaire'].toDouble(),
|
||||
sousTotal: map['sousTotal'].toDouble(),
|
||||
produitNom: map['produitNom'],
|
||||
produitImage: map['produitImage'],
|
||||
produitReference: map['produitReference'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -142,37 +142,78 @@ class AppDatabase {
|
||||
|
||||
}
|
||||
|
||||
Future<void> insertDefaultPermissions() async {
|
||||
final db = await database;
|
||||
final existing = await db.query('permissions');
|
||||
if (existing.isEmpty) {
|
||||
await db.insert('permissions', {'name': 'view'});
|
||||
await db.insert('permissions', {'name': 'create'});
|
||||
await db.insert('permissions', {'name': 'update'});
|
||||
await db.insert('permissions', {'name': 'delete'});
|
||||
await db.insert('permissions', {'name': 'admin'});
|
||||
print("Permissions par défaut insérées");
|
||||
Future<void> insertDefaultPermissions() async {
|
||||
final db = await database;
|
||||
final existing = await db.query('permissions');
|
||||
if (existing.isEmpty) {
|
||||
await db.insert('permissions', {'name': 'view'});
|
||||
await db.insert('permissions', {'name': 'create'});
|
||||
await db.insert('permissions', {'name': 'update'});
|
||||
await db.insert('permissions', {'name': 'delete'});
|
||||
await db.insert('permissions', {'name': 'admin'});
|
||||
await db.insert('permissions', {'name': 'manage'}); // Nouvelle permission
|
||||
await db.insert('permissions', {'name': 'read'}); // Nouvelle permission
|
||||
print("Permissions par défaut insérées");
|
||||
} else {
|
||||
// Vérifier et ajouter les nouvelles permissions si elles n'existent pas
|
||||
final newPermissions = ['manage', 'read'];
|
||||
for (var permission in newPermissions) {
|
||||
final existingPermission = await db.query('permissions', where: 'name = ?', whereArgs: [permission]);
|
||||
if (existingPermission.isEmpty) {
|
||||
await db.insert('permissions', {'name': permission});
|
||||
print("Permission ajoutée: $permission");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> insertDefaultMenus() async {
|
||||
final db = await database;
|
||||
final existingMenus = await db.query('menu');
|
||||
final db = await database;
|
||||
final existingMenus = await db.query('menu');
|
||||
|
||||
if (existingMenus.isEmpty) {
|
||||
await db.insert('menu', {'name': 'Accueil', 'route': '/accueil'});
|
||||
await db.insert('menu', {'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'});
|
||||
await db.insert('menu', {'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'});
|
||||
await db.insert('menu', {'name': 'Ajouter un produit', 'route': '/ajouter-produit'});
|
||||
await db.insert('menu', {'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'});
|
||||
await db.insert('menu', {'name': 'Bilan', 'route': '/bilan'});
|
||||
await db.insert('menu', {'name': 'Gérer les rôles', 'route': '/gerer-roles'});
|
||||
await db.insert('menu', {'name': 'Gestion de stock', 'route': '/gestion-stock'});
|
||||
await db.insert('menu', {'name': 'Historique', 'route': '/historique'});
|
||||
await db.insert('menu', {'name': 'Déconnexion', 'route': '/deconnexion'});
|
||||
print("Menus par défaut insérés");
|
||||
if (existingMenus.isEmpty) {
|
||||
// Menus existants
|
||||
await db.insert('menu', {'name': 'Accueil', 'route': '/accueil'});
|
||||
await db.insert('menu', {'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'});
|
||||
await db.insert('menu', {'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'});
|
||||
await db.insert('menu', {'name': 'Ajouter un produit', 'route': '/ajouter-produit'});
|
||||
await db.insert('menu', {'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'});
|
||||
await db.insert('menu', {'name': 'Bilan', 'route': '/bilan'});
|
||||
await db.insert('menu', {'name': 'Gérer les rôles', 'route': '/gerer-roles'});
|
||||
await db.insert('menu', {'name': 'Gestion de stock', 'route': '/gestion-stock'});
|
||||
await db.insert('menu', {'name': 'Historique', 'route': '/historique'});
|
||||
await db.insert('menu', {'name': 'Déconnexion', 'route': '/deconnexion'});
|
||||
|
||||
// Nouveaux menus ajoutés
|
||||
await db.insert('menu', {'name': 'Nouvelle commande', 'route': '/nouvelle-commande'});
|
||||
await db.insert('menu', {'name': 'Gérer les commandes', 'route': '/gerer-commandes'});
|
||||
|
||||
print("Menus par défaut insérés");
|
||||
} else {
|
||||
// Si des menus existent déjà, vérifier et ajouter les nouveaux menus manquants
|
||||
await _addMissingMenus(db);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addMissingMenus(Database db) async {
|
||||
final menusToAdd = [
|
||||
{'name': 'Nouvelle commande', 'route': '/nouvelle-commande'},
|
||||
{'name': 'Gérer les commandes', 'route': '/gerer-commandes'},
|
||||
];
|
||||
|
||||
for (var menu in menusToAdd) {
|
||||
final existing = await db.query(
|
||||
'menu',
|
||||
where: 'route = ?',
|
||||
whereArgs: [menu['route']],
|
||||
);
|
||||
|
||||
if (existing.isEmpty) {
|
||||
await db.insert('menu', menu);
|
||||
print("Menu ajouté: ${menu['name']}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> insertDefaultRoles() async {
|
||||
final db = await database;
|
||||
@ -197,69 +238,94 @@ class AppDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
// Assigner quelques permissions à l'Admin et à l'User
|
||||
final viewPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['view']);
|
||||
final createPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['create']);
|
||||
final updatePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['update']);
|
||||
|
||||
if (viewPermission.isNotEmpty) {
|
||||
await db.insert('role_menu_permissions', {
|
||||
'role_id': adminRoleId,
|
||||
'menu_id': 1, // Assurez-vous que l'ID du menu est correct
|
||||
'permission_id': viewPermission.first['id'],
|
||||
});
|
||||
await db.insert('role_menu_permissions', {
|
||||
'role_id': userRoleId,
|
||||
'menu_id': 1, // Assurez-vous que l'ID du menu est correct
|
||||
'permission_id': viewPermission.first['id'],
|
||||
});
|
||||
}
|
||||
|
||||
if (createPermission.isNotEmpty) {
|
||||
await db.insert('role_menu_permissions', {
|
||||
'role_id': adminRoleId,
|
||||
'menu_id': 1, // Assurez-vous que l'ID du menu est correct
|
||||
'permission_id': createPermission.first['id'],
|
||||
});
|
||||
}
|
||||
|
||||
if (updatePermission.isNotEmpty) {
|
||||
await db.insert('role_menu_permissions', {
|
||||
'role_id': adminRoleId,
|
||||
'menu_id': 1, // Assurez-vous que l'ID du menu est correct
|
||||
'permission_id': updatePermission.first['id'],
|
||||
});
|
||||
}
|
||||
// Assigner quelques permissions à l'Admin et à l'User pour les nouveaux menus
|
||||
await _assignBasicPermissionsToRoles(db, adminRoleId, userRoleId);
|
||||
|
||||
print("Rôles par défaut créés et permissions assignées");
|
||||
} else {
|
||||
// Si les rôles existent déjà, vérifier et ajouter les permissions manquantes pour le Super Admin
|
||||
final superAdminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Super Admin']);
|
||||
if (superAdminRole.isNotEmpty) {
|
||||
final superAdminRoleId = superAdminRole.first['id'] as int;
|
||||
final permissions = await db.query('permissions');
|
||||
final menus = await db.query('menu');
|
||||
// Si les rôles existent déjà, vérifier et ajouter les permissions manquantes
|
||||
await _updateExistingRolePermissions(db);
|
||||
}
|
||||
}
|
||||
// Nouvelle méthode pour assigner les permissions de base aux nouveaux menus
|
||||
Future<void> _assignBasicPermissionsToRoles(Database db, int adminRoleId, int userRoleId) async {
|
||||
final viewPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['view']);
|
||||
final createPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['create']);
|
||||
final updatePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['update']);
|
||||
final managePermission = await db.query('permissions', where: 'name = ?', whereArgs: ['manage']);
|
||||
|
||||
// Vérifier et ajouter les permissions manquantes pour le Super Admin
|
||||
for (var menu in menus) {
|
||||
for (var permission in permissions) {
|
||||
final existingPermission = await db.query(
|
||||
'role_menu_permissions',
|
||||
where: 'role_id = ? AND menu_id = ? AND permission_id = ?',
|
||||
whereArgs: [superAdminRoleId, menu['id'], permission['id']],
|
||||
);
|
||||
if (existingPermission.isEmpty) {
|
||||
await db.insert('role_menu_permissions', {
|
||||
'role_id': superAdminRoleId,
|
||||
'menu_id': menu['id'],
|
||||
'permission_id': permission['id'],
|
||||
});
|
||||
}
|
||||
// Récupérer les IDs des nouveaux menus
|
||||
final nouvelleCommandeMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/nouvelle-commande']);
|
||||
final gererCommandesMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/gerer-commandes']);
|
||||
|
||||
if (nouvelleCommandeMenu.isNotEmpty && createPermission.isNotEmpty) {
|
||||
// Admin peut créer de nouvelles commandes
|
||||
await db.insert('role_menu_permissions', {
|
||||
'role_id': adminRoleId,
|
||||
'menu_id': nouvelleCommandeMenu.first['id'],
|
||||
'permission_id': createPermission.first['id'],
|
||||
});
|
||||
|
||||
// User peut aussi créer de nouvelles commandes
|
||||
await db.insert('role_menu_permissions', {
|
||||
'role_id': userRoleId,
|
||||
'menu_id': nouvelleCommandeMenu.first['id'],
|
||||
'permission_id': createPermission.first['id'],
|
||||
});
|
||||
}
|
||||
|
||||
if (gererCommandesMenu.isNotEmpty && managePermission.isNotEmpty) {
|
||||
// Admin peut gérer les commandes
|
||||
await db.insert('role_menu_permissions', {
|
||||
'role_id': adminRoleId,
|
||||
'menu_id': gererCommandesMenu.first['id'],
|
||||
'permission_id': managePermission.first['id'],
|
||||
});
|
||||
}
|
||||
|
||||
if (gererCommandesMenu.isNotEmpty && viewPermission.isNotEmpty) {
|
||||
// User peut voir les commandes
|
||||
await db.insert('role_menu_permissions', {
|
||||
'role_id': userRoleId,
|
||||
'menu_id': gererCommandesMenu.first['id'],
|
||||
'permission_id': viewPermission.first['id'],
|
||||
});
|
||||
}
|
||||
}
|
||||
Future<void> _updateExistingRolePermissions(Database db) async {
|
||||
final superAdminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Super Admin']);
|
||||
if (superAdminRole.isNotEmpty) {
|
||||
final superAdminRoleId = superAdminRole.first['id'] as int;
|
||||
final permissions = await db.query('permissions');
|
||||
final menus = await db.query('menu');
|
||||
|
||||
// Vérifier et ajouter les permissions manquantes pour le Super Admin sur tous les menus
|
||||
for (var menu in menus) {
|
||||
for (var permission in permissions) {
|
||||
final existingPermission = await db.query(
|
||||
'role_menu_permissions',
|
||||
where: 'role_id = ? AND menu_id = ? AND permission_id = ?',
|
||||
whereArgs: [superAdminRoleId, menu['id'], permission['id']],
|
||||
);
|
||||
if (existingPermission.isEmpty) {
|
||||
await db.insert('role_menu_permissions', {
|
||||
'role_id': superAdminRoleId,
|
||||
'menu_id': menu['id'],
|
||||
'permission_id': permission['id'],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
print("Permissions manquantes ajoutées pour le Super Admin");
|
||||
}
|
||||
|
||||
// Assigner les permissions de base aux autres rôles pour les nouveaux menus
|
||||
final adminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Admin']);
|
||||
final userRole = await db.query('roles', where: 'designation = ?', whereArgs: ['User']);
|
||||
|
||||
if (adminRole.isNotEmpty && userRole.isNotEmpty) {
|
||||
await _assignBasicPermissionsToRoles(db, adminRole.first['id'] as int, userRole.first['id'] as int);
|
||||
}
|
||||
|
||||
print("Permissions mises à jour pour tous les rôles");
|
||||
}
|
||||
}
|
||||
|
||||
@ -576,7 +642,7 @@ Future<void> deleteDatabaseFile() async {
|
||||
final file = File(path);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
print("Base de données supprimée");
|
||||
print("Base de données utilisateur supprimée");
|
||||
}
|
||||
}
|
||||
Future<void> assignRoleMenuPermission(int roleId, int menuId, int permissionId) async {
|
||||
|
||||
@ -5,6 +5,8 @@ import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import '../Models/produit.dart';
|
||||
import '../Models/client.dart';
|
||||
|
||||
|
||||
class ProductDatabase {
|
||||
static final ProductDatabase instance = ProductDatabase._init();
|
||||
@ -25,6 +27,8 @@ class ProductDatabase {
|
||||
Future<void> initDatabase() async {
|
||||
_database = await _initDB('products2.db');
|
||||
await _createDB(_database, 1);
|
||||
await _insertDefaultClients();
|
||||
await _insertDefaultCommandes();
|
||||
}
|
||||
|
||||
Future<Database> _initDB(String filePath) async {
|
||||
@ -33,74 +37,129 @@ class ProductDatabase {
|
||||
|
||||
bool dbExists = await File(path).exists();
|
||||
if (!dbExists) {
|
||||
ByteData data = await rootBundle.load('assets/database/$filePath');
|
||||
List<int> bytes =
|
||||
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
|
||||
await File(path).writeAsBytes(bytes);
|
||||
try {
|
||||
ByteData data = await rootBundle.load('assets/database/$filePath');
|
||||
List<int> bytes =
|
||||
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
|
||||
await File(path).writeAsBytes(bytes);
|
||||
} catch (e) {
|
||||
print('Pas de fichier DB dans assets, création nouvelle DB');
|
||||
}
|
||||
}
|
||||
|
||||
return await databaseFactoryFfi.openDatabase(path);
|
||||
}
|
||||
|
||||
Future<void> _createDB(Database db, int version) async {
|
||||
// Récupère la liste des colonnes de la table "products"
|
||||
final tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'");
|
||||
final tableNames = tables.map((row) => row['name'] as String).toList();
|
||||
Future<void> _createDB(Database db, int version) async {
|
||||
final tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'");
|
||||
final tableNames = tables.map((row) => row['name'] as String).toList();
|
||||
|
||||
// Si la table "products" n'existe pas encore, on la crée entièrement
|
||||
if (!tableNames.contains('products')) {
|
||||
await db.execute('''
|
||||
CREATE TABLE products(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT,
|
||||
price REAL,
|
||||
image TEXT,
|
||||
category TEXT,
|
||||
stock INTEGER,
|
||||
description TEXT,
|
||||
qrCode TEXT,
|
||||
reference TEXT
|
||||
)
|
||||
''');
|
||||
print("Table 'products' créée avec toutes les colonnes.");
|
||||
} else {
|
||||
// Vérifie si les colonnes "description", "qrCode" et "reference" existent déjà
|
||||
// Table products (existante avec améliorations)
|
||||
if (!tableNames.contains('products')) {
|
||||
await db.execute('''
|
||||
CREATE TABLE products(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
price REAL NOT NULL,
|
||||
image TEXT,
|
||||
category TEXT NOT NULL,
|
||||
stock INTEGER NOT NULL DEFAULT 0,
|
||||
description TEXT,
|
||||
qrCode TEXT,
|
||||
reference TEXT UNIQUE
|
||||
)
|
||||
''');
|
||||
print("Table 'products' créée.");
|
||||
} else {
|
||||
// Vérifier et ajouter les colonnes manquantes
|
||||
await _updateProductsTable(db);
|
||||
}
|
||||
|
||||
// Table clients
|
||||
if (!tableNames.contains('clients')) {
|
||||
await db.execute('''
|
||||
CREATE TABLE clients(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nom TEXT NOT NULL,
|
||||
prenom TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
telephone TEXT NOT NULL,
|
||||
adresse TEXT,
|
||||
dateCreation TEXT NOT NULL,
|
||||
actif INTEGER NOT NULL DEFAULT 1
|
||||
)
|
||||
''');
|
||||
print("Table 'clients' créée.");
|
||||
}
|
||||
|
||||
// Table commandes
|
||||
if (!tableNames.contains('commandes')) {
|
||||
await db.execute('''
|
||||
CREATE TABLE commandes(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
clientId INTEGER NOT NULL,
|
||||
dateCommande TEXT NOT NULL,
|
||||
statut INTEGER NOT NULL DEFAULT 0,
|
||||
montantTotal REAL NOT NULL,
|
||||
notes TEXT,
|
||||
dateLivraison TEXT,
|
||||
FOREIGN KEY (clientId) REFERENCES clients(id) ON DELETE CASCADE
|
||||
)
|
||||
''');
|
||||
print("Table 'commandes' créée.");
|
||||
}
|
||||
|
||||
// Table détails commandes
|
||||
if (!tableNames.contains('details_commandes')) {
|
||||
await db.execute('''
|
||||
CREATE TABLE details_commandes(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
commandeId INTEGER NOT NULL,
|
||||
produitId INTEGER NOT NULL,
|
||||
quantite INTEGER NOT NULL,
|
||||
prixUnitaire REAL NOT NULL,
|
||||
sousTotal REAL NOT NULL,
|
||||
FOREIGN KEY (commandeId) REFERENCES commandes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (produitId) REFERENCES products(id) ON DELETE CASCADE
|
||||
)
|
||||
''');
|
||||
print("Table 'details_commandes' créée.");
|
||||
}
|
||||
|
||||
// Créer les index pour optimiser les performances
|
||||
await _createIndexes(db);
|
||||
}
|
||||
|
||||
Future<void> _updateProductsTable(Database db) async {
|
||||
final columns = await db.rawQuery('PRAGMA table_info(products)');
|
||||
final columnNames = columns.map((e) => e['name'] as String).toList();
|
||||
|
||||
// Ajoute la colonne "description" si elle n'existe pas
|
||||
if (!columnNames.contains('description')) {
|
||||
try {
|
||||
await db.execute("ALTER TABLE products ADD COLUMN description TEXT");
|
||||
print("Colonne 'description' ajoutée.");
|
||||
} catch (e) {
|
||||
print("Erreur ajout colonne description : $e");
|
||||
}
|
||||
await db.execute("ALTER TABLE products ADD COLUMN description TEXT");
|
||||
print("Colonne 'description' ajoutée.");
|
||||
}
|
||||
|
||||
// Ajoute la colonne "qrCode" si elle n'existe pas
|
||||
if (!columnNames.contains('qrCode')) {
|
||||
try {
|
||||
await db.execute("ALTER TABLE products ADD COLUMN qrCode TEXT");
|
||||
print("Colonne 'qrCode' ajoutée.");
|
||||
} catch (e) {
|
||||
print("Erreur ajout colonne qrCode : $e");
|
||||
}
|
||||
await db.execute("ALTER TABLE products ADD COLUMN qrCode TEXT");
|
||||
print("Colonne 'qrCode' ajoutée.");
|
||||
}
|
||||
|
||||
// Ajoute la colonne "reference" si elle n'existe pas
|
||||
if (!columnNames.contains('reference')) {
|
||||
try {
|
||||
await db.execute("ALTER TABLE products ADD COLUMN reference TEXT");
|
||||
print("Colonne 'reference' ajoutée.");
|
||||
} catch (e) {
|
||||
print("Erreur ajout colonne reference : $e");
|
||||
}
|
||||
await db.execute("ALTER TABLE products ADD COLUMN reference TEXT");
|
||||
print("Colonne 'reference' ajoutée.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createIndexes(Database db) async {
|
||||
await db.execute('CREATE INDEX IF NOT EXISTS idx_products_category ON products(category)');
|
||||
await db.execute('CREATE INDEX IF NOT EXISTS idx_products_reference ON products(reference)');
|
||||
await db.execute('CREATE INDEX IF NOT EXISTS idx_commandes_client ON commandes(clientId)');
|
||||
await db.execute('CREATE INDEX IF NOT EXISTS idx_commandes_date ON commandes(dateCommande)');
|
||||
await db.execute('CREATE INDEX IF NOT EXISTS idx_details_commande ON details_commandes(commandeId)');
|
||||
print("Index créés pour optimiser les performances.");
|
||||
}
|
||||
|
||||
// =========================
|
||||
// MÉTHODES PRODUCTS (existantes)
|
||||
// =========================
|
||||
Future<int> createProduct(Product product) async {
|
||||
final db = await database;
|
||||
return await db.insert('products', product.toMap());
|
||||
@ -108,7 +167,7 @@ class ProductDatabase {
|
||||
|
||||
Future<List<Product>> getProducts() async {
|
||||
final db = await database;
|
||||
final maps = await db.query('products');
|
||||
final maps = await db.query('products', orderBy: 'name ASC');
|
||||
return List.generate(maps.length, (i) {
|
||||
return Product.fromMap(maps[i]);
|
||||
});
|
||||
@ -135,7 +194,7 @@ class ProductDatabase {
|
||||
|
||||
Future<List<String>> getCategories() async {
|
||||
final db = await database;
|
||||
final result = await db.rawQuery('SELECT DISTINCT category FROM products');
|
||||
final result = await db.rawQuery('SELECT DISTINCT category FROM products ORDER BY category');
|
||||
return List.generate(
|
||||
result.length, (index) => result[index]['category'] as String);
|
||||
}
|
||||
@ -143,7 +202,7 @@ class ProductDatabase {
|
||||
Future<List<Product>> getProductsByCategory(String category) async {
|
||||
final db = await database;
|
||||
final maps = await db
|
||||
.query('products', where: 'category = ?', whereArgs: [category]);
|
||||
.query('products', where: 'category = ?', whereArgs: [category], orderBy: 'name ASC');
|
||||
return List.generate(maps.length, (i) {
|
||||
return Product.fromMap(maps[i]);
|
||||
});
|
||||
@ -154,19 +213,347 @@ class ProductDatabase {
|
||||
return await db
|
||||
.rawUpdate('UPDATE products SET stock = ? WHERE id = ?', [stock, id]);
|
||||
}
|
||||
// Ajouter cette méthode dans la classe ProductDatabase
|
||||
|
||||
Future<Product?> getProductByReference(String reference) async {
|
||||
final db = await database;
|
||||
final maps = await db.query(
|
||||
'products',
|
||||
where: 'reference = ?',
|
||||
whereArgs: [reference],
|
||||
);
|
||||
|
||||
if (maps.isNotEmpty) {
|
||||
return Product.fromMap(maps.first);
|
||||
Future<Product?> getProductByReference(String reference) async {
|
||||
final db = await database;
|
||||
final maps = await db.query(
|
||||
'products',
|
||||
where: 'reference = ?',
|
||||
whereArgs: [reference],
|
||||
);
|
||||
|
||||
if (maps.isNotEmpty) {
|
||||
return Product.fromMap(maps.first);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
|
||||
// =========================
|
||||
// MÉTHODES CLIENTS
|
||||
// =========================
|
||||
Future<int> createClient(Client client) async {
|
||||
final db = await database;
|
||||
return await db.insert('clients', client.toMap());
|
||||
}
|
||||
|
||||
Future<List<Client>> getClients() async {
|
||||
final db = await database;
|
||||
final maps = await db.query('clients', where: 'actif = 1', orderBy: 'nom ASC, prenom ASC');
|
||||
return List.generate(maps.length, (i) {
|
||||
return Client.fromMap(maps[i]);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Client?> getClientById(int id) async {
|
||||
final db = await database;
|
||||
final maps = await db.query('clients', where: 'id = ?', whereArgs: [id]);
|
||||
if (maps.isNotEmpty) {
|
||||
return Client.fromMap(maps.first);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<int> updateClient(Client client) async {
|
||||
final db = await database;
|
||||
return await db.update(
|
||||
'clients',
|
||||
client.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [client.id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> deleteClient(int id) async {
|
||||
final db = await database;
|
||||
// Soft delete
|
||||
return await db.update(
|
||||
'clients',
|
||||
{'actif': 0},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Client>> searchClients(String query) async {
|
||||
final db = await database;
|
||||
final maps = await db.query(
|
||||
'clients',
|
||||
where: 'actif = 1 AND (nom LIKE ? OR prenom LIKE ? OR email LIKE ?)',
|
||||
whereArgs: ['%$query%', '%$query%', '%$query%'],
|
||||
orderBy: 'nom ASC, prenom ASC',
|
||||
);
|
||||
return List.generate(maps.length, (i) {
|
||||
return Client.fromMap(maps[i]);
|
||||
});
|
||||
}
|
||||
|
||||
// =========================
|
||||
// MÉTHODES COMMANDES
|
||||
// =========================
|
||||
Future<int> createCommande(Commande commande) async {
|
||||
final db = await database;
|
||||
return await db.insert('commandes', commande.toMap());
|
||||
}
|
||||
|
||||
Future<List<Commande>> getCommandes() async {
|
||||
final db = await database;
|
||||
final maps = await db.rawQuery('''
|
||||
SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail
|
||||
FROM commandes c
|
||||
LEFT JOIN clients cl ON c.clientId = cl.id
|
||||
ORDER BY c.dateCommande DESC
|
||||
''');
|
||||
return List.generate(maps.length, (i) {
|
||||
return Commande.fromMap(maps[i]);
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<Commande>> getCommandesByClient(int clientId) async {
|
||||
final db = await database;
|
||||
final maps = await db.rawQuery('''
|
||||
SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail
|
||||
FROM commandes c
|
||||
LEFT JOIN clients cl ON c.clientId = cl.id
|
||||
WHERE c.clientId = ?
|
||||
ORDER BY c.dateCommande DESC
|
||||
''', [clientId]);
|
||||
return List.generate(maps.length, (i) {
|
||||
return Commande.fromMap(maps[i]);
|
||||
});
|
||||
}
|
||||
|
||||
Future<Commande?> getCommandeById(int id) async {
|
||||
final db = await database;
|
||||
final maps = await db.rawQuery('''
|
||||
SELECT c.*, cl.nom as clientNom, cl.prenom as clientPrenom, cl.email as clientEmail
|
||||
FROM commandes c
|
||||
LEFT JOIN clients cl ON c.clientId = cl.id
|
||||
WHERE c.id = ?
|
||||
''', [id]);
|
||||
if (maps.isNotEmpty) {
|
||||
return Commande.fromMap(maps.first);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<int> updateCommande(Commande commande) async {
|
||||
final db = await database;
|
||||
return await db.update(
|
||||
'commandes',
|
||||
commande.toMap(),
|
||||
where: 'id = ?',
|
||||
whereArgs: [commande.id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> updateStatutCommande(int commandeId, StatutCommande statut) async {
|
||||
final db = await database;
|
||||
return await db.update(
|
||||
'commandes',
|
||||
{'statut': statut.index},
|
||||
where: 'id = ?',
|
||||
whereArgs: [commandeId],
|
||||
);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// MÉTHODES DÉTAILS COMMANDES
|
||||
// =========================
|
||||
Future<int> createDetailCommande(DetailCommande detail) async {
|
||||
final db = await database;
|
||||
return await db.insert('details_commandes', detail.toMap());
|
||||
}
|
||||
|
||||
Future<List<DetailCommande>> getDetailsCommande(int commandeId) async {
|
||||
final db = await database;
|
||||
final maps = await db.rawQuery('''
|
||||
SELECT dc.*, p.name as produitNom, p.image as produitImage, p.reference as produitReference
|
||||
FROM details_commandes dc
|
||||
LEFT JOIN products p ON dc.produitId = p.id
|
||||
WHERE dc.commandeId = ?
|
||||
ORDER BY dc.id
|
||||
''', [commandeId]);
|
||||
return List.generate(maps.length, (i) {
|
||||
return DetailCommande.fromMap(maps[i]);
|
||||
});
|
||||
}
|
||||
|
||||
// =========================
|
||||
// MÉTHODES TRANSACTION COMPLÈTE
|
||||
// =========================
|
||||
Future<int> createCommandeComplete(Client client, Commande commande, List<DetailCommande> details) async {
|
||||
final db = await database;
|
||||
|
||||
return await db.transaction((txn) async {
|
||||
// Créer le client
|
||||
final clientId = await txn.insert('clients', client.toMap());
|
||||
|
||||
// Créer la commande
|
||||
final commandeMap = commande.toMap();
|
||||
commandeMap['clientId'] = clientId;
|
||||
final commandeId = await txn.insert('commandes', commandeMap);
|
||||
|
||||
// Créer les détails et mettre à jour le stock
|
||||
for (var detail in details) {
|
||||
final detailMap = detail.toMap();
|
||||
detailMap['commandeId'] = commandeId; // Ajoute l'ID de la commande
|
||||
await txn.insert('details_commandes', detailMap);
|
||||
|
||||
// Mettre à jour le stock du produit
|
||||
await txn.rawUpdate(
|
||||
'UPDATE products SET stock = stock - ? WHERE id = ?',
|
||||
[detail.quantite, detail.produitId],
|
||||
);
|
||||
}
|
||||
|
||||
return commandeId;
|
||||
});
|
||||
}
|
||||
|
||||
// =========================
|
||||
// STATISTIQUES
|
||||
// =========================
|
||||
Future<Map<String, dynamic>> getStatistiques() async {
|
||||
final db = await database;
|
||||
|
||||
final totalClients = await db.rawQuery('SELECT COUNT(*) as count FROM clients WHERE actif = 1');
|
||||
final totalCommandes = await db.rawQuery('SELECT COUNT(*) as count FROM commandes');
|
||||
final totalProduits = await db.rawQuery('SELECT COUNT(*) as count FROM products');
|
||||
final chiffreAffaires = await db.rawQuery('SELECT SUM(montantTotal) as total FROM commandes WHERE statut != 5'); // 5 = annulée
|
||||
|
||||
return {
|
||||
'totalClients': totalClients.first['count'],
|
||||
'totalCommandes': totalCommandes.first['count'],
|
||||
'totalProduits': totalProduits.first['count'],
|
||||
'chiffreAffaires': chiffreAffaires.first['total'] ?? 0.0,
|
||||
};
|
||||
}
|
||||
|
||||
// =========================
|
||||
// DONNÉES PAR DÉFAUT
|
||||
// =========================
|
||||
Future<void> _insertDefaultClients() async {
|
||||
final db = await database;
|
||||
final existingClients = await db.query('clients');
|
||||
|
||||
if (existingClients.isEmpty) {
|
||||
final defaultClients = [
|
||||
Client(
|
||||
nom: 'Dupont',
|
||||
prenom: 'Jean',
|
||||
email: 'jean.dupont@email.com',
|
||||
telephone: '0123456789',
|
||||
adresse: '123 Rue de la Paix, Paris',
|
||||
dateCreation: DateTime.now(),
|
||||
),
|
||||
Client(
|
||||
nom: 'Martin',
|
||||
prenom: 'Marie',
|
||||
email: 'marie.martin@email.com',
|
||||
telephone: '0987654321',
|
||||
adresse: '456 Avenue des Champs, Lyon',
|
||||
dateCreation: DateTime.now(),
|
||||
),
|
||||
Client(
|
||||
nom: 'Bernard',
|
||||
prenom: 'Pierre',
|
||||
email: 'pierre.bernard@email.com',
|
||||
telephone: '0456789123',
|
||||
adresse: '789 Boulevard Saint-Michel, Marseille',
|
||||
dateCreation: DateTime.now(),
|
||||
),
|
||||
];
|
||||
|
||||
for (var client in defaultClients) {
|
||||
await db.insert('clients', client.toMap());
|
||||
}
|
||||
print("Clients par défaut insérés");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _insertDefaultCommandes() async {
|
||||
final db = await database;
|
||||
final existingCommandes = await db.query('commandes');
|
||||
|
||||
if (existingCommandes.isEmpty) {
|
||||
// Récupérer quelques produits pour créer des commandes
|
||||
final produits = await db.query('products', limit: 3);
|
||||
final clients = await db.query('clients', limit: 3);
|
||||
|
||||
if (produits.isNotEmpty && clients.isNotEmpty) {
|
||||
// Commande 1
|
||||
final commande1Id = await db.insert('commandes', {
|
||||
'clientId': clients[0]['id'],
|
||||
'dateCommande': DateTime.now().subtract(Duration(days: 5)).toIso8601String(),
|
||||
'statut': StatutCommande.livree.index,
|
||||
'montantTotal': 150.0,
|
||||
'notes': 'Commande urgente',
|
||||
});
|
||||
|
||||
await db.insert('details_commandes', {
|
||||
'commandeId': commande1Id,
|
||||
'produitId': produits[0]['id'],
|
||||
'quantite': 2,
|
||||
'prixUnitaire': 75.0,
|
||||
'sousTotal': 150.0,
|
||||
});
|
||||
|
||||
// Commande 2
|
||||
final commande2Id = await db.insert('commandes', {
|
||||
'clientId': clients[1]['id'],
|
||||
'dateCommande': DateTime.now().subtract(Duration(days: 2)).toIso8601String(),
|
||||
'statut': StatutCommande.enPreparation.index,
|
||||
'montantTotal': 225.0,
|
||||
'notes': 'Livraison prévue demain',
|
||||
});
|
||||
|
||||
if (produits.length > 1) {
|
||||
await db.insert('details_commandes', {
|
||||
'commandeId': commande2Id,
|
||||
'produitId': produits[1]['id'],
|
||||
'quantite': 3,
|
||||
'prixUnitaire': 75.0,
|
||||
'sousTotal': 225.0,
|
||||
});
|
||||
}
|
||||
|
||||
// Commande 3
|
||||
final commande3Id = await db.insert('commandes', {
|
||||
'clientId': clients[2]['id'],
|
||||
'dateCommande': DateTime.now().subtract(Duration(hours: 6)).toIso8601String(),
|
||||
'statut': StatutCommande.confirmee.index,
|
||||
'montantTotal': 300.0,
|
||||
'notes': 'Commande standard',
|
||||
});
|
||||
|
||||
if (produits.length > 2) {
|
||||
await db.insert('details_commandes', {
|
||||
'commandeId': commande3Id,
|
||||
'produitId': produits[2]['id'],
|
||||
'quantite': 4,
|
||||
'prixUnitaire': 75.0,
|
||||
'sousTotal': 300.0,
|
||||
});
|
||||
}
|
||||
|
||||
print("Commandes par défaut insérées");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
if (_database.isOpen) {
|
||||
await _database.close();
|
||||
}
|
||||
}
|
||||
// Ajoutez cette méthode temporaire pour supprimer la DB corrompue
|
||||
Future<void> deleteDatabaseFile() async {
|
||||
final documentsDirectory = await getApplicationDocumentsDirectory();
|
||||
final path = join(documentsDirectory.path, 'products2.db');
|
||||
final file = File(path);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
print("Base de données product supprimée");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,812 +0,0 @@
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:excel/excel.dart' hide Border;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../Components/appDrawer.dart';
|
||||
import '../Components/app_bar.dart';
|
||||
import '../Models/produit.dart';
|
||||
import '../Services/productDatabase.dart';
|
||||
|
||||
class AddProductPage extends StatefulWidget {
|
||||
const AddProductPage({super.key});
|
||||
|
||||
@override
|
||||
_AddProductPageState createState() => _AddProductPageState();
|
||||
}
|
||||
|
||||
class _AddProductPageState extends State<AddProductPage> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _priceController = TextEditingController();
|
||||
final TextEditingController _imageController = TextEditingController();
|
||||
final TextEditingController _descriptionController = TextEditingController();
|
||||
final TextEditingController _stockController = TextEditingController();
|
||||
|
||||
final List<String> _categories = ['Sucré', 'Salé', 'Jus', 'Gateaux', 'Non catégorisé'];
|
||||
String? _selectedCategory;
|
||||
File? _pickedImage;
|
||||
String? _qrData;
|
||||
String? _currentReference; // Ajout pour stocker la référence actuelle
|
||||
late ProductDatabase _productDatabase;
|
||||
|
||||
// Variables pour la barre de progression
|
||||
bool _isImporting = false;
|
||||
double _importProgress = 0.0;
|
||||
String _importStatusText = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_productDatabase = ProductDatabase.instance;
|
||||
_productDatabase.initDatabase();
|
||||
_nameController.addListener(_updateQrData);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nameController.removeListener(_updateQrData);
|
||||
_nameController.dispose();
|
||||
_priceController.dispose();
|
||||
_imageController.dispose();
|
||||
_descriptionController.dispose();
|
||||
_stockController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Méthode pour générer une référence unique
|
||||
String _generateUniqueReference() {
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final randomSuffix = DateTime.now().microsecond.toString().padLeft(6, '0');
|
||||
return 'PROD_${timestamp}${randomSuffix}';
|
||||
}
|
||||
|
||||
void _updateQrData() {
|
||||
if (_nameController.text.isNotEmpty) {
|
||||
// Générer une nouvelle référence si elle n'existe pas encore
|
||||
if (_currentReference == null) {
|
||||
_currentReference = _generateUniqueReference();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
// Utiliser la référence courante dans l'URL du QR code
|
||||
_qrData = 'https://stock.guycom.mg/$_currentReference';
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_currentReference = null;
|
||||
_qrData = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
Future<void> _selectImage() async {
|
||||
final result = await FilePicker.platform.pickFiles(type: FileType.image);
|
||||
if (result != null && result.files.single.path != null) {
|
||||
setState(() {
|
||||
_pickedImage = File(result.files.single.path!);
|
||||
_imageController.text = _pickedImage!.path;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Assurez-vous aussi que _generateAndSaveQRCode utilise bien la référence passée :
|
||||
Future<String> _generateAndSaveQRCode(String reference) async {
|
||||
final qrUrl = 'https://stock.guycom.mg/$reference'; // Utilise le paramètre reference
|
||||
|
||||
final validation = QrValidator.validate(
|
||||
data: qrUrl,
|
||||
version: QrVersions.auto,
|
||||
errorCorrectionLevel: QrErrorCorrectLevel.L,
|
||||
);
|
||||
|
||||
if (validation.status != QrValidationStatus.valid) {
|
||||
throw Exception('Données QR invalides: ${validation.error}');
|
||||
}
|
||||
|
||||
final qrCode = validation.qrCode!;
|
||||
final painter = QrPainter.withQr(
|
||||
qr: qrCode,
|
||||
color: Colors.black,
|
||||
emptyColor: Colors.white,
|
||||
gapless: true,
|
||||
);
|
||||
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final path = '${directory.path}/$reference.png'; // Utilise le paramètre reference
|
||||
|
||||
try {
|
||||
final picData = await painter.toImageData(2048, format: ImageByteFormat.png);
|
||||
if (picData != null) {
|
||||
await File(path).writeAsBytes(picData.buffer.asUint8List());
|
||||
} else {
|
||||
throw Exception('Impossible de générer l\'image QR');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la génération du QR code: $e');
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
void _addProduct() async {
|
||||
final name = _nameController.text.trim();
|
||||
final price = double.tryParse(_priceController.text.trim()) ?? 0.0;
|
||||
final image = _imageController.text.trim();
|
||||
final category = _selectedCategory ?? 'Non catégorisé';
|
||||
final description = _descriptionController.text.trim();
|
||||
final stock = int.tryParse(_stockController.text.trim()) ?? 0;
|
||||
|
||||
if (name.isEmpty || price <= 0) {
|
||||
Get.snackbar('Erreur', 'Nom et prix sont obligatoires');
|
||||
return;
|
||||
}
|
||||
|
||||
// Utiliser la référence générée ou en créer une nouvelle
|
||||
String finalReference = _currentReference ?? _generateUniqueReference();
|
||||
|
||||
// Vérifier l'unicité de la référence en base
|
||||
var existingProduct = await _productDatabase.getProductByReference(finalReference);
|
||||
|
||||
// Si la référence existe déjà, en générer une nouvelle
|
||||
while (existingProduct != null) {
|
||||
finalReference = _generateUniqueReference();
|
||||
existingProduct = await _productDatabase.getProductByReference(finalReference);
|
||||
}
|
||||
|
||||
// Mettre à jour la référence courante avec la référence finale
|
||||
_currentReference = finalReference;
|
||||
|
||||
// Générer le QR code avec la référence finale
|
||||
final qrPath = await _generateAndSaveQRCode(finalReference);
|
||||
|
||||
final product = Product(
|
||||
name: name,
|
||||
price: price,
|
||||
image: image,
|
||||
category: category,
|
||||
description: description,
|
||||
qrCode: qrPath,
|
||||
reference: finalReference, // Utiliser la référence finale
|
||||
stock: stock,
|
||||
);
|
||||
|
||||
try {
|
||||
await _productDatabase.createProduct(product);
|
||||
Get.snackbar('Succès', 'Produit ajouté avec succès\nRéférence: $finalReference');
|
||||
|
||||
setState(() {
|
||||
_nameController.clear();
|
||||
_priceController.clear();
|
||||
_imageController.clear();
|
||||
_descriptionController.clear();
|
||||
_stockController.clear();
|
||||
_selectedCategory = null;
|
||||
_pickedImage = null;
|
||||
_qrData = null;
|
||||
_currentReference = null; // Reset de la référence
|
||||
});
|
||||
} catch (e) {
|
||||
Get.snackbar('Erreur', 'Ajout du produit échoué : $e');
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
// Méthode pour réinitialiser l'état d'importation
|
||||
void _resetImportState() {
|
||||
setState(() {
|
||||
_isImporting = false;
|
||||
_importProgress = 0.0;
|
||||
_importStatusText = '';
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _importFromExcel() async {
|
||||
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['xlsx', 'xls','csv'],
|
||||
allowMultiple: false,
|
||||
);
|
||||
|
||||
if (result == null || result.files.isEmpty) {
|
||||
Get.snackbar('Annulé', 'Aucun fichier sélectionné');
|
||||
return;
|
||||
}
|
||||
|
||||
// Démarrer la progression
|
||||
setState(() {
|
||||
_isImporting = true;
|
||||
_importProgress = 0.0;
|
||||
_importStatusText = 'Lecture du fichier...';
|
||||
});
|
||||
|
||||
final file = File(result.files.single.path!);
|
||||
|
||||
// Vérifier que le fichier existe
|
||||
if (!await file.exists()) {
|
||||
_resetImportState();
|
||||
Get.snackbar('Erreur', 'Le fichier sélectionné n\'existe pas');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_importProgress = 0.1;
|
||||
_importStatusText = 'Vérification du fichier...';
|
||||
});
|
||||
|
||||
final bytes = await file.readAsBytes();
|
||||
|
||||
// Vérifier que le fichier n'est pas vide
|
||||
if (bytes.isEmpty) {
|
||||
_resetImportState();
|
||||
Get.snackbar('Erreur', 'Le fichier Excel est vide');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_importProgress = 0.2;
|
||||
_importStatusText = 'Décodage du fichier Excel...';
|
||||
});
|
||||
|
||||
Excel excel;
|
||||
try {
|
||||
// Initialisation
|
||||
setState(() {
|
||||
_isImporting = true;
|
||||
_importProgress = 0.0;
|
||||
_importStatusText = 'Initialisation...';
|
||||
});
|
||||
|
||||
// Petit délai pour permettre au build de s'exécuter
|
||||
await Future.delayed(Duration(milliseconds: 50));
|
||||
excel = Excel.decodeBytes(bytes);
|
||||
} catch (e) {
|
||||
_resetImportState();
|
||||
debugPrint('Erreur décodage Excel: $e');
|
||||
|
||||
if (e.toString().contains('styles') || e.toString().contains('Damaged')) {
|
||||
_showExcelCompatibilityError();
|
||||
return;
|
||||
} else {
|
||||
Get.snackbar('Erreur', 'Impossible de lire le fichier Excel. Format non supporté.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (excel.tables.isEmpty) {
|
||||
_resetImportState();
|
||||
Get.snackbar('Erreur', 'Le fichier Excel ne contient aucune feuille');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_importProgress = 0.3;
|
||||
_importStatusText = 'Analyse des données...';
|
||||
});
|
||||
|
||||
int successCount = 0;
|
||||
int errorCount = 0;
|
||||
List<String> errorMessages = [];
|
||||
|
||||
// Prendre la première feuille disponible
|
||||
final sheetName = excel.tables.keys.first;
|
||||
final sheet = excel.tables[sheetName]!;
|
||||
|
||||
if (sheet.rows.isEmpty) {
|
||||
_resetImportState();
|
||||
Get.snackbar('Erreur', 'La feuille Excel est vide');
|
||||
return;
|
||||
}
|
||||
|
||||
final totalRows = sheet.rows.length - 1; // -1 pour exclure l'en-tête
|
||||
|
||||
setState(() {
|
||||
_importStatusText = 'Importation en cours... (0/$totalRows)';
|
||||
});
|
||||
|
||||
// Ignorer la première ligne (en-têtes) et traiter les données
|
||||
for (var i = 1; i < sheet.rows.length; i++) {
|
||||
try {
|
||||
// Mettre à jour la progression
|
||||
final currentProgress = 0.3 + (0.6 * (i - 1) / totalRows);
|
||||
setState(() {
|
||||
_importProgress = currentProgress;
|
||||
_importStatusText = 'Importation en cours... (${i - 1}/$totalRows)';
|
||||
});
|
||||
|
||||
// Petite pause pour permettre à l'UI de se mettre à jour
|
||||
await Future.delayed(const Duration(milliseconds: 10));
|
||||
|
||||
final row = sheet.rows[i];
|
||||
|
||||
// Vérifier que la ligne a au moins les colonnes obligatoires (nom et prix)
|
||||
if (row.isEmpty || row.length < 2) {
|
||||
errorCount++;
|
||||
errorMessages.add('Ligne ${i + 1}: Données insuffisantes');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extraire les valeurs avec vérifications sécurisées
|
||||
final nameCell = row[0];
|
||||
final priceCell = row[1];
|
||||
|
||||
// Extraction sécurisée des valeurs
|
||||
String? nameValue;
|
||||
String? priceValue;
|
||||
|
||||
if (nameCell?.value != null) {
|
||||
nameValue = nameCell!.value.toString().trim();
|
||||
}
|
||||
|
||||
if (priceCell?.value != null) {
|
||||
priceValue = priceCell!.value.toString().trim();
|
||||
}
|
||||
|
||||
if (nameValue == null || nameValue.isEmpty) {
|
||||
errorCount++;
|
||||
errorMessages.add('Ligne ${i + 1}: Nom du produit manquant');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (priceValue == null || priceValue.isEmpty) {
|
||||
errorCount++;
|
||||
errorMessages.add('Ligne ${i + 1}: Prix manquant');
|
||||
continue;
|
||||
}
|
||||
|
||||
final name = nameValue;
|
||||
// Remplacer les virgules par des points pour les décimaux
|
||||
final price = double.tryParse(priceValue.replaceAll(',', '.'));
|
||||
|
||||
if (price == null || price <= 0) {
|
||||
errorCount++;
|
||||
errorMessages.add('Ligne ${i + 1}: Prix invalide ($priceValue)');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extraire les autres colonnes optionnelles de manière sécurisée
|
||||
String category = 'Non catégorisé';
|
||||
if (row.length > 2 && row[2]?.value != null) {
|
||||
final categoryValue = row[2]!.value.toString().trim();
|
||||
if (categoryValue.isNotEmpty) {
|
||||
category = categoryValue;
|
||||
}
|
||||
}
|
||||
|
||||
String description = '';
|
||||
if (row.length > 3 && row[3]?.value != null) {
|
||||
description = row[3]!.value.toString().trim();
|
||||
}
|
||||
|
||||
int stock = 0;
|
||||
if (row.length > 4 && row[4]?.value != null) {
|
||||
final stockStr = row[4]!.value.toString().trim();
|
||||
stock = int.tryParse(stockStr) ?? 0;
|
||||
}
|
||||
|
||||
// Générer une référence unique et vérifier son unicité
|
||||
String reference = _generateUniqueReference();
|
||||
|
||||
// Vérifier l'unicité en base de données
|
||||
var existingProduct = await _productDatabase.getProductByReference(reference);
|
||||
while (existingProduct != null) {
|
||||
reference = _generateUniqueReference();
|
||||
existingProduct = await _productDatabase.getProductByReference(reference);
|
||||
}
|
||||
|
||||
// Créer le produit
|
||||
final product = Product(
|
||||
name: name,
|
||||
price: price,
|
||||
image: '', // Pas d'image lors de l'import
|
||||
category: category,
|
||||
description: description,
|
||||
stock: stock,
|
||||
qrCode: '', // Sera généré après
|
||||
reference: reference,
|
||||
);
|
||||
|
||||
// Générer et sauvegarder le QR code avec la nouvelle URL
|
||||
setState(() {
|
||||
_importStatusText = 'Génération QR Code... (${i - 1}/$totalRows)';
|
||||
});
|
||||
|
||||
final qrPath = await _generateAndSaveQRCode(reference);
|
||||
product.qrCode = qrPath;
|
||||
|
||||
// Sauvegarder en base de données
|
||||
await _productDatabase.createProduct(product);
|
||||
successCount++;
|
||||
|
||||
} catch (e) {
|
||||
errorCount++;
|
||||
errorMessages.add('Ligne ${i + 1}: Erreur de traitement - $e');
|
||||
debugPrint('Erreur ligne ${i + 1}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Finalisation
|
||||
setState(() {
|
||||
_importProgress = 1.0;
|
||||
_importStatusText = 'Finalisation...';
|
||||
});
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// Réinitialiser l'état d'importation
|
||||
_resetImportState();
|
||||
|
||||
// Afficher le résultat
|
||||
String message = '$successCount produits importés avec succès';
|
||||
if (errorCount > 0) {
|
||||
message += ', $errorCount erreurs';
|
||||
|
||||
// Afficher les détails des erreurs si pas trop nombreuses
|
||||
if (errorMessages.length <= 5) {
|
||||
message += ':\n${errorMessages.join('\n')}';
|
||||
}
|
||||
}
|
||||
|
||||
Get.snackbar(
|
||||
'Importation terminée',
|
||||
message,
|
||||
duration: const Duration(seconds: 6),
|
||||
colorText: Colors.white,
|
||||
backgroundColor: successCount > 0 ? Colors.green : Colors.orange,
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
_resetImportState();
|
||||
Get.snackbar('Erreur', 'Erreur lors de l\'importation Excel: $e');
|
||||
debugPrint('Erreur générale import Excel: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _showExcelCompatibilityError() {
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: const Text('Fichier Excel incompatible'),
|
||||
content: const Text(
|
||||
'Ce fichier Excel contient des éléments qui ne sont pas compatibles avec notre système d\'importation.\n\n'
|
||||
'Solutions recommandées :\n'
|
||||
'• Téléchargez notre modèle Excel et copiez-y vos données\n'
|
||||
'• Ou exportez votre fichier en format simple: Classeur Excel .xlsx depuis Excel\n'
|
||||
'• Ou créez un nouveau fichier Excel simple sans formatage complexe'
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Get.back();
|
||||
_downloadExcelTemplate();
|
||||
},
|
||||
child: const Text('Télécharger modèle'),
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _downloadExcelTemplate() async {
|
||||
try {
|
||||
// Créer un fichier Excel temporaire comme modèle
|
||||
final excel = Excel.createExcel();
|
||||
|
||||
// Supprimer la feuille par défaut et créer une nouvelle
|
||||
excel.delete('Sheet1');
|
||||
excel.copy('Sheet1', 'Produits');
|
||||
excel.delete('Sheet1');
|
||||
|
||||
final sheet = excel['Produits'];
|
||||
|
||||
// Ajouter les en-têtes avec du style
|
||||
final headers = ['Nom', 'Prix', 'Catégorie', 'Description', 'Stock'];
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0));
|
||||
cell.value = headers[i];
|
||||
cell.cellStyle = CellStyle(
|
||||
bold: true,
|
||||
backgroundColorHex: '#E8F4FD',
|
||||
);
|
||||
}
|
||||
|
||||
// Ajouter des exemples
|
||||
final examples = [
|
||||
['Croissant', '1.50', 'Sucré', 'Délicieux croissant beurré', '20'],
|
||||
['Sandwich jambon', '4.00', 'Salé', 'Sandwich fait maison', '15'],
|
||||
['Jus d\'orange', '2.50', 'Jus', 'Jus d\'orange frais', '30'],
|
||||
['Gâteau chocolat', '18.00', 'Gateaux', 'Gâteau au chocolat portion 8 personnes', '5'],
|
||||
];
|
||||
|
||||
for (int row = 0; row < examples.length; row++) {
|
||||
for (int col = 0; col < examples[row].length; col++) {
|
||||
final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row + 1));
|
||||
cell.value = examples[row][col];
|
||||
}
|
||||
}
|
||||
|
||||
// Ajuster la largeur des colonnes
|
||||
sheet.setColWidth(0, 20); // Nom
|
||||
sheet.setColWidth(1, 10); // Prix
|
||||
sheet.setColWidth(2, 15); // Catégorie
|
||||
sheet.setColWidth(3, 30); // Description
|
||||
sheet.setColWidth(4, 10); // Stock
|
||||
|
||||
// Sauvegarder en mémoire
|
||||
final bytes = excel.save();
|
||||
|
||||
if (bytes == null) {
|
||||
Get.snackbar('Erreur', 'Impossible de créer le fichier modèle');
|
||||
return;
|
||||
}
|
||||
|
||||
// Demander où sauvegarder
|
||||
final String? outputFile = await FilePicker.platform.saveFile(
|
||||
fileName: 'modele_import_produits.xlsx',
|
||||
allowedExtensions: ['xlsx'],
|
||||
type: FileType.custom,
|
||||
);
|
||||
|
||||
if (outputFile != null) {
|
||||
try {
|
||||
await File(outputFile).writeAsBytes(bytes);
|
||||
Get.snackbar(
|
||||
'Succès',
|
||||
'Modèle téléchargé avec succès\n$outputFile',
|
||||
duration: const Duration(seconds: 4),
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} catch (e) {
|
||||
Get.snackbar('Erreur', 'Impossible d\'écrire le fichier: $e');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar('Erreur', 'Erreur lors de la création du modèle: $e');
|
||||
debugPrint('Erreur création modèle Excel: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Widget _displayImage() {
|
||||
if (_pickedImage != null) {
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
child: Image.file(
|
||||
_pickedImage!,
|
||||
width: 100,
|
||||
height: 100,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Icon(Icons.image, size: 32, color: Colors.grey),
|
||||
Text('Aucune image', style: TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const CustomAppBar(title: 'Ajouter un produit'),
|
||||
drawer: CustomDrawer(),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Ajouter un produit', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Boutons d'importation
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _isImporting ? null : _importFromExcel,
|
||||
icon: const Icon(Icons.upload),
|
||||
label: const Text('Importer depuis Excel'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
TextButton(
|
||||
onPressed: _isImporting ? null : _downloadExcelTemplate,
|
||||
child: const Text('Modèle'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Barre de progression
|
||||
if (_isImporting) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.blue.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Importation en cours...',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
value: _importProgress,
|
||||
backgroundColor: Colors.blue.shade100,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue.shade600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_importStatusText,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${(_importProgress * 100).round()}%',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Formulaire d'ajout manuel
|
||||
TextField(
|
||||
controller: _nameController,
|
||||
enabled: !_isImporting,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom du produit*',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextField(
|
||||
controller: _priceController,
|
||||
enabled: !_isImporting,
|
||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Prix*',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextField(
|
||||
controller: _stockController,
|
||||
enabled: !_isImporting,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Stock',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Section image (optionnelle)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _imageController,
|
||||
enabled: !_isImporting,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Chemin de l\'image (optionnel)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton(
|
||||
onPressed: _isImporting ? null : _selectImage,
|
||||
child: const Text('Sélectionner'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_displayImage(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
DropdownButtonFormField<String>(
|
||||
value: _selectedCategory,
|
||||
items: _categories
|
||||
.map((c) => DropdownMenuItem(value: c, child: Text(c)))
|
||||
.toList(),
|
||||
onChanged: _isImporting ? null : (value) => setState(() => _selectedCategory = value),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Catégorie',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
TextField(
|
||||
controller: _descriptionController,
|
||||
enabled: !_isImporting,
|
||||
maxLines: 3,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Description (optionnel)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (_qrData != null) ...[
|
||||
const Text('Aperçu du QR Code :'),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: QrImageView(
|
||||
data: _qrData!,
|
||||
version: QrVersions.auto,
|
||||
size: 120,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: Text(
|
||||
_qrData!,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isImporting ? null : _addProduct,
|
||||
child: const Text('Ajouter le produit'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
1329
lib/Views/commandManagement.dart
Normal file
1329
lib/Views/commandManagement.dart
Normal file
File diff suppressed because it is too large
Load Diff
623
lib/Views/newCommand.dart
Normal file
623
lib/Views/newCommand.dart
Normal file
@ -0,0 +1,623 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:youmazgestion/Components/app_bar.dart';
|
||||
import 'package:youmazgestion/Components/appDrawer.dart';
|
||||
import 'package:youmazgestion/Models/client.dart';
|
||||
import 'package:youmazgestion/Models/produit.dart';
|
||||
import 'package:youmazgestion/Services/productDatabase.dart';
|
||||
|
||||
class NouvelleCommandePage extends StatefulWidget {
|
||||
const NouvelleCommandePage({super.key});
|
||||
|
||||
@override
|
||||
_NouvelleCommandePageState createState() => _NouvelleCommandePageState();
|
||||
}
|
||||
|
||||
class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
final ProductDatabase _database = ProductDatabase.instance;
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Informations client
|
||||
final TextEditingController _nomController = TextEditingController();
|
||||
final TextEditingController _prenomController = TextEditingController();
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
final TextEditingController _telephoneController = TextEditingController();
|
||||
final TextEditingController _adresseController = TextEditingController();
|
||||
|
||||
// Panier
|
||||
final List<Product> _products = [];
|
||||
final Map<int, int> _quantites = {}; // productId -> quantity
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadProducts();
|
||||
}
|
||||
|
||||
Future<void> _loadProducts() async {
|
||||
final products = await _database.getProducts();
|
||||
setState(() {
|
||||
_products.addAll(products);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const CustomAppBar(title: 'Nouvelle Commande'),
|
||||
drawer: CustomDrawer(),
|
||||
body: Column(
|
||||
children: [
|
||||
// Header avec logo et titre
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.blue.shade50, Colors.white],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Logo et titre
|
||||
Row(
|
||||
children: [
|
||||
// Logo de l'entreprise
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
'assets/logo.png',
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade800,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.shopping_cart,
|
||||
color: Colors.white,
|
||||
size: 30,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Nouvelle Commande',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Créez une nouvelle commande pour un client',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Contenu principal
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildClientForm(),
|
||||
const SizedBox(height: 20),
|
||||
_buildProductList(),
|
||||
const SizedBox(height: 20),
|
||||
_buildCartSection(),
|
||||
const SizedBox(height: 20),
|
||||
_buildTotalSection(),
|
||||
const SizedBox(height: 20),
|
||||
_buildSubmitButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildClientForm() {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Informations Client',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _nomController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nom',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer un nom';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _prenomController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Prénom',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer un prénom';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer un email';
|
||||
}
|
||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
|
||||
return 'Veuillez entrer un email valide';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _telephoneController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Téléphone',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
keyboardType: TextInputType.phone,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer un numéro de téléphone';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _adresseController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Adresse de livraison',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
maxLines: 2,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer une adresse';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProductList() {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Produits Disponibles',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_products.isEmpty
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _products.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = _products[index];
|
||||
final quantity = _quantites[product.id] ?? 0;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
leading: Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(Icons.shopping_bag,
|
||||
color: Colors.blue),
|
||||
),
|
||||
title: Text(
|
||||
product.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${product.price.toStringAsFixed(2)} DA',
|
||||
style: TextStyle(
|
||||
color: Colors.green.shade700,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (product.stock != null)
|
||||
Text(
|
||||
'Stock: ${product.stock}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove, size: 18),
|
||||
onPressed: () {
|
||||
if (quantity > 0) {
|
||||
setState(() {
|
||||
_quantites[product.id!] = quantity - 1;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
Text(
|
||||
quantity.toString(),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
onPressed: () {
|
||||
if (product.stock == null || quantity < product.stock!) {
|
||||
setState(() {
|
||||
_quantites[product.id!] = quantity + 1;
|
||||
});
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Stock insuffisant',
|
||||
'Quantité demandée non disponible',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCartSection() {
|
||||
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList();
|
||||
|
||||
if (itemsInCart.isEmpty) {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Votre panier est vide',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Votre Panier',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...itemsInCart.map((entry) {
|
||||
final product = _products.firstWhere((p) => p.id == entry.key);
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
elevation: 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
leading: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(Icons.shopping_bag, size: 20),
|
||||
),
|
||||
title: Text(product.name),
|
||||
subtitle: Text('${entry.value} x ${product.price.toStringAsFixed(2)} DA'),
|
||||
trailing: Text(
|
||||
'${(entry.value * product.price).toStringAsFixed(2)} DA',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade800,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTotalSection() {
|
||||
double total = 0;
|
||||
_quantites.forEach((productId, quantity) {
|
||||
final product = _products.firstWhere((p) => p.id == productId);
|
||||
total += quantity * product.price;
|
||||
});
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Total:',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
'${total.toStringAsFixed(2)} DA',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubmitButton() {
|
||||
return ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
backgroundColor: Colors.blue.shade600,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 4,
|
||||
),
|
||||
onPressed: _submitOrder,
|
||||
child: const Text(
|
||||
'Valider la Commande',
|
||||
style: TextStyle(fontSize: 16, color: Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _submitOrder() async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList();
|
||||
if (itemsInCart.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Panier vide',
|
||||
'Veuillez ajouter des produits à votre commande',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Créer le client
|
||||
final client = Client(
|
||||
nom: _nomController.text,
|
||||
prenom: _prenomController.text,
|
||||
email: _emailController.text,
|
||||
telephone: _telephoneController.text,
|
||||
adresse: _adresseController.text,
|
||||
dateCreation: DateTime.now(),
|
||||
);
|
||||
|
||||
// Calculer le total et préparer les détails
|
||||
double total = 0;
|
||||
final details = <DetailCommande>[];
|
||||
|
||||
for (final entry in itemsInCart) {
|
||||
final product = _products.firstWhere((p) => p.id == entry.key);
|
||||
total += entry.value * product.price;
|
||||
|
||||
details.add(DetailCommande(
|
||||
commandeId: 0, // Valeur temporaire, sera remplacée dans la transaction
|
||||
produitId: product.id!,
|
||||
quantite: entry.value,
|
||||
prixUnitaire: product.price,
|
||||
sousTotal: entry.value * product.price,
|
||||
));
|
||||
}
|
||||
|
||||
// Créer la commande
|
||||
final commande = Commande(
|
||||
clientId: 0, // sera mis à jour après création du client
|
||||
dateCommande: DateTime.now(),
|
||||
statut: StatutCommande.enAttente,
|
||||
montantTotal: total,
|
||||
notes: 'Commande passée via l\'application',
|
||||
);
|
||||
|
||||
try {
|
||||
// Enregistrer la commande dans la base de données
|
||||
await _database.createCommandeComplete(client, commande, details);
|
||||
|
||||
Get.snackbar(
|
||||
'Succès',
|
||||
'Votre commande a été enregistrée',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
// Réinitialiser le formulaire
|
||||
_formKey.currentState!.reset();
|
||||
setState(() {
|
||||
_quantites.clear();
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Erreur',
|
||||
'Une erreur est survenue lors de l\'enregistrement de la commande: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_nomController.dispose();
|
||||
_prenomController.dispose();
|
||||
_emailController.dispose();
|
||||
_telephoneController.dispose();
|
||||
_adresseController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:io';
|
||||
import 'package:quantity_input/quantity_input.dart';
|
||||
import 'package:youmazgestion/Models/produit.dart';
|
||||
|
||||
class ProductCard extends StatefulWidget {
|
||||
@ -163,7 +162,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
@ -300,81 +299,84 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
|
||||
const SizedBox(width: 8),
|
||||
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTapDown: _onTapDown,
|
||||
onTapUp: _onTapUp,
|
||||
onTapCancel: _onTapCancel,
|
||||
onTap: () {
|
||||
widget.onAddToCart(widget.product, selectedQuantity);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.shopping_cart,
|
||||
color: Colors.white,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${widget.product.name} (x$selectedQuantity) ajouté au panier',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTapDown: _onTapDown,
|
||||
onTapUp: _onTapUp,
|
||||
onTapCancel: _onTapCancel,
|
||||
onTap: () {
|
||||
widget.onAddToCart(widget.product, selectedQuantity);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.shopping_cart,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 1),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color.fromARGB(255, 4, 54, 95),
|
||||
Color.fromARGB(255, 6, 80, 140),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color.fromARGB(255, 4, 54, 95).withOpacity(0.3),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.add_shopping_cart,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Flexible(
|
||||
child: Text(
|
||||
'Ajouter',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${widget.product.name} (x$selectedQuantity) ajouté au panier',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
duration: const Duration(seconds: 1),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color.fromARGB(255, 4, 54, 95),
|
||||
Color.fromARGB(255, 6, 80, 140),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color.fromARGB(255, 4, 54, 95).withOpacity(0.3),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.add_shopping_cart,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Flexible(
|
||||
child: Text(
|
||||
'Ajouter',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
453
lib/accueil.dart
453
lib/accueil.dart
@ -2,7 +2,6 @@ import 'dart:io';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:quantity_input/quantity_input.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
|
||||
import 'package:youmazgestion/Views/produitsCard.dart';
|
||||
@ -39,27 +38,21 @@ class _AccueilPageState extends State<AccueilPage> {
|
||||
int selectedQuantity = 1;
|
||||
double totalCartPrice = 0;
|
||||
double amountPaid = 0;
|
||||
final TextEditingController _amountController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initorder();
|
||||
initwork();
|
||||
_initializeDatabases();
|
||||
loadUserData();
|
||||
productsFuture = _initDatabaseAndFetchProducts();
|
||||
_initializeRegister();
|
||||
super.initState();
|
||||
_initializeDatabases();
|
||||
loadUserData();
|
||||
productsFuture = _initDatabaseAndFetchProducts();
|
||||
}
|
||||
|
||||
|
||||
Future<void> _initializeDatabases() async {
|
||||
await orderDatabase.initDatabase();
|
||||
await workDatabase.initDatabase(); // Attendre l'initialisation complète
|
||||
await _initializeRegister();
|
||||
}
|
||||
Future<void> _initializeDatabases() async {
|
||||
await orderDatabase.initDatabase();
|
||||
await workDatabase.initDatabase();
|
||||
await _initializeRegister();
|
||||
}
|
||||
|
||||
Future<void> _initializeRegister() async {
|
||||
if (!MyApp.isRegisterOpen) {
|
||||
@ -86,6 +79,30 @@ Future<void> _initializeDatabases() async {
|
||||
final dateTime = DateTime.now().toString();
|
||||
String user = userController.username;
|
||||
|
||||
if (selectedProducts.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Panier vide',
|
||||
'Ajoutez des produits avant de passer commande.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
duration: const Duration(seconds: 3),
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (amountPaid < totalPrice) {
|
||||
Get.snackbar(
|
||||
'Paiement incomplet',
|
||||
'Le montant payé est insuffisant.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
duration: const Duration(seconds: 3),
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
orderId = await orderDatabase.insertOrder(
|
||||
totalPrice, dateTime, MyApp.startDate!, user);
|
||||
|
||||
@ -100,7 +117,14 @@ Future<void> _initializeDatabases() async {
|
||||
final updatedStock = product.stock! - quantity;
|
||||
await productDatabase.updateStock(product.id!, updatedStock);
|
||||
}
|
||||
|
||||
// Afficher le ticket et réinitialiser le panier
|
||||
showTicketPage();
|
||||
setState(() {
|
||||
selectedProducts.clear();
|
||||
_amountController.clear();
|
||||
amountPaid = 0;
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, List<Product>>> _initDatabaseAndFetchProducts() async {
|
||||
@ -119,14 +143,6 @@ Future<void> _initializeDatabases() async {
|
||||
return productsByCategory;
|
||||
}
|
||||
|
||||
void initorder() async {
|
||||
await orderDatabase.initDatabase();
|
||||
}
|
||||
|
||||
void initwork() async {
|
||||
await workDatabase.initDatabase();
|
||||
}
|
||||
|
||||
double calculateTotalPrice() {
|
||||
double totalPrice = 0;
|
||||
for (final cartItem in selectedProducts) {
|
||||
@ -159,49 +175,24 @@ Future<void> _initializeDatabases() async {
|
||||
}
|
||||
|
||||
void showTicketPage() {
|
||||
final double totalCartPrice = calculateTotalPrice();
|
||||
|
||||
if (selectedProducts.isNotEmpty) {
|
||||
if (amountPaid >= totalCartPrice) {
|
||||
Get.offAll(TicketPage(
|
||||
businessName: 'Youmaz',
|
||||
businessAddress:
|
||||
'quartier escale, Diourbel, Sénégal, en face de Sonatel',
|
||||
businessPhoneNumber: '77 446 92 68',
|
||||
cartItems: selectedProducts,
|
||||
totalCartPrice: totalCartPrice,
|
||||
amountPaid: amountPaid,
|
||||
));
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Paiement incomplet',
|
||||
'Le montant payé est insuffisant.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
duration: const Duration(seconds: 3),
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Panier vide',
|
||||
'Ajoutez des produits avant de passer commande.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
duration: const Duration(seconds: 3),
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
Get.offAll(TicketPage(
|
||||
businessName: 'Youmaz',
|
||||
businessAddress:
|
||||
'quartier escale, Diourbel, Sénégal, en face de Sonatel',
|
||||
businessPhoneNumber: '77 446 92 68',
|
||||
cartItems: selectedProducts,
|
||||
totalCartPrice: calculateTotalPrice(),
|
||||
amountPaid: amountPaid,
|
||||
));
|
||||
}
|
||||
@override
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
title: "Accueil",
|
||||
subtitle: Text('Bienvenue $username ! (Rôle: $role)'),
|
||||
subtitle: Text('Bienvenue $username ! (Rôle: $role)',
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 14)),
|
||||
),
|
||||
drawer: CustomDrawer(),
|
||||
body: ParticleBackground(
|
||||
@ -216,98 +207,159 @@ Future<void> _initializeDatabases() async {
|
||||
future: productsFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Color.fromARGB(255, 4, 54, 95),),
|
||||
));
|
||||
} else if (snapshot.hasError) {
|
||||
return const Center(child: Text("Erreur de chargement"));
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error, color: Colors.red, size: 48),
|
||||
SizedBox(height: 16),
|
||||
Text("Erreur de chargement des produits",
|
||||
style: TextStyle(fontSize: 16, color: Colors.white)),
|
||||
],
|
||||
));
|
||||
} else if (snapshot.hasData) {
|
||||
final productsByCategory = snapshot.data!;
|
||||
final categories = productsByCategory.keys.toList();
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// Section produits
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: ListView.builder(
|
||||
itemCount: categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = categories[index];
|
||||
final products = productsByCategory[category]!;
|
||||
child: Container(
|
||||
padding:const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
|
||||
),
|
||||
child: ListView.builder(
|
||||
itemCount: categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = categories[index];
|
||||
final products = productsByCategory[category]!;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Text(
|
||||
category,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
padding:const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Color.fromARGB(255, 4, 54, 95),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),)
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
category,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GridView.builder(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
childAspectRatio: 1,
|
||||
GridView.builder(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
childAspectRatio: 0.9,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: products.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = products[index];
|
||||
return ProductCard(
|
||||
product: product,
|
||||
onAddToCart: (product, quantity) {
|
||||
addToCartWithDetails(product, quantity);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: products.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = products[index];
|
||||
return ProductCard(
|
||||
product: product,
|
||||
onAddToCart: (product, quantity) {
|
||||
addToCartWithDetails(product, quantity);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
|
||||
// Section panier
|
||||
|
||||
|
||||
),
|
||||
Expanded(flex: 1,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: const BorderRadius.only(
|
||||
borderRadius:const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
bottomLeft: Radius.circular(20),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Panier',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Color.fromARGB(255, 4, 54, 95),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child:const Text(
|
||||
'Panier',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Liste des produits dans le panier
|
||||
Expanded(
|
||||
child: selectedProducts.isEmpty
|
||||
? const Center(
|
||||
child: Text(
|
||||
"Votre panier est vide",
|
||||
style: TextStyle(fontSize: 16),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.shopping_cart,
|
||||
size: 48, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
"Votre panier est vide",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
@ -315,86 +367,135 @@ Future<void> _initializeDatabases() async {
|
||||
itemBuilder: (context, index) {
|
||||
final cartItem = selectedProducts[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 4),
|
||||
margin: EdgeInsets.symmetric(vertical: 4),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
elevation: 2,
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 4),
|
||||
leading: Icon(Icons.shopping_basket,
|
||||
color: Color.fromARGB(255, 4, 54, 95),),
|
||||
title: Text(
|
||||
cartItem.product.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${NumberFormat('#,##0').format(cartItem.product.price)} FCFA x ${cartItem.quantity}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14),
|
||||
style:const TextStyle(fontSize: 14),
|
||||
),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete,
|
||||
color: Colors.red),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
selectedProducts
|
||||
.removeAt(index);
|
||||
});
|
||||
},
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${NumberFormat('#,##0').format(cartItem.product.price * cartItem.quantity)}',
|
||||
style:const TextStyle(
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete,
|
||||
color: Colors.red),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
selectedProducts
|
||||
.removeAt(index);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(thickness: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
'Total: ${NumberFormat('#,##0.00').format(calculateTotalPrice())} FCFA',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
|
||||
// Total et paiement
|
||||
Container(
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Montant payé',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
),
|
||||
onChanged: (value) {
|
||||
amountPaid = double.tryParse(value) ?? 0;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
onPressed: saveOrderToDatabase,
|
||||
child: const Text(
|
||||
'Valider la commande',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Total:',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold)),
|
||||
Text(
|
||||
'${NumberFormat('#,##0.00').format(calculateTotalPrice())} FCFA',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color.fromARGB(255, 4, 54, 95),),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Montant payé',
|
||||
prefixIcon: Icon(Icons.attach_money),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100],
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
amountPaid = double.tryParse(value) ?? 0;
|
||||
});
|
||||
},
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
onPressed: saveOrderToDatabase,
|
||||
icon:const Icon(Icons.check_circle),
|
||||
label:const Text(
|
||||
'Valider la commande',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),)
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const Center(child: Text("Aucun produit disponible"));
|
||||
return const Center(
|
||||
child: Text("Aucun produit disponible",
|
||||
style: TextStyle(color: Colors.white)),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -402,4 +503,10 @@ Future<void> _initializeDatabases() async {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_amountController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,8 @@ void main() async {
|
||||
try {
|
||||
// Initialiser les bases de données une seule fois
|
||||
// await AppDatabase.instance.deleteDatabaseFile();
|
||||
|
||||
//await ProductDatabase.instance.deleteDatabaseFile();
|
||||
|
||||
await ProductDatabase.instance.initDatabase();
|
||||
await AppDatabase.instance.initDatabase();
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user