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:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:youmazgestion/Views/HandleProduct.dart';
|
import 'package:youmazgestion/Views/HandleProduct.dart';
|
||||||
import 'package:youmazgestion/Views/RoleListPage.dart';
|
import 'package:youmazgestion/Views/RoleListPage.dart';
|
||||||
|
import 'package:youmazgestion/Views/commandManagement.dart';
|
||||||
import 'package:youmazgestion/Views/historique.dart';
|
import 'package:youmazgestion/Views/historique.dart';
|
||||||
import 'package:youmazgestion/Views/addProduct.dart';
|
|
||||||
import 'package:youmazgestion/Views/bilanMois.dart';
|
import 'package:youmazgestion/Views/bilanMois.dart';
|
||||||
import 'package:youmazgestion/Views/gestionProduct.dart';
|
|
||||||
import 'package:youmazgestion/Views/gestionStock.dart';
|
import 'package:youmazgestion/Views/gestionStock.dart';
|
||||||
import 'package:youmazgestion/Views/listUser.dart';
|
import 'package:youmazgestion/Views/listUser.dart';
|
||||||
import 'package:youmazgestion/Views/loginPage.dart';
|
import 'package:youmazgestion/Views/loginPage.dart';
|
||||||
|
import 'package:youmazgestion/Views/newCommand.dart';
|
||||||
import 'package:youmazgestion/Views/registrationPage.dart';
|
import 'package:youmazgestion/Views/registrationPage.dart';
|
||||||
import 'package:youmazgestion/Views/gestionRole.dart';
|
|
||||||
import 'package:youmazgestion/accueil.dart';
|
import 'package:youmazgestion/accueil.dart';
|
||||||
import 'package:youmazgestion/controller/userController.dart';
|
import 'package:youmazgestion/controller/userController.dart';
|
||||||
|
|
||||||
@ -31,201 +30,230 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
return Drawer(
|
return Drawer(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
children: [
|
children: [
|
||||||
|
// Header avec informations utilisateur
|
||||||
GetBuilder<UserController>(
|
GetBuilder<UserController>(
|
||||||
builder: (controller) => UserAccountsDrawerHeader(
|
builder: (controller) => Container(
|
||||||
accountEmail: Text(controller.email),
|
padding: const EdgeInsets.only(top: 50, left: 20, bottom: 20),
|
||||||
accountName: Text(controller.name),
|
|
||||||
currentAccountPicture: const CircleAvatar(
|
|
||||||
backgroundImage: AssetImage("assets/youmaz2.png"),
|
|
||||||
),
|
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [Colors.white, Color.fromARGB(255, 4, 54, 95)],
|
colors: [Color.fromARGB(255, 4, 54, 95), Colors.blue],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
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),
|
// Section Accueil
|
||||||
iconColor: Colors.lightBlueAccent,
|
_buildDrawerItem(
|
||||||
title: const Text("Accueil"),
|
icon: Icons.home,
|
||||||
onTap: () async {
|
title: "Accueil",
|
||||||
bool hasPermission = await userController.hasPermission('view', '/accueil');
|
color: Colors.blue,
|
||||||
if (hasPermission) {
|
permissionAction: 'view',
|
||||||
Get.to(const AccueilPage());
|
permissionRoute: '/accueil',
|
||||||
} else {
|
onTap: () => Get.to(const AccueilPage()),
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
|
||||||
ListTile(
|
// Section Utilisateurs
|
||||||
leading: const Icon(Icons.bar_chart),
|
const Padding(
|
||||||
title: const Text("Bilan"),
|
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
||||||
onTap: () async {
|
child: Text(
|
||||||
bool hasPermission = await userController.hasPermission('read', '/bilan');
|
"GESTION UTILISATEURS",
|
||||||
if (hasPermission) {
|
style: TextStyle(
|
||||||
Get.to(const BilanMois());
|
color: Colors.grey,
|
||||||
} else {
|
fontSize: 12,
|
||||||
Get.snackbar(
|
fontWeight: FontWeight.bold,
|
||||||
"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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
_buildDrawerItem(
|
||||||
leading: const Icon(Icons.warning_amber),
|
icon: Icons.person_add,
|
||||||
title: const Text("Gérer les rôles"),
|
title: "Ajouter un utilisateur",
|
||||||
onTap: () async {
|
color: Colors.green,
|
||||||
bool hasPermission = await userController.hasPermission('admin', '/gerer-roles');
|
permissionAction: 'create',
|
||||||
if (hasPermission) {
|
permissionRoute: '/ajouter-utilisateur',
|
||||||
Get.to(const RoleListPage());
|
onTap: () => Get.to(const RegistrationPage()),
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
_buildDrawerItem(
|
||||||
leading: const Icon(Icons.inventory),
|
icon: Icons.supervised_user_circle,
|
||||||
iconColor: Colors.blueAccent,
|
title: "Gérer les utilisateurs",
|
||||||
title: const Text("Gestion de stock"),
|
color: Color.fromARGB(255, 4, 54, 95),
|
||||||
onTap: () async {
|
permissionAction: 'update',
|
||||||
bool hasPermission = await userController.hasPermission('update', '/gestion-stock');
|
permissionRoute: '/modifier-utilisateur',
|
||||||
if (hasPermission) {
|
onTap: () => Get.to(const ListUserPage()),
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.history),
|
// Section Produits
|
||||||
iconColor: Colors.blue,
|
const Padding(
|
||||||
title: const Text("Historique"),
|
padding: EdgeInsets.only(left: 20, top: 15, bottom: 5),
|
||||||
onTap: () {
|
child: Text(
|
||||||
Get.to(HistoryPage());
|
"GESTION PRODUITS",
|
||||||
},
|
style: TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
_buildDrawerItem(
|
||||||
leading: const Icon(Icons.logout),
|
icon: Icons.inventory,
|
||||||
iconColor: Colors.red,
|
title: "Gestion des produits",
|
||||||
title: const Text("Déconnexion"),
|
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: () {
|
onTap: () {
|
||||||
Get.defaultDialog(
|
Get.defaultDialog(
|
||||||
title: "Déconnexion",
|
title: "Déconnexion",
|
||||||
content: const Text("Voulez-vous vraiment vous déconnecter ?"),
|
content: const Text("Voulez-vous vraiment vous déconnecter ?"),
|
||||||
actions: [
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
child: const Text("Non"),
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
child: const Text("Oui"),
|
child: const Text("Oui"),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
clearUserData();
|
clearUserData();
|
||||||
Get.offAll(const LoginPage());
|
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 {
|
Future<void> insertDefaultPermissions() async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
final existing = await db.query('permissions');
|
final existing = await db.query('permissions');
|
||||||
if (existing.isEmpty) {
|
if (existing.isEmpty) {
|
||||||
await db.insert('permissions', {'name': 'view'});
|
await db.insert('permissions', {'name': 'view'});
|
||||||
await db.insert('permissions', {'name': 'create'});
|
await db.insert('permissions', {'name': 'create'});
|
||||||
await db.insert('permissions', {'name': 'update'});
|
await db.insert('permissions', {'name': 'update'});
|
||||||
await db.insert('permissions', {'name': 'delete'});
|
await db.insert('permissions', {'name': 'delete'});
|
||||||
await db.insert('permissions', {'name': 'admin'});
|
await db.insert('permissions', {'name': 'admin'});
|
||||||
print("Permissions par défaut insérées");
|
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 {
|
Future<void> insertDefaultMenus() async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
final existingMenus = await db.query('menu');
|
final existingMenus = await db.query('menu');
|
||||||
|
|
||||||
if (existingMenus.isEmpty) {
|
if (existingMenus.isEmpty) {
|
||||||
await db.insert('menu', {'name': 'Accueil', 'route': '/accueil'});
|
// Menus existants
|
||||||
await db.insert('menu', {'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'});
|
await db.insert('menu', {'name': 'Accueil', 'route': '/accueil'});
|
||||||
await db.insert('menu', {'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'});
|
await db.insert('menu', {'name': 'Ajouter un utilisateur', 'route': '/ajouter-utilisateur'});
|
||||||
await db.insert('menu', {'name': 'Ajouter un produit', 'route': '/ajouter-produit'});
|
await db.insert('menu', {'name': 'Modifier/Supprimer un utilisateur', 'route': '/modifier-utilisateur'});
|
||||||
await db.insert('menu', {'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'});
|
await db.insert('menu', {'name': 'Ajouter un produit', 'route': '/ajouter-produit'});
|
||||||
await db.insert('menu', {'name': 'Bilan', 'route': '/bilan'});
|
await db.insert('menu', {'name': 'Modifier/Supprimer un produit', 'route': '/modifier-produit'});
|
||||||
await db.insert('menu', {'name': 'Gérer les rôles', 'route': '/gerer-roles'});
|
await db.insert('menu', {'name': 'Bilan', 'route': '/bilan'});
|
||||||
await db.insert('menu', {'name': 'Gestion de stock', 'route': '/gestion-stock'});
|
await db.insert('menu', {'name': 'Gérer les rôles', 'route': '/gerer-roles'});
|
||||||
await db.insert('menu', {'name': 'Historique', 'route': '/historique'});
|
await db.insert('menu', {'name': 'Gestion de stock', 'route': '/gestion-stock'});
|
||||||
await db.insert('menu', {'name': 'Déconnexion', 'route': '/deconnexion'});
|
await db.insert('menu', {'name': 'Historique', 'route': '/historique'});
|
||||||
print("Menus par défaut insérés");
|
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 {
|
Future<void> insertDefaultRoles() async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
@ -197,69 +238,94 @@ class AppDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assigner quelques permissions à l'Admin et à l'User
|
// Assigner quelques permissions à l'Admin et à l'User pour les nouveaux menus
|
||||||
final viewPermission = await db.query('permissions', where: 'name = ?', whereArgs: ['view']);
|
await _assignBasicPermissionsToRoles(db, adminRoleId, userRoleId);
|
||||||
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'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Rôles par défaut créés et permissions assignées");
|
print("Rôles par défaut créés et permissions assignées");
|
||||||
} else {
|
} else {
|
||||||
// Si les rôles existent déjà, vérifier et ajouter les permissions manquantes pour le Super Admin
|
// Si les rôles existent déjà, vérifier et ajouter les permissions manquantes
|
||||||
final superAdminRole = await db.query('roles', where: 'designation = ?', whereArgs: ['Super Admin']);
|
await _updateExistingRolePermissions(db);
|
||||||
if (superAdminRole.isNotEmpty) {
|
}
|
||||||
final superAdminRoleId = superAdminRole.first['id'] as int;
|
}
|
||||||
final permissions = await db.query('permissions');
|
// Nouvelle méthode pour assigner les permissions de base aux nouveaux menus
|
||||||
final menus = await db.query('menu');
|
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
|
// Récupérer les IDs des nouveaux menus
|
||||||
for (var menu in menus) {
|
final nouvelleCommandeMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/nouvelle-commande']);
|
||||||
for (var permission in permissions) {
|
final gererCommandesMenu = await db.query('menu', where: 'route = ?', whereArgs: ['/gerer-commandes']);
|
||||||
final existingPermission = await db.query(
|
|
||||||
'role_menu_permissions',
|
if (nouvelleCommandeMenu.isNotEmpty && createPermission.isNotEmpty) {
|
||||||
where: 'role_id = ? AND menu_id = ? AND permission_id = ?',
|
// Admin peut créer de nouvelles commandes
|
||||||
whereArgs: [superAdminRoleId, menu['id'], permission['id']],
|
await db.insert('role_menu_permissions', {
|
||||||
);
|
'role_id': adminRoleId,
|
||||||
if (existingPermission.isEmpty) {
|
'menu_id': nouvelleCommandeMenu.first['id'],
|
||||||
await db.insert('role_menu_permissions', {
|
'permission_id': createPermission.first['id'],
|
||||||
'role_id': superAdminRoleId,
|
});
|
||||||
'menu_id': menu['id'],
|
|
||||||
'permission_id': permission['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);
|
final file = File(path);
|
||||||
if (await file.exists()) {
|
if (await file.exists()) {
|
||||||
await file.delete();
|
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 {
|
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:path_provider/path_provider.dart';
|
||||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||||
import '../Models/produit.dart';
|
import '../Models/produit.dart';
|
||||||
|
import '../Models/client.dart';
|
||||||
|
|
||||||
|
|
||||||
class ProductDatabase {
|
class ProductDatabase {
|
||||||
static final ProductDatabase instance = ProductDatabase._init();
|
static final ProductDatabase instance = ProductDatabase._init();
|
||||||
@ -25,6 +27,8 @@ class ProductDatabase {
|
|||||||
Future<void> initDatabase() async {
|
Future<void> initDatabase() async {
|
||||||
_database = await _initDB('products2.db');
|
_database = await _initDB('products2.db');
|
||||||
await _createDB(_database, 1);
|
await _createDB(_database, 1);
|
||||||
|
await _insertDefaultClients();
|
||||||
|
await _insertDefaultCommandes();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Database> _initDB(String filePath) async {
|
Future<Database> _initDB(String filePath) async {
|
||||||
@ -33,74 +37,129 @@ class ProductDatabase {
|
|||||||
|
|
||||||
bool dbExists = await File(path).exists();
|
bool dbExists = await File(path).exists();
|
||||||
if (!dbExists) {
|
if (!dbExists) {
|
||||||
ByteData data = await rootBundle.load('assets/database/$filePath');
|
try {
|
||||||
List<int> bytes =
|
ByteData data = await rootBundle.load('assets/database/$filePath');
|
||||||
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
|
List<int> bytes =
|
||||||
await File(path).writeAsBytes(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);
|
return await databaseFactoryFfi.openDatabase(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _createDB(Database db, int version) async {
|
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 tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'");
|
final tableNames = tables.map((row) => row['name'] as String).toList();
|
||||||
final tableNames = tables.map((row) => row['name'] as String).toList();
|
|
||||||
|
|
||||||
// Si la table "products" n'existe pas encore, on la crée entièrement
|
// Table products (existante avec améliorations)
|
||||||
if (!tableNames.contains('products')) {
|
if (!tableNames.contains('products')) {
|
||||||
await db.execute('''
|
await db.execute('''
|
||||||
CREATE TABLE products(
|
CREATE TABLE products(
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT,
|
name TEXT NOT NULL,
|
||||||
price REAL,
|
price REAL NOT NULL,
|
||||||
image TEXT,
|
image TEXT,
|
||||||
category TEXT,
|
category TEXT NOT NULL,
|
||||||
stock INTEGER,
|
stock INTEGER NOT NULL DEFAULT 0,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
qrCode TEXT,
|
qrCode TEXT,
|
||||||
reference TEXT
|
reference TEXT UNIQUE
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
print("Table 'products' créée avec toutes les colonnes.");
|
print("Table 'products' créée.");
|
||||||
} else {
|
} else {
|
||||||
// Vérifie si les colonnes "description", "qrCode" et "reference" existent déjà
|
// 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 columns = await db.rawQuery('PRAGMA table_info(products)');
|
||||||
final columnNames = columns.map((e) => e['name'] as String).toList();
|
final columnNames = columns.map((e) => e['name'] as String).toList();
|
||||||
|
|
||||||
// Ajoute la colonne "description" si elle n'existe pas
|
|
||||||
if (!columnNames.contains('description')) {
|
if (!columnNames.contains('description')) {
|
||||||
try {
|
await db.execute("ALTER TABLE products ADD COLUMN description TEXT");
|
||||||
await db.execute("ALTER TABLE products ADD COLUMN description TEXT");
|
print("Colonne 'description' ajoutée.");
|
||||||
print("Colonne 'description' ajoutée.");
|
|
||||||
} catch (e) {
|
|
||||||
print("Erreur ajout colonne description : $e");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajoute la colonne "qrCode" si elle n'existe pas
|
|
||||||
if (!columnNames.contains('qrCode')) {
|
if (!columnNames.contains('qrCode')) {
|
||||||
try {
|
await db.execute("ALTER TABLE products ADD COLUMN qrCode TEXT");
|
||||||
await db.execute("ALTER TABLE products ADD COLUMN qrCode TEXT");
|
print("Colonne 'qrCode' ajoutée.");
|
||||||
print("Colonne 'qrCode' ajoutée.");
|
|
||||||
} catch (e) {
|
|
||||||
print("Erreur ajout colonne qrCode : $e");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajoute la colonne "reference" si elle n'existe pas
|
|
||||||
if (!columnNames.contains('reference')) {
|
if (!columnNames.contains('reference')) {
|
||||||
try {
|
await db.execute("ALTER TABLE products ADD COLUMN reference TEXT");
|
||||||
await db.execute("ALTER TABLE products ADD COLUMN reference TEXT");
|
print("Colonne 'reference' ajoutée.");
|
||||||
print("Colonne 'reference' ajoutée.");
|
|
||||||
} catch (e) {
|
|
||||||
print("Erreur ajout colonne reference : $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 {
|
Future<int> createProduct(Product product) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
return await db.insert('products', product.toMap());
|
return await db.insert('products', product.toMap());
|
||||||
@ -108,7 +167,7 @@ class ProductDatabase {
|
|||||||
|
|
||||||
Future<List<Product>> getProducts() async {
|
Future<List<Product>> getProducts() async {
|
||||||
final db = await database;
|
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 List.generate(maps.length, (i) {
|
||||||
return Product.fromMap(maps[i]);
|
return Product.fromMap(maps[i]);
|
||||||
});
|
});
|
||||||
@ -135,7 +194,7 @@ class ProductDatabase {
|
|||||||
|
|
||||||
Future<List<String>> getCategories() async {
|
Future<List<String>> getCategories() async {
|
||||||
final db = await database;
|
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(
|
return List.generate(
|
||||||
result.length, (index) => result[index]['category'] as String);
|
result.length, (index) => result[index]['category'] as String);
|
||||||
}
|
}
|
||||||
@ -143,7 +202,7 @@ class ProductDatabase {
|
|||||||
Future<List<Product>> getProductsByCategory(String category) async {
|
Future<List<Product>> getProductsByCategory(String category) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
final maps = await db
|
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 List.generate(maps.length, (i) {
|
||||||
return Product.fromMap(maps[i]);
|
return Product.fromMap(maps[i]);
|
||||||
});
|
});
|
||||||
@ -154,19 +213,347 @@ class ProductDatabase {
|
|||||||
return await db
|
return await db
|
||||||
.rawUpdate('UPDATE products SET stock = ? WHERE id = ?', [stock, id]);
|
.rawUpdate('UPDATE products SET stock = ? WHERE id = ?', [stock, id]);
|
||||||
}
|
}
|
||||||
// Ajouter cette méthode dans la classe ProductDatabase
|
|
||||||
|
|
||||||
Future<Product?> getProductByReference(String reference) async {
|
Future<Product?> getProductByReference(String reference) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
final maps = await db.query(
|
final maps = await db.query(
|
||||||
'products',
|
'products',
|
||||||
where: 'reference = ?',
|
where: 'reference = ?',
|
||||||
whereArgs: [reference],
|
whereArgs: [reference],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (maps.isNotEmpty) {
|
if (maps.isNotEmpty) {
|
||||||
return Product.fromMap(maps.first);
|
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 'package:flutter/material.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'package:quantity_input/quantity_input.dart';
|
|
||||||
import 'package:youmazgestion/Models/produit.dart';
|
import 'package:youmazgestion/Models/produit.dart';
|
||||||
|
|
||||||
class ProductCard extends StatefulWidget {
|
class ProductCard extends StatefulWidget {
|
||||||
@ -163,7 +162,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: const Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
@ -300,81 +299,84 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: MouseRegion(
|
||||||
onTapDown: _onTapDown,
|
cursor: SystemMouseCursors.click,
|
||||||
onTapUp: _onTapUp,
|
child: GestureDetector(
|
||||||
onTapCancel: _onTapCancel,
|
onTapDown: _onTapDown,
|
||||||
onTap: () {
|
onTapUp: _onTapUp,
|
||||||
widget.onAddToCart(widget.product, selectedQuantity);
|
onTapCancel: _onTapCancel,
|
||||||
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
widget.onAddToCart(widget.product, selectedQuantity);
|
||||||
SnackBar(
|
|
||||||
content: Row(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
children: [
|
SnackBar(
|
||||||
const Icon(
|
content: Row(
|
||||||
Icons.shopping_cart,
|
children: [
|
||||||
color: Colors.white,
|
const Icon(
|
||||||
),
|
Icons.shopping_cart,
|
||||||
const SizedBox(width: 8),
|
color: Colors.white,
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'${widget.product.name} (x$selectedQuantity) ajouté au panier',
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
],
|
Expanded(
|
||||||
),
|
child: Text(
|
||||||
backgroundColor: Colors.green,
|
'${widget.product.name} (x$selectedQuantity) ajouté au panier',
|
||||||
duration: const Duration(seconds: 1),
|
overflow: TextOverflow.ellipsis,
|
||||||
behavior: SnackBarBehavior.floating,
|
),
|
||||||
shape: RoundedRectangleBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(10),
|
],
|
||||||
),
|
),
|
||||||
),
|
backgroundColor: Colors.green,
|
||||||
);
|
duration: const Duration(seconds: 1),
|
||||||
},
|
behavior: SnackBarBehavior.floating,
|
||||||
child: Container(
|
shape: RoundedRectangleBorder(
|
||||||
padding: const EdgeInsets.symmetric(
|
borderRadius: BorderRadius.circular(10),
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
|
},
|
||||||
|
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:intl/intl.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:quantity_input/quantity_input.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
|
import 'package:youmazgestion/Views/particles.dart' show ParticleBackground;
|
||||||
import 'package:youmazgestion/Views/produitsCard.dart';
|
import 'package:youmazgestion/Views/produitsCard.dart';
|
||||||
@ -39,27 +38,21 @@ class _AccueilPageState extends State<AccueilPage> {
|
|||||||
int selectedQuantity = 1;
|
int selectedQuantity = 1;
|
||||||
double totalCartPrice = 0;
|
double totalCartPrice = 0;
|
||||||
double amountPaid = 0;
|
double amountPaid = 0;
|
||||||
|
final TextEditingController _amountController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
initorder();
|
_initializeDatabases();
|
||||||
initwork();
|
|
||||||
loadUserData();
|
loadUserData();
|
||||||
productsFuture = _initDatabaseAndFetchProducts();
|
productsFuture = _initDatabaseAndFetchProducts();
|
||||||
_initializeRegister();
|
|
||||||
super.initState();
|
|
||||||
_initializeDatabases();
|
|
||||||
loadUserData();
|
|
||||||
productsFuture = _initDatabaseAndFetchProducts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<void> _initializeDatabases() async {
|
Future<void> _initializeDatabases() async {
|
||||||
await orderDatabase.initDatabase();
|
await orderDatabase.initDatabase();
|
||||||
await workDatabase.initDatabase(); // Attendre l'initialisation complète
|
await workDatabase.initDatabase();
|
||||||
await _initializeRegister();
|
await _initializeRegister();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeRegister() async {
|
Future<void> _initializeRegister() async {
|
||||||
if (!MyApp.isRegisterOpen) {
|
if (!MyApp.isRegisterOpen) {
|
||||||
@ -86,6 +79,30 @@ Future<void> _initializeDatabases() async {
|
|||||||
final dateTime = DateTime.now().toString();
|
final dateTime = DateTime.now().toString();
|
||||||
String user = userController.username;
|
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(
|
orderId = await orderDatabase.insertOrder(
|
||||||
totalPrice, dateTime, MyApp.startDate!, user);
|
totalPrice, dateTime, MyApp.startDate!, user);
|
||||||
|
|
||||||
@ -100,7 +117,14 @@ Future<void> _initializeDatabases() async {
|
|||||||
final updatedStock = product.stock! - quantity;
|
final updatedStock = product.stock! - quantity;
|
||||||
await productDatabase.updateStock(product.id!, updatedStock);
|
await productDatabase.updateStock(product.id!, updatedStock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Afficher le ticket et réinitialiser le panier
|
||||||
showTicketPage();
|
showTicketPage();
|
||||||
|
setState(() {
|
||||||
|
selectedProducts.clear();
|
||||||
|
_amountController.clear();
|
||||||
|
amountPaid = 0;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, List<Product>>> _initDatabaseAndFetchProducts() async {
|
Future<Map<String, List<Product>>> _initDatabaseAndFetchProducts() async {
|
||||||
@ -119,14 +143,6 @@ Future<void> _initializeDatabases() async {
|
|||||||
return productsByCategory;
|
return productsByCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
void initorder() async {
|
|
||||||
await orderDatabase.initDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
void initwork() async {
|
|
||||||
await workDatabase.initDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
double calculateTotalPrice() {
|
double calculateTotalPrice() {
|
||||||
double totalPrice = 0;
|
double totalPrice = 0;
|
||||||
for (final cartItem in selectedProducts) {
|
for (final cartItem in selectedProducts) {
|
||||||
@ -159,49 +175,24 @@ Future<void> _initializeDatabases() async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void showTicketPage() {
|
void showTicketPage() {
|
||||||
final double totalCartPrice = calculateTotalPrice();
|
Get.offAll(TicketPage(
|
||||||
|
businessName: 'Youmaz',
|
||||||
if (selectedProducts.isNotEmpty) {
|
businessAddress:
|
||||||
if (amountPaid >= totalCartPrice) {
|
'quartier escale, Diourbel, Sénégal, en face de Sonatel',
|
||||||
Get.offAll(TicketPage(
|
businessPhoneNumber: '77 446 92 68',
|
||||||
businessName: 'Youmaz',
|
cartItems: selectedProducts,
|
||||||
businessAddress:
|
totalCartPrice: calculateTotalPrice(),
|
||||||
'quartier escale, Diourbel, Sénégal, en face de Sonatel',
|
amountPaid: amountPaid,
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@override
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: "Accueil",
|
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(),
|
drawer: CustomDrawer(),
|
||||||
body: ParticleBackground(
|
body: ParticleBackground(
|
||||||
@ -216,98 +207,159 @@ Future<void> _initializeDatabases() async {
|
|||||||
future: productsFuture,
|
future: productsFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
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) {
|
} 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) {
|
} else if (snapshot.hasData) {
|
||||||
final productsByCategory = snapshot.data!;
|
final productsByCategory = snapshot.data!;
|
||||||
final categories = productsByCategory.keys.toList();
|
final categories = productsByCategory.keys.toList();
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
|
// Section produits
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: ListView.builder(
|
child: Container(
|
||||||
itemCount: categories.length,
|
padding:const EdgeInsets.all(8),
|
||||||
itemBuilder: (context, index) {
|
decoration: BoxDecoration(
|
||||||
final category = categories[index];
|
|
||||||
final products = productsByCategory[category]!;
|
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(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Container(
|
||||||
child: Padding(
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding:const EdgeInsets.all(12),
|
||||||
child: Text(
|
decoration: BoxDecoration(
|
||||||
category,
|
color: Color.fromARGB(255, 4, 54, 95),
|
||||||
style: const TextStyle(
|
borderRadius: BorderRadius.circular(12),
|
||||||
fontSize: 22,
|
boxShadow: [
|
||||||
fontWeight: FontWeight.bold,
|
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(
|
||||||
GridView.builder(
|
gridDelegate:
|
||||||
gridDelegate:
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
crossAxisCount: 4,
|
||||||
crossAxisCount: 4,
|
childAspectRatio: 0.9,
|
||||||
childAspectRatio: 1,
|
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(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
borderRadius: const BorderRadius.only(
|
borderRadius:const BorderRadius.only(
|
||||||
topLeft: Radius.circular(20),
|
topLeft: Radius.circular(20),
|
||||||
bottomLeft: Radius.circular(20),
|
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.1),
|
color: Colors.black.withOpacity(0.2),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
spreadRadius: 2,
|
spreadRadius: 2,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Container(
|
||||||
'Panier',
|
padding: EdgeInsets.all(12),
|
||||||
style: TextStyle(
|
decoration: BoxDecoration(
|
||||||
fontSize: 20,
|
color: Color.fromARGB(255, 4, 54, 95),
|
||||||
fontWeight: FontWeight.bold,
|
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(
|
Expanded(
|
||||||
child: selectedProducts.isEmpty
|
child: selectedProducts.isEmpty
|
||||||
? const Center(
|
? const Center(
|
||||||
child: Text(
|
child: Column(
|
||||||
"Votre panier est vide",
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
style: TextStyle(fontSize: 16),
|
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(
|
: ListView.builder(
|
||||||
@ -315,86 +367,135 @@ Future<void> _initializeDatabases() async {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final cartItem = selectedProducts[index];
|
final cartItem = selectedProducts[index];
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(
|
margin: EdgeInsets.symmetric(vertical: 4),
|
||||||
vertical: 4),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
elevation: 2,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
|
contentPadding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 12, vertical: 4),
|
||||||
|
leading: Icon(Icons.shopping_basket,
|
||||||
|
color: Color.fromARGB(255, 4, 54, 95),),
|
||||||
title: Text(
|
title: Text(
|
||||||
cartItem.product.name,
|
cartItem.product.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold),
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
'${NumberFormat('#,##0').format(cartItem.product.price)} FCFA x ${cartItem.quantity}',
|
'${NumberFormat('#,##0').format(cartItem.product.price)} FCFA x ${cartItem.quantity}',
|
||||||
style: const TextStyle(
|
style:const TextStyle(fontSize: 14),
|
||||||
fontSize: 14),
|
|
||||||
),
|
),
|
||||||
trailing: IconButton(
|
trailing: Row(
|
||||||
icon: const Icon(Icons.delete,
|
mainAxisSize: MainAxisSize.min,
|
||||||
color: Colors.red),
|
children: [
|
||||||
onPressed: () {
|
Text(
|
||||||
setState(() {
|
'${NumberFormat('#,##0').format(cartItem.product.price * cartItem.quantity)}',
|
||||||
selectedProducts
|
style:const TextStyle(
|
||||||
.removeAt(index);
|
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(
|
// Total et paiement
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
Container(
|
||||||
child: Text(
|
padding: EdgeInsets.all(12),
|
||||||
'Total: ${NumberFormat('#,##0.00').format(calculateTotalPrice())} FCFA',
|
decoration: BoxDecoration(
|
||||||
style: const TextStyle(
|
color: Colors.white,
|
||||||
fontSize: 18,
|
borderRadius: BorderRadius.circular(12),
|
||||||
fontWeight: FontWeight.bold,
|
boxShadow: [
|
||||||
),
|
BoxShadow(
|
||||||
textAlign: TextAlign.center,
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
child: Column(
|
||||||
TextField(
|
children: [
|
||||||
keyboardType: TextInputType.number,
|
Row(
|
||||||
decoration: InputDecoration(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
labelText: 'Montant payé',
|
children: [
|
||||||
border: OutlineInputBorder(
|
const Text('Total:',
|
||||||
borderRadius: BorderRadius.circular(10),
|
style: TextStyle(
|
||||||
),
|
fontSize: 16,
|
||||||
filled: true,
|
fontWeight: FontWeight.bold)),
|
||||||
fillColor: Colors.white,
|
Text(
|
||||||
),
|
'${NumberFormat('#,##0.00').format(calculateTotalPrice())} FCFA',
|
||||||
onChanged: (value) {
|
style: TextStyle(
|
||||||
amountPaid = double.tryParse(value) ?? 0;
|
fontSize: 18,
|
||||||
},
|
fontWeight: FontWeight.bold,
|
||||||
),
|
color: Color.fromARGB(255, 4, 54, 95),),
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
ElevatedButton(
|
],
|
||||||
style: ElevatedButton.styleFrom(
|
),
|
||||||
backgroundColor: Colors.green,
|
const SizedBox(height: 12),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
TextField(
|
||||||
shape: RoundedRectangleBorder(
|
controller: _amountController,
|
||||||
borderRadius: BorderRadius.circular(10),
|
keyboardType: TextInputType.number,
|
||||||
),
|
decoration: InputDecoration(
|
||||||
),
|
labelText: 'Montant payé',
|
||||||
onPressed: saveOrderToDatabase,
|
prefixIcon: Icon(Icons.attach_money),
|
||||||
child: const Text(
|
border: OutlineInputBorder(
|
||||||
'Valider la commande',
|
borderRadius: BorderRadius.circular(10),
|
||||||
style: TextStyle(
|
),
|
||||||
fontSize: 16,
|
filled: true,
|
||||||
fontWeight: FontWeight.bold,
|
fillColor: Colors.grey[100],
|
||||||
color: Colors.white),
|
),
|
||||||
|
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 {
|
} 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 {
|
try {
|
||||||
// Initialiser les bases de données une seule fois
|
// Initialiser les bases de données une seule fois
|
||||||
// await AppDatabase.instance.deleteDatabaseFile();
|
// await AppDatabase.instance.deleteDatabaseFile();
|
||||||
|
//await ProductDatabase.instance.deleteDatabaseFile();
|
||||||
|
|
||||||
await ProductDatabase.instance.initDatabase();
|
await ProductDatabase.instance.initDatabase();
|
||||||
await AppDatabase.instance.initDatabase();
|
await AppDatabase.instance.initDatabase();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user