Browse Source

last last last update

06062025_01
b.razafimandimbihery 6 months ago
parent
commit
831cce13da
  1. BIN
      assets/fa-solid-900.ttf
  2. BIN
      assets/fonts/Roboto-Italic.ttf
  3. 135
      lib/Components/appDrawer.dart
  4. 14
      lib/Components/app_bar.dart
  5. 15
      lib/Models/Client.dart
  6. 80
      lib/Models/produit.dart
  7. 635
      lib/Services/stock_managementDatabase.dart
  8. 360
      lib/Views/Dashboard.dart
  9. 2837
      lib/Views/HandleProduct.dart
  10. 892
      lib/Views/commandManagement.dart
  11. 918
      lib/Views/historique.dart
  12. 183
      lib/Views/loginPage.dart
  13. 1151
      lib/Views/mobilepage.dart
  14. 669
      lib/Views/newCommand.dart
  15. 5
      lib/Views/registrationPage.dart
  16. 1
      lib/main.dart
  17. 8
      pubspec.lock
  18. 3
      pubspec.yaml

BIN
assets/fa-solid-900.ttf

Binary file not shown.

BIN
assets/fonts/Roboto-Italic.ttf

Binary file not shown.

135
lib/Components/appDrawer.dart

@ -298,26 +298,123 @@ class CustomDrawer extends StatelessWidget {
leading: const Icon(Icons.logout, color: Colors.red), leading: const Icon(Icons.logout, color: Colors.red),
title: const Text("Déconnexion"), title: const Text("Déconnexion"),
onTap: () { onTap: () {
Get.defaultDialog( Get.dialog(
title: "Déconnexion", AlertDialog(
content: const Text("Voulez-vous vraiment vous déconnecter ?"), shape: RoundedRectangleBorder(
actions: [ borderRadius: BorderRadius.circular(16),
TextButton( ),
child: const Text("Non"), contentPadding: EdgeInsets.zero,
onPressed: () => Get.back(), content: Container(
), constraints: const BoxConstraints(maxWidth: 400),
ElevatedButton( child: Column(
style: ElevatedButton.styleFrom( mainAxisSize: MainAxisSize.min,
backgroundColor: Colors.red, children: [
// Header
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
child: Column(
children: [
Icon(
Icons.logout_rounded,
size: 48,
color: Colors.orange.shade600,
), ),
child: const Text("Oui"), const SizedBox(height: 16),
onPressed: () async { const Text(
await clearUserData(); "Déconnexion",
Get.offAll(const LoginPage()); style: TextStyle(
}, fontSize: 20,
), fontWeight: FontWeight.w600,
], color: Colors.black87,
); ),
),
const SizedBox(height: 12),
const Text(
"Êtes-vous sûr de vouloir vous déconnecter ?",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Colors.black87,
height: 1.4,
),
),
const SizedBox(height: 8),
Text(
"Vous devrez vous reconnecter pour accéder à votre compte.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
height: 1.3,
),
),
],
),
),
// Actions
Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Get.back(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
side: BorderSide(
color: Colors.grey.shade300,
width: 1.5,
),
),
child: const Text(
"Annuler",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () async {
await clearUserData();
Get.offAll(const LoginPage());
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
foregroundColor: Colors.white,
elevation: 2,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
"Se déconnecter",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
],
),
),
),
barrierDismissible: true,
);
}, },
), ),
); );

14
lib/Components/app_bar.dart

@ -8,9 +8,10 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final List<Widget>? actions; final List<Widget>? actions;
final bool automaticallyImplyLeading; final bool automaticallyImplyLeading;
final Color? backgroundColor; final Color? backgroundColor;
final bool isDesktop; // Add this parameter
final UserController userController = Get.put(UserController()); final UserController userController = Get.put(UserController());
CustomAppBar({ CustomAppBar({
Key? key, Key? key,
required this.title, required this.title,
@ -18,11 +19,12 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
this.actions, this.actions,
this.automaticallyImplyLeading = true, this.automaticallyImplyLeading = true,
this.backgroundColor, this.backgroundColor,
this.isDesktop = false, // Add this parameter with default value
}) : super(key: key); }) : super(key: key);
@override @override
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 80.0); Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 80.0);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
@ -78,7 +80,9 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Obx(() => Text( Obx(() => Text(
userController.role!='Super Admin'?'Point de vente: ${userController.pointDeVenteDesignation}':'', userController.role != 'Super Admin'
? 'Point de vente: ${userController.pointDeVenteDesignation}'
: '',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w400, fontWeight: FontWeight.w400,

15
lib/Models/Client.dart

@ -52,9 +52,6 @@ class Client {
enum StatutCommande { enum StatutCommande {
enAttente, enAttente,
confirmee, confirmee,
enPreparation,
expediee,
livree,
annulee annulee
} }
@ -128,12 +125,12 @@ class Commande {
return 'En attente'; return 'En attente';
case StatutCommande.confirmee: case StatutCommande.confirmee:
return 'Confirmée'; return 'Confirmée';
case StatutCommande.enPreparation: // case StatutCommande.enPreparation:
return 'En préparation'; // return 'En préparation';
case StatutCommande.expediee: // case StatutCommande.expediee:
return 'Expédiée'; // return 'Expédiée';
case StatutCommande.livree: // case StatutCommande.livree:
return 'Livrée'; // return 'Livrée';
case StatutCommande.annulee: case StatutCommande.annulee:
return 'Annulée'; return 'Annulée';
default: default:

80
lib/Models/produit.dart

@ -1,14 +1,18 @@
class Product { class Product {
int? id; final int? id;
final String name; final String name;
final double price; final double price;
final String? image; final String? image;
final String category; final String category;
final int? stock; final int stock;
final String? description; final String? description;
String? qrCode; String? qrCode;
final String? reference; final String? reference;
final int? pointDeVenteId; final int? pointDeVenteId;
final String? marque;
final String? ram;
final String? memoireInterne;
final String? imei;
Product({ Product({
this.id, this.id,
@ -17,12 +21,16 @@ class Product {
this.image, this.image,
required this.category, required this.category,
this.stock = 0, this.stock = 0,
this.description = '', this.description,
this.qrCode, this.qrCode,
this.reference, this.reference,
this.pointDeVenteId this.pointDeVenteId,
this.marque,
this.ram,
this.memoireInterne,
this.imei,
}); });
// Vérifie si le stock est défini
bool isStockDefined() { bool isStockDefined() {
if (stock != null) { if (stock != null) {
print("stock is defined : $stock $name"); print("stock is defined : $stock $name");
@ -31,33 +39,37 @@ class Product {
return false; return false;
} }
} }
Map<String, dynamic> toMap() { factory Product.fromMap(Map<String, dynamic> map) => Product(
return { id: map['id'],
'id': id, name: map['name'],
'name': name, price: map['price'],
'price': price, image: map['image'],
'image': image ?? '', category: map['category'],
'category': category, stock: map['stock'],
'stock': stock ?? 0, description: map['description'],
'description': description ?? '', qrCode: map['qrCode'],
'qrCode': qrCode ?? '', reference: map['reference'],
'reference': reference ?? '', pointDeVenteId: map['point_de_vente_id'],
'point_de_vente_id':pointDeVenteId marque: map['marque'],
}; ram: map['ram'],
} memoireInterne: map['memoire_interne'],
imei: map['imei'],
);
factory Product.fromMap(Map<String, dynamic> map) { Map<String, dynamic> toMap() => {
return Product( 'id': id,
id: map['id'], 'name': name,
name: map['name'], 'price': price,
price: map['price'], 'image': image,
image: map['image'], 'category': category,
category: map['category'], 'stock': stock,
stock: map['stock'], 'description': description,
description: map['description'], 'qrCode': qrCode,
qrCode: map['qrCode'], 'reference': reference,
reference: map['reference'], 'point_de_vente_id': pointDeVenteId,
pointDeVenteId : map['point_de_vente_id'] 'marque': marque,
); 'ram': ram,
} 'memoire_interne': memoireInterne,
'imei': imei,
};
} }

635
lib/Services/stock_managementDatabase.dart

@ -37,8 +37,8 @@ class AppDatabase {
await insertDefaultMenus(); await insertDefaultMenus();
await insertDefaultRoles(); await insertDefaultRoles();
await insertDefaultSuperAdmin(); await insertDefaultSuperAdmin();
await _insertDefaultClients(); // await _insertDefaultClients();
await _insertDefaultCommandes(); // await _insertDefaultCommandes();
await insertDefaultPointsDeVente(); // Ajouté ici await insertDefaultPointsDeVente(); // Ajouté ici
} }
@ -110,12 +110,19 @@ class AppDatabase {
} }
// --- POINTS DE VENTE --- // --- POINTS DE VENTE ---
if (!tableNames.contains('points_de_vente')) { if (!tableNames.contains('points_de_vente')) {
await db.execute('''CREATE TABLE points_de_vente ( await db.execute('''CREATE TABLE points_de_vente (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
designation TEXT NOT NULL UNIQUE nom TEXT NOT NULL UNIQUE
)'''); )''');
} } else {
// Si la table existe déjà, ajouter la colonne code si elle n'existe pas
try {
await db.execute('ALTER TABLE points_de_vente ADD COLUMN nom TEXT UNIQUE');
} catch (e) {
print("La colonne nom existe déjà dans la table points_de_vente");
}
}
// --- UTILISATEURS --- // --- UTILISATEURS ---
if (!tableNames.contains('users')) { if (!tableNames.contains('users')) {
@ -140,29 +147,54 @@ class AppDatabase {
} }
} }
// --- PRODUITS --- // Dans la méthode _createDB, modifier la partie concernant la table products
if (!tableNames.contains('products')) { if (!tableNames.contains('products')) {
await db.execute('''CREATE TABLE products ( await db.execute('''CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
price REAL NOT NULL, price REAL NOT NULL,
image TEXT, image TEXT,
category TEXT NOT NULL, category TEXT NOT NULL,
stock INTEGER NOT NULL DEFAULT 0, stock INTEGER NOT NULL DEFAULT 0,
description TEXT, description TEXT,
qrCode TEXT, qrCode TEXT,
reference TEXT UNIQUE, reference TEXT,
point_de_vente_id INTEGER, point_de_vente_id INTEGER,
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id) marque TEXT,
)'''); ram TEXT,
} else { memoire_interne TEXT,
// Si la table existe déjà, ajouter la colonne si elle n'existe pas imei TEXT UNIQUE,
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id)
)''');
} else {
// Si la table existe déjà, ajouter les colonnes si elles n'existent pas
final columns = await db.rawQuery('PRAGMA table_info(products)');
final columnNames = columns.map((col) => col['name'] as String).toList();
final newColumns = [
'marque',
'ram',
'memoire_interne',
'imei'
];
for (var column in newColumns) {
if (!columnNames.contains(column)) {
try { try {
await db.execute('ALTER TABLE products ADD COLUMN point_de_vente_id INTEGER REFERENCES points_de_vente(id)'); await db.execute('ALTER TABLE products ADD COLUMN $column TEXT');
} catch (e) { } catch (e) {
print("La colonne point_de_vente_id existe déjà dans la table products"); print("La colonne $column existe déjà dans la table products");
} }
} }
}
// Vérifier aussi point_de_vente_id au cas
try {
await db.execute('ALTER TABLE products ADD COLUMN point_de_vente_id INTEGER REFERENCES points_de_vente(id)');
} catch (e) {
print("La colonne point_de_vente_id existe déjà dans la table products");
}
}
// --- CLIENTS --- // --- CLIENTS ---
if (!tableNames.contains('clients')) { if (!tableNames.contains('clients')) {
@ -301,25 +333,61 @@ class AppDatabase {
}/* Copier depuis ton code */ } }/* Copier depuis ton code */ }
Future<void> insertDefaultPointsDeVente() async { Future<void> insertDefaultPointsDeVente() async {
final db = await database; final db = await database;
final existing = await db.query('points_de_vente'); final existing = await db.query('points_de_vente');
if (existing.isEmpty) { if (existing.isEmpty) {
final defaultPoints = [ final defaultPoints = [
{'designation': 'Behoririka'}, {'nom': '405A'},
{'designation': 'Antanimena'}, {'nom': '405B'},
{'designation': 'Analakely'}, {'nom': '416'},
{'designation': 'Andravoahangy'}, {'nom': 'S405A'},
{'designation': 'Anosy'}, {'nom': '417'},
]; ];
for (var point in defaultPoints) { for (var point in defaultPoints) {
await db.insert('points_de_vente', point); try {
await db.insert(
'points_de_vente',
point,
conflictAlgorithm: ConflictAlgorithm.ignore
);
} catch (e) {
print("Erreur insertion point de vente ${point['nom']}: $e");
}
} }
print("Points de vente par défaut insérés"); print("Points de vente par défaut insérés");
} }
} }
Future<void> debugPointsDeVenteTable() async {
final db = await database;
try {
// Vérifie si la table existe
final tables = await db.rawQuery(
"SELECT name FROM sqlite_master WHERE type='table' AND name='points_de_vente'"
);
if (tables.isEmpty) {
print("La table points_de_vente n'existe pas!");
return;
}
// Compte le nombre d'entrées
final count = await db.rawQuery("SELECT COUNT(*) as count FROM points_de_vente");
print("Nombre de points de vente: ${count.first['count']}");
// Affiche le contenu
final content = await db.query('points_de_vente');
print("Contenu de la table points_de_vente:");
for (var row in content) {
print("ID: ${row['id']}, Nom: ${row['nom']}");
}
} catch (e) {
print("Erreur debug table points_de_vente: $e");
}
}
Future<void> insertDefaultSuperAdmin() async { final db = await database; Future<void> insertDefaultSuperAdmin() async { final db = await database;
final existingSuperAdmin = await db.rawQuery(''' final existingSuperAdmin = await db.rawQuery('''
@ -557,13 +625,17 @@ Future<Users?> getUserById(int id) async {
Future<int> createProduct(Product product) async { Future<int> createProduct(Product product) async {
final db = await database; final db = await database;
// Récupérer le point de vente de l'utilisateur connecté // Si le produit a un point_de_vente_id, on l'utilise directement
if (product.pointDeVenteId != null && product.pointDeVenteId! > 0) {
return await db.insert('products', product.toMap());
}
// Sinon, on utilise le point de vente de l'utilisateur connecté
final userCtrl = Get.find<UserController>(); final userCtrl = Get.find<UserController>();
final currentPointDeVenteId = userCtrl.pointDeVenteId; final currentPointDeVenteId = userCtrl.pointDeVenteId;
// Si le produit na pas de point_de_vente_id, on lui assigne celui de l'utilisateur connecté
final Map<String, dynamic> productData = product.toMap(); final Map<String, dynamic> productData = product.toMap();
if (currentPointDeVenteId > 0 && (product.pointDeVenteId == null || product.pointDeVenteId == 0)) { if (currentPointDeVenteId > 0) {
productData['point_de_vente_id'] = currentPointDeVenteId; productData['point_de_vente_id'] = currentPointDeVenteId;
} }
@ -592,6 +664,19 @@ Future<int> updateProduct(Product product) async {
// where: 'id = ?', // where: 'id = ?',
// whereArgs: [product.id], // whereArgs: [product.id],
// );/* Copier depuis ton code */ } // );/* Copier depuis ton code */ }
Future<Product?> getProductById(int id) async {
final db = await database;
final maps = await db.query(
'products',
where: 'id = ?',
whereArgs: [id],
);
if (maps.isNotEmpty) {
return Product.fromMap(maps.first);
}
return null;
}
Future<int> deleteProduct(int? id) async { final db = await database; Future<int> deleteProduct(int? id) async { final db = await database;
return await db.delete( return await db.delete(
'products', 'products',
@ -739,6 +824,21 @@ Future<int> deleteCommande(int id) async {
} }
return null; return null;
} }
Future<Product?> getProductByIMEI(String imei) async {
final db = await database;
final maps = await db.query(
'products',
where: 'imei = ?',
whereArgs: [imei],
);
if (maps.isNotEmpty) {
return Product.fromMap(maps.first);
}
return null;
}
// Détails commandes // Détails commandes
// Créer un détail de commande // Créer un détail de commande
Future<int> createDetailCommande(DetailCommande detail) async { Future<int> createDetailCommande(DetailCommande detail) async {
@ -835,110 +935,110 @@ Future<int> updateStock(int productId, int newStock) async {
); );
} }
// Données par défaut // // Données par défaut
Future<void> _insertDefaultClients() async {final db = await database; // Future<void> _insertDefaultClients() async {final db = await database;
final existingClients = await db.query('clients'); // final existingClients = await db.query('clients');
if (existingClients.isEmpty) { // if (existingClients.isEmpty) {
final defaultClients = [ // final defaultClients = [
Client( // Client(
nom: 'Dupont', // nom: 'Dupont',
prenom: 'Jean', // prenom: 'Jean',
email: 'jean.dupont@email.com', // email: 'jean.dupont@email.com',
telephone: '0123456789', // telephone: '0123456789',
adresse: '123 Rue de la Paix, Paris', // adresse: '123 Rue de la Paix, Paris',
dateCreation: DateTime.now(), // dateCreation: DateTime.now(),
), // ),
Client( // Client(
nom: 'Martin', // nom: 'Martin',
prenom: 'Marie', // prenom: 'Marie',
email: 'marie.martin@email.com', // email: 'marie.martin@email.com',
telephone: '0987654321', // telephone: '0987654321',
adresse: '456 Avenue des Champs, Lyon', // adresse: '456 Avenue des Champs, Lyon',
dateCreation: DateTime.now(), // dateCreation: DateTime.now(),
), // ),
Client( // Client(
nom: 'Bernard', // nom: 'Bernard',
prenom: 'Pierre', // prenom: 'Pierre',
email: 'pierre.bernard@email.com', // email: 'pierre.bernard@email.com',
telephone: '0456789123', // telephone: '0456789123',
adresse: '789 Boulevard Saint-Michel, Marseille', // adresse: '789 Boulevard Saint-Michel, Marseille',
dateCreation: DateTime.now(), // dateCreation: DateTime.now(),
), // ),
]; // ];
for (var client in defaultClients) { // for (var client in defaultClients) {
await db.insert('clients', client.toMap()); // await db.insert('clients', client.toMap());
} // }
print("Clients par défaut insérés"); // print("Clients par défaut insérés");
} /* Copier depuis ton code */ } // } /* Copier depuis ton code */ }
Future<void> _insertDefaultCommandes() async { final db = await database; // Future<void> _insertDefaultCommandes() async { final db = await database;
final existingCommandes = await db.query('commandes'); // final existingCommandes = await db.query('commandes');
if (existingCommandes.isEmpty) { // if (existingCommandes.isEmpty) {
// Récupérer quelques produits pour créer des commandes // // Récupérer quelques produits pour créer des commandes
final produits = await db.query('products', limit: 3); // final produits = await db.query('products', limit: 3);
final clients = await db.query('clients', limit: 3); // final clients = await db.query('clients', limit: 3);
if (produits.isNotEmpty && clients.isNotEmpty) { // if (produits.isNotEmpty && clients.isNotEmpty) {
// Commande 1 // // Commande 1
final commande1Id = await db.insert('commandes', { // final commande1Id = await db.insert('commandes', {
'clientId': clients[0]['id'], // 'clientId': clients[0]['id'],
'dateCommande': DateTime.now().subtract(Duration(days: 5)).toIso8601String(), // 'dateCommande': DateTime.now().subtract(Duration(days: 5)).toIso8601String(),
'statut': StatutCommande.livree.index, // 'statut': StatutCommande.livree.index,
'montantTotal': 150.0, // 'montantTotal': 150.0,
'notes': 'Commande urgente', // 'notes': 'Commande urgente',
}); // });
await db.insert('details_commandes', { // await db.insert('details_commandes', {
'commandeId': commande1Id, // 'commandeId': commande1Id,
'produitId': produits[0]['id'], // 'produitId': produits[0]['id'],
'quantite': 2, // 'quantite': 2,
'prixUnitaire': 75.0, // 'prixUnitaire': 75.0,
'sousTotal': 150.0, // 'sousTotal': 150.0,
}); // });
// Commande 2 // // Commande 2
final commande2Id = await db.insert('commandes', { // final commande2Id = await db.insert('commandes', {
'clientId': clients[1]['id'], // 'clientId': clients[1]['id'],
'dateCommande': DateTime.now().subtract(Duration(days: 2)).toIso8601String(), // 'dateCommande': DateTime.now().subtract(Duration(days: 2)).toIso8601String(),
'statut': StatutCommande.enPreparation.index, // 'statut': StatutCommande.enPreparation.index,
'montantTotal': 225.0, // 'montantTotal': 225.0,
'notes': 'Livraison prévue demain', // 'notes': 'Livraison prévue demain',
}); // });
if (produits.length > 1) { // if (produits.length > 1) {
await db.insert('details_commandes', { // await db.insert('details_commandes', {
'commandeId': commande2Id, // 'commandeId': commande2Id,
'produitId': produits[1]['id'], // 'produitId': produits[1]['id'],
'quantite': 3, // 'quantite': 3,
'prixUnitaire': 75.0, // 'prixUnitaire': 75.0,
'sousTotal': 225.0, // 'sousTotal': 225.0,
}); // });
} // }
// Commande 3 // // Commande 3
final commande3Id = await db.insert('commandes', { // final commande3Id = await db.insert('commandes', {
'clientId': clients[2]['id'], // 'clientId': clients[2]['id'],
'dateCommande': DateTime.now().subtract(Duration(hours: 6)).toIso8601String(), // 'dateCommande': DateTime.now().subtract(Duration(hours: 6)).toIso8601String(),
'statut': StatutCommande.confirmee.index, // 'statut': StatutCommande.confirmee.index,
'montantTotal': 300.0, // 'montantTotal': 300.0,
'notes': 'Commande standard', // 'notes': 'Commande standard',
}); // });
if (produits.length > 2) { // if (produits.length > 2) {
await db.insert('details_commandes', { // await db.insert('details_commandes', {
'commandeId': commande3Id, // 'commandeId': commande3Id,
'produitId': produits[2]['id'], // 'produitId': produits[2]['id'],
'quantite': 4, // 'quantite': 4,
'prixUnitaire': 75.0, // 'prixUnitaire': 75.0,
'sousTotal': 300.0, // 'sousTotal': 300.0,
}); // });
} // }
print("Commandes par défaut insérées"); // print("Commandes par défaut insérées");
} // }
}/* Copier depuis ton code */ } // }/* Copier depuis ton code */ }
// Statistiques // Statistiques
Future<Map<String, dynamic>> getStatistiques() async { final db = await database; Future<Map<String, dynamic>> getStatistiques() async { final db = await database;
@ -1094,25 +1194,46 @@ Future<bool> hasPermission(String username, String permissionName, String menuRo
print("Base de données product supprimée"); print("Base de données product supprimée");
}/* Copier depuis ton code */ } }/* Copier depuis ton code */ }
// CRUD Points de vente // CRUD Points de vente
Future<int> createPointDeVente(String designation) async { // CRUD Points de vente
Future<int> createPointDeVente(String designation, String code) async {
final db = await database; final db = await database;
return await db.insert('points_de_vente', { return await db.insert('points_de_vente', {
'designation': designation 'designation': designation,
}, 'code': code
conflictAlgorithm: ConflictAlgorithm.ignore }, conflictAlgorithm: ConflictAlgorithm.ignore);
);
} }
Future<List<Map<String, dynamic>>> getPointsDeVente() async { Future<List<Map<String, dynamic>>> getPointsDeVente() async {
final db = await database; final db = await database;
return await db.query('points_de_vente', orderBy: 'designation ASC'); try {
final result = await db.query(
'points_de_vente',
orderBy: 'nom ASC',
where: 'nom IS NOT NULL AND nom != ""' // Filtre les noms vides
);
if (result.isEmpty) {
print("Aucun point de vente trouvé dans la base de données");
// Optionnel: Insérer les points de vente par défaut si table vide
await insertDefaultPointsDeVente();
return await db.query('points_de_vente', orderBy: 'nom ASC');
}
return result;
} catch (e) {
print("Erreur lors de la récupération des points de vente: $e");
return [];
}
} }
Future<int> updatePointDeVente(int id, String newDesignation) async { Future<int> updatePointDeVente(int id, String newDesignation, String newCode) async {
final db = await database; final db = await database;
return await db.update( return await db.update(
'points_de_vente', 'points_de_vente',
{'designation': newDesignation}, {
'designation': newDesignation,
'code': newCode
},
where: 'id = ?', where: 'id = ?',
whereArgs: [id], whereArgs: [id],
); );
@ -1139,6 +1260,8 @@ Future<Map<String, int>> getProductCountByCategory() async {
return Map.fromEntries(result.map((e) => return Map.fromEntries(result.map((e) =>
MapEntry(e['category'] as String, e['count'] as int))); MapEntry(e['category'] as String, e['count'] as int)));
} }
Future<Map<String, dynamic>?> getPointDeVenteById(int id) async { Future<Map<String, dynamic>?> getPointDeVenteById(int id) async {
final db = await database; final db = await database;
final result = await db.query( final result = await db.query(
@ -1148,4 +1271,238 @@ Future<Map<String, dynamic>?> getPointDeVenteById(int id) async {
); );
return result.isNotEmpty ? result.first : null; return result.isNotEmpty ? result.first : null;
} }
Future<int?> getOrCreatePointDeVenteByNom(String nom) async {
final db = await database;
// Vérifier si le point de vente existe déjà
final existing = await db.query(
'points_de_vente',
where: 'nom = ?',
whereArgs: [nom.trim()],
);
if (existing.isNotEmpty) {
return existing.first['id'] as int;
}
// Créer le point de vente s'il n'existe pas
try {
final id = await db.insert('points_de_vente', {
'nom': nom.trim()
});
print("Point de vente créé: $nom (ID: $id)");
return id;
} catch (e) {
print("Erreur lors de la création du point de vente $nom: $e");
return null;
}
}
Future<String?> getPointDeVenteNomById(int id) async {
if (id == 0 || id == null) return null;
final db = await database;
try {
final result = await db.query(
'points_de_vente',
where: 'id = ?',
whereArgs: [id],
limit: 1,
);
return result.isNotEmpty ? result.first['nom'] as String : null;
} catch (e) {
print("Erreur getPointDeVenteNomById: $e");
return null;
}
}
Future<List<Product>> searchProducts({
String? name,
String? imei,
String? reference,
bool onlyInStock = false,
String? category,
int? pointDeVenteId,
}) async {
final db = await database;
List<String> whereConditions = [];
List<dynamic> whereArgs = [];
if (name != null && name.isNotEmpty) {
whereConditions.add('name LIKE ?');
whereArgs.add('%$name%');
}
if (imei != null && imei.isNotEmpty) {
whereConditions.add('imei LIKE ?');
whereArgs.add('%$imei%');
}
if (reference != null && reference.isNotEmpty) {
whereConditions.add('reference LIKE ?');
whereArgs.add('%$reference%');
}
if (onlyInStock) {
whereConditions.add('stock > 0');
}
if (category != null && category.isNotEmpty) {
whereConditions.add('category = ?');
whereArgs.add(category);
}
if (pointDeVenteId != null && pointDeVenteId > 0) {
whereConditions.add('point_de_vente_id = ?');
whereArgs.add(pointDeVenteId);
}
String whereClause = whereConditions.isNotEmpty
? whereConditions.join(' AND ')
: '';
final maps = await db.query(
'products',
where: whereClause.isNotEmpty ? whereClause : null,
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
orderBy: 'name ASC',
);
return List.generate(maps.length, (i) => Product.fromMap(maps[i]));
}
// Obtenir le nombre de produits en stock par catégorie
Future<Map<String, Map<String, int>>> getStockStatsByCategory() async {
final db = await database;
final result = await db.rawQuery('''
SELECT
category,
COUNT(*) as total_products,
SUM(CASE WHEN stock > 0 THEN 1 ELSE 0 END) as in_stock,
SUM(CASE WHEN stock = 0 OR stock IS NULL THEN 1 ELSE 0 END) as out_of_stock,
SUM(stock) as total_stock
FROM products
GROUP BY category
ORDER BY category
''');
Map<String, Map<String, int>> stats = {};
for (var row in result) {
stats[row['category'] as String] = {
'total': row['total_products'] as int,
'in_stock': row['in_stock'] as int,
'out_of_stock': row['out_of_stock'] as int,
'total_stock': row['total_stock'] as int? ?? 0,
};
}
return stats;
}
// Recherche rapide par code-barres/QR/IMEI
Future<Product?> findProductByCode(String code) async {
final db = await database;
// Essayer de trouver par référence d'abord
var maps = await db.query(
'products',
where: 'reference = ?',
whereArgs: [code],
limit: 1,
);
if (maps.isNotEmpty) {
return Product.fromMap(maps.first);
}
// Ensuite par IMEI
maps = await db.query(
'products',
where: 'imei = ?',
whereArgs: [code],
limit: 1,
);
if (maps.isNotEmpty) {
return Product.fromMap(maps.first);
}
// Enfin par QR code si disponible
maps = await db.query(
'products',
where: 'qrCode = ?',
whereArgs: [code],
limit: 1,
);
if (maps.isNotEmpty) {
return Product.fromMap(maps.first);
}
return null;
}
// Obtenir les produits avec stock faible (seuil personnalisable)
Future<List<Product>> getLowStockProducts({int threshold = 5}) async {
final db = await database;
final maps = await db.query(
'products',
where: 'stock <= ? AND stock > 0',
whereArgs: [threshold],
orderBy: 'stock ASC',
);
return List.generate(maps.length, (i) => Product.fromMap(maps[i]));
}
// Obtenir les produits les plus vendus (basé sur les commandes)
Future<List<Map<String, dynamic>>> getMostSoldProducts({int limit = 10}) async {
final db = await database;
final result = await db.rawQuery('''
SELECT
p.id,
p.name,
p.price,
p.stock,
p.category,
SUM(dc.quantite) as total_sold,
COUNT(DISTINCT dc.commandeId) as order_count
FROM products p
INNER JOIN details_commandes dc ON p.id = dc.produitId
INNER JOIN commandes c ON dc.commandeId = c.id
WHERE c.statut != 5 -- Exclure les commandes annulées
GROUP BY p.id, p.name, p.price, p.stock, p.category
ORDER BY total_sold DESC
LIMIT ?
''', [limit]);
return result;
}
// Recherche de produits similaires (par nom ou catégorie)
Future<List<Product>> getSimilarProducts(Product product, {int limit = 5}) async {
final db = await database;
// Rechercher par catégorie et nom similaire, exclure le produit actuel
final maps = await db.rawQuery('''
SELECT *
FROM products
WHERE id != ?
AND (
category = ?
OR name LIKE ?
)
ORDER BY
CASE WHEN category = ? THEN 1 ELSE 2 END,
name ASC
LIMIT ?
''', [
product.id,
product.category,
'%${product.name.split(' ').first}%',
product.category,
limit
]);
return List.generate(maps.length, (i) => Product.fromMap(maps[i]));
}
} }

360
lib/Views/Dashboard.dart

@ -30,24 +30,29 @@ final GlobalKey _salesChartKey = GlobalKey();
late AnimationController _animationController; late AnimationController _animationController;
late Animation<double> _fadeAnimation; late Animation<double> _fadeAnimation;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadData(); _loadData();
_animationController = AnimationController( _animationController = AnimationController(
vsync: this, vsync: this,
duration: Duration(milliseconds: 800), duration: Duration(milliseconds: 800),
); );
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate( _fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation( CurvedAnimation(
parent: _animationController, parent: _animationController,
curve: Curves.easeInOut, curve: Curves.easeInOut,
), ),
); );
_animationController.forward(); // Démarrer l'animation après un léger délai
} Future.delayed(Duration(milliseconds: 50), () {
if (mounted) {
_animationController.forward();
}
});
}
@override @override
void dispose() { void dispose() {
@ -354,47 +359,51 @@ Future<void> _showCategoryProductsDialog(String category) async {
} }
Widget _buildSalesChart() { Widget _buildSalesChart() {
key: _salesChartKey;
return Card( return Card(
elevation: 4, key: _salesChartKey,
shape: RoundedRectangleBorder( elevation: 4,
borderRadius: BorderRadius.circular(12), shape: RoundedRectangleBorder(
), borderRadius: BorderRadius.circular(12),
child: Padding( ),
padding: EdgeInsets.all(16), child: Padding(
child: Column( padding: EdgeInsets.all(16),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Row( children: [
children: [ // ... titre
Icon(Icons.trending_up, color: Colors.blue), Container(
SizedBox(width: 8), height: 200,
Text( child: FutureBuilder<List<Commande>>(
'Ventes par mois', future: _allOrdersFuture,
style: TextStyle( builder: (context, snapshot) {
fontSize: 16, if (snapshot.connectionState == ConnectionState.waiting) {
fontWeight: FontWeight.bold, return Center(child: CircularProgressIndicator());
), }
),
], if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) {
), return Center(
SizedBox(height: 16), child: Column(
Container( mainAxisAlignment: MainAxisAlignment.center,
height: 200, children: [
child: FutureBuilder<List<Commande>>( Icon(Icons.trending_up_outlined, size: 64, color: Colors.grey),
future: _allOrdersFuture, SizedBox(height: 16),
builder: (context, snapshot) { Text('Aucune donnée de vente disponible', style: TextStyle(color: Colors.grey)),
if (snapshot.connectionState == ConnectionState.waiting) { ],
return Center(child: CircularProgressIndicator()); ),
} );
}
if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('Aucune donnée disponible')); final salesData = _groupOrdersByMonth(snapshot.data!);
}
// Vérification si salesData est vide
final salesData = _groupOrdersByMonth(snapshot.data!); if (salesData.isEmpty) {
return Center(
return BarChart( child: Text('Aucune donnée de vente disponible', style: TextStyle(color: Colors.grey)),
);
}
return BarChart(
BarChartData( BarChartData(
alignment: BarChartAlignment.spaceAround, alignment: BarChartAlignment.spaceAround,
maxY: salesData.map((e) => e['total']).reduce((a, b) => a > b ? a : b) * 1.2, maxY: salesData.map((e) => e['total']).reduce((a, b) => a > b ? a : b) * 1.2,
@ -498,99 +507,147 @@ Future<void> _showCategoryProductsDialog(String category) async {
} }
Widget _buildStockChart() { Widget _buildStockChart() {
return Card( return Card(
elevation: 4, elevation: 4,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Padding( child: Padding(
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
Icon(Icons.inventory, color: Colors.blue), Icon(Icons.inventory, color: Colors.blue),
SizedBox(width: 8), SizedBox(width: 8),
Text( Text(
'État du stock', 'État du stock',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
),
), ),
], ),
), ],
SizedBox(height: 16), ),
Container( SizedBox(height: 16),
height: 200, Container(
child: FutureBuilder<List<Product>>( height: 200,
future: _database.getProducts(), child: FutureBuilder<List<Product>>(
builder: (context, snapshot) { future: _database.getProducts(),
if (snapshot.connectionState == ConnectionState.waiting) { builder: (context, snapshot) {
return Center(child: CircularProgressIndicator()); if (snapshot.connectionState == ConnectionState.waiting) {
} return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError || !snapshot.hasData) {
return Center(child: Text('Aucune donnée disponible')); if (snapshot.hasError || !snapshot.hasData) {
} return Center(child: Text('Aucune donnée disponible'));
}
final products = snapshot.data!;
final lowStock = products.where((p) => (p.stock ?? 0) < 10).length; final products = snapshot.data!;
final inStock = products.length - lowStock;
// Vérification si la liste est vide
return PieChart( if (products.isEmpty) {
PieChartData( return Center(
sectionsSpace: 0, child: Column(
centerSpaceRadius: 40, mainAxisAlignment: MainAxisAlignment.center,
sections: [ children: [
PieChartSectionData( Icon(Icons.inventory_2_outlined, size: 64, color: Colors.grey),
color: Colors.orange, SizedBox(height: 16),
value: lowStock.toDouble(), Text('Aucun produit en stock', style: TextStyle(color: Colors.grey)),
title: '$lowStock',
radius: 20,
titleStyle: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: Colors.green,
value: inStock.toDouble(),
title: '$inStock',
radius: 20,
titleStyle: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
], ],
pieTouchData: PieTouchData( ),
touchCallback: (FlTouchEvent event, pieTouchResponse) {}, );
}
final lowStock = products.where((p) => (p.stock ?? 0) < 10).length;
final inStock = products.length - lowStock;
// Vérification pour éviter les sections vides
List<PieChartSectionData> sections = [];
if (lowStock > 0) {
sections.add(
PieChartSectionData(
color: Colors.orange,
value: lowStock.toDouble(),
title: '$lowStock',
radius: 20,
titleStyle: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
), ),
startDegreeOffset: 180,
borderData: FlBorderData(show: false),
), ),
); );
}, }
),
), if (inStock > 0) {
SizedBox(height: 8), sections.add(
Row( PieChartSectionData(
mainAxisAlignment: MainAxisAlignment.center, color: Colors.green,
children: [ value: inStock.toDouble(),
_buildLegendItem(Colors.orange, 'Stock faible'), title: '$inStock',
SizedBox(width: 16), radius: 20,
_buildLegendItem(Colors.green, 'En stock'), titleStyle: TextStyle(
], fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
);
}
// Si toutes les sections sont vides, afficher un message
if (sections.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.info_outline, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('Aucune donnée de stock disponible', style: TextStyle(color: Colors.grey)),
],
),
);
}
return PieChart(
PieChartData(
sectionsSpace: 0,
centerSpaceRadius: 40,
sections: sections,
pieTouchData: PieTouchData(
enabled: true, // Activé pour permettre les interactions
touchCallback: (FlTouchEvent event, pieTouchResponse) {
// Gestion sécurisée des interactions
if (pieTouchResponse != null &&
pieTouchResponse.touchedSection != null) {
// Vous pouvez ajouter une logique ici si nécessaire
}
},
),
startDegreeOffset: 180,
borderData: FlBorderData(show: false),
),
);
},
), ),
], ),
), SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildLegendItem(Colors.orange, 'Stock faible'),
SizedBox(width: 16),
_buildLegendItem(Colors.green, 'En stock'),
],
),
],
), ),
); ),
} );
}
Widget _buildLegendItem(Color color, String text) { Widget _buildLegendItem(Color color, String text) {
return Row( return Row(
@ -805,8 +862,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
} }
Widget _buildRecentOrdersCard() { Widget _buildRecentOrdersCard() {
key: _recentOrdersKey;
return Card( return Card(
key: _recentOrdersKey,
elevation: 4, elevation: 4,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -944,8 +1002,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
Widget _buildRecentClientsCard() { Widget _buildRecentClientsCard() {
key: _recentClientsKey;
return Card( return Card(
key: _recentClientsKey,
elevation: 4, elevation: 4,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -1029,8 +1088,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
} }
Widget _buildLowStockCard() { Widget _buildLowStockCard() {
key: _lowStockKey;
return Card( return Card(
key: _lowStockKey,
elevation: 4, elevation: 4,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -1136,12 +1196,6 @@ Future<void> _showCategoryProductsDialog(String category) async {
return Colors.orange; return Colors.orange;
case StatutCommande.confirmee: case StatutCommande.confirmee:
return Colors.blue; return Colors.blue;
case StatutCommande.enPreparation:
return Colors.purple;
case StatutCommande.expediee:
return Colors.teal;
case StatutCommande.livree:
return Colors.green;
case StatutCommande.annulee: case StatutCommande.annulee:
return Colors.red; return Colors.red;
default: default:

2837
lib/Views/HandleProduct.dart

File diff suppressed because it is too large

892
lib/Views/commandManagement.dart

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:numbers_to_letters/numbers_to_letters.dart';
import 'package:pdf/pdf.dart'; import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw; import 'package:pdf/widgets.dart' as pw;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -118,10 +119,6 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
message = 'Commande annulée avec succès'; message = 'Commande annulée avec succès';
backgroundColor = Colors.orange; backgroundColor = Colors.orange;
break; break;
case StatutCommande.livree:
message = 'Commande marquée comme livrée';
backgroundColor = Colors.green;
break;
case StatutCommande.confirmee: case StatutCommande.confirmee:
message = 'Commande confirmée'; message = 'Commande confirmée';
backgroundColor = Colors.blue; backgroundColor = Colors.blue;
@ -230,394 +227,527 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
}, },
); );
} }
Future<pw.Widget> buildIconPhoneText() async {
final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf'));
return pw.Text(String.fromCharCode(0xf095), style: pw.TextStyle(font: font));
}
Future<pw.Widget> buildIconCheckedText() async {
final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf'));
return pw.Text(String.fromCharCode(0xf14a), style: pw.TextStyle(font: font));
}
Future<void> _generateInvoice(Commande commande) async { Future<pw.Widget> buildIconGlobeText() async {
final details = await _database.getDetailsCommande(commande.id!); final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf'));
final client = await _database.getClientById(commande.clientId); return pw.Text(String.fromCharCode(0xf0ac), style: pw.TextStyle(font: font));
final commandeur = commande.commandeurId != null }
? await _database.getUserById(commande.commandeurId!)
: null;
final validateur = commande.validateurId != null
? await _database.getUserById(commande.validateurId!)
: null;
final pointDeVente = commandeur?.pointDeVenteId != null
? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!)
: null;
final pdf = pw.Document();
final imageBytes = await loadImage();
final image = pw.MemoryImage(imageBytes);
final headerStyle = pw.TextStyle(
fontSize: 18,
fontWeight: pw.FontWeight.bold,
color: PdfColors.blue900,
);
final titleStyle = pw.TextStyle(
fontSize: 14,
fontWeight: pw.FontWeight.bold,
);
final subtitleStyle = pw.TextStyle(
fontSize: 12,
color: PdfColors.grey600,
);
pdf.addPage( Future<void> _generateInvoice(Commande commande) async {
pw.Page( final details = await _database.getDetailsCommande(commande.id!);
margin: const pw.EdgeInsets.all(20), final client = await _database.getClientById(commande.clientId);
build: (pw.Context context) { final pointDeVente = await _database.getPointDeVenteById(1);
return pw.Column( final iconPhone = await buildIconPhoneText();
crossAxisAlignment: pw.CrossAxisAlignment.start, final iconChecked = await buildIconCheckedText();
children: [ final iconGlobe = await buildIconGlobeText();
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, // IMPORTANT: Récupérer tous les détails des produits AVANT de créer le PDF
crossAxisAlignment: pw.CrossAxisAlignment.start, final List<Map<String, dynamic>> detailsAvecProduits = [];
children: [ for (final detail in details) {
pw.Column( final produit = await _database.getProductById(detail.produitId);
crossAxisAlignment: pw.CrossAxisAlignment.start, detailsAvecProduits.add({
children: [ 'detail': detail,
pw.Container( 'produit': produit,
width: 100, });
height: 80, }
decoration: pw.BoxDecoration(
border: final pdf = pw.Document();
pw.Border.all(color: PdfColors.blue900, width: 2), final imageBytes = await loadImage();
borderRadius: pw.BorderRadius.circular(8), final image = pw.MemoryImage(imageBytes);
final italicFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf'));
// Styles de texte
final smallTextStyle = pw.TextStyle(fontSize: 9);
final smallBoldTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold);
final normalTextStyle = pw.TextStyle(fontSize: 10);
final boldTextStyle = pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold);
final boldTexClienttStyle = pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold);
final frameTextStyle = pw.TextStyle(fontSize: 10);
final italicTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont);
final italicTextStyleLogo = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: italicFont);
pdf.addPage(
pw.Page(
margin: const pw.EdgeInsets.all(20),
build: (pw.Context context) {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
// Première ligne: Logo à gauche, informations à droite
pw.Row(
crossAxisAlignment: pw.CrossAxisAlignment.start,
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
// Colonne de gauche avec logo et points de vente
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
// Logo
pw.Container(
width: 150,
height: 150,
child: pw.Image(image),
),
pw.Text(' NOTRE COMPETENCE, A VOTRE SERVICE', style: italicTextStyleLogo),
pw.SizedBox(height: 12),
// Liste des points de vente avec checkbox
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('REMAX by GUYCOM Andravoangy', style: smallTextStyle)]),
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 405', style: smallTextStyle)]),
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 416', style: smallTextStyle)]),
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 119', style: smallTextStyle)]),
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('TRIPOLITSA Analakely BOX 7', style: smallTextStyle)]),
],
),
// Informations de contact
pw.SizedBox(height: 10),
pw.Row(children: [iconPhone, pw.SizedBox(width: 5), pw.Text('033 37 808 18', style: smallTextStyle)]),
pw.Row(children: [iconGlobe, pw.SizedBox(width: 5), pw.Text('www.guycom.mg', style: smallTextStyle)]),
pw.Text('Facebook: GuyCom', style: smallTextStyle),
],
),
// Colonne de droite avec cadres de texte
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', style: boldTexClienttStyle),
pw.SizedBox(height: 10),
pw.Container(width: 200, height: 1, color: PdfColors.black),
// Deux petits cadres côte à côte
pw.SizedBox(height: 10),
pw.Row(
children: [
pw.Container(
width: 100,
height: 40,
padding: const pw.EdgeInsets.all(5),
child: pw.Column(
children: [
pw.Text('Boutique:', style: frameTextStyle),
pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldTexClienttStyle),
]
)
),
pw.SizedBox(width: 10),
pw.Container(
width: 100,
height: 40,
padding: const pw.EdgeInsets.all(5),
child: pw.Column(
children: [
pw.Text('Bon de livraison N°:', style: frameTextStyle),
pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTexClienttStyle),
]
)
), ),
child: pw.Center(child: pw.Image(image)), ],
),
// Grand cadre en dessous
pw.SizedBox(height: 20),
pw.Container(
width: 300,
height: 100,
decoration: pw.BoxDecoration(
border: pw.Border.all(color: PdfColors.black, width: 1),
), ),
pw.SizedBox(height: 10), padding: const pw.EdgeInsets.all(10),
pw.Text('guycom', style: headerStyle), child: pw.Column(
if (pointDeVente != null) crossAxisAlignment: pw.CrossAxisAlignment.center,
pw.Text('Point de vente: ${pointDeVente['designation']}', style: subtitleStyle), children: [
pw.Text('Tél: +213 123 456 789', style: subtitleStyle), pw.Text('ID Client: ', style: frameTextStyle),
], pw.SizedBox(height: 5),
), pw.Text('${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', style: boldTexClienttStyle),
pw.Column( pw.SizedBox(height: 5),
crossAxisAlignment: pw.CrossAxisAlignment.end, pw.Container(width: 200, height: 1, color: PdfColors.black),
pw.Text(client?.nom ?? 'Non spécifié', style: boldTexClienttStyle),
pw.SizedBox(height: 10),
pw.Text(client?.telephone ?? 'Non spécifié', style: frameTextStyle),
],
),
),
],
),
],
),
pw.SizedBox(height: 20),
// Tableau des produits avec plus de colonnes
pw.Table(
border: pw.TableBorder.all(width: 0.5),
columnWidths: {
0: const pw.FlexColumnWidth(3), // Désignation
1: const pw.FlexColumnWidth(1), // Qté
2: const pw.FlexColumnWidth(2), // Prix unitaire
3: const pw.FlexColumnWidth(2), // Montant
},
children: [
// En-tête du tableau
pw.TableRow(
decoration: const pw.BoxDecoration(color: PdfColors.grey200),
children: [
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Désignations', style: boldTextStyle)),
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Qté', style: boldTextStyle, textAlign: pw.TextAlign.center)),
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Prix unitaire', style: boldTextStyle, textAlign: pw.TextAlign.right)),
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right)),
],
),
// Lignes des produits avec détails complets
...detailsAvecProduits.map((item) {
final detail = item['detail'] as DetailCommande;
final produit = item['produit'];
return pw.TableRow(
children: [ children: [
pw.Container( pw.Padding(
padding: const pw.EdgeInsets.all(12), padding: const pw.EdgeInsets.all(4),
decoration: pw.BoxDecoration(
color: PdfColors.blue50,
borderRadius: pw.BorderRadius.circular(8),
),
child: pw.Column( child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start, crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [ children: [
pw.Text( // Nom du produit
'FACTURE', pw.Text(detail.produitNom ?? 'Produit inconnu',
style: pw.TextStyle( style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold)),
fontSize: 20, pw.SizedBox(height: 2),
fontWeight: pw.FontWeight.bold,
color: PdfColors.blue900,
), if (produit?.category != null && produit!.category.isNotEmpty && produit?.marque != null && produit!.marque.isNotEmpty)
), pw.Text('${produit.category} ${produit.marque}', style: smallTextStyle),
pw.SizedBox(height: 8),
pw.Text('N°: ${commande.id}', style: titleStyle), // IMEI
pw.Text( if (produit?.imei != null && produit!.imei!.isNotEmpty)
'Date: ${DateFormat('dd/MM/yyyy').format(commande.dateCommande)}'), pw.Text('${produit.imei}', style: smallTextStyle),
// Référence
if (produit?.reference != null && produit!.reference!.isNotEmpty && produit?.ram != null && produit!.ram!.isNotEmpty && produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty)
pw.Text('${produit.ram} | ${produit.memoireInterne} | ${produit.reference}', style: smallTextStyle),
// // IMEI
// if (produit?.imei != null && produit!.imei!.isNotEmpty)
// pw.Text('IMEI: ${produit.imei}', style: smallTextStyle),
// // RAM
// if (produit?.ram != null && produit!.ram!.isNotEmpty)
// pw.Text('RAM: ${produit.ram}', style: smallTextStyle),
// // Stockage
// if (produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty)
// pw.Text('Stockage: ${produit.memoireInterne}', style: smallTextStyle),
// // Catégorie
], ],
), ),
), ),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('${detail.quantite}', style: normalTextStyle, textAlign: pw.TextAlign.center),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: normalTextStyle, textAlign: pw.TextAlign.right),
),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('${detail.sousTotal.toStringAsFixed(0)}', style: normalTextStyle, textAlign: pw.TextAlign.right),
),
], ],
), );
], }).toList(),
), ],
),
pw.SizedBox(height: 30),
pw.SizedBox(height: 10),
// Informations client
pw.Container( // Total
width: double.infinity, pw.Row(
padding: const pw.EdgeInsets.all(12), mainAxisAlignment: pw.MainAxisAlignment.end,
decoration: pw.BoxDecoration( children: [
color: PdfColors.grey100, pw.Text('TOTAL', style: boldTextStyle),
borderRadius: pw.BorderRadius.circular(8), pw.SizedBox(width: 20),
), pw.Text('${commande.montantTotal.toStringAsFixed(0)}', style: boldTextStyle),
child: pw.Column( ],
),
pw.SizedBox(height: 10),
// Montant en lettres
pw.Text('Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', style: italicTextStyle),
pw.SizedBox(height: 30),
// Signatures
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start, crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [ children: [
pw.Text('FACTURÉ À:', style: titleStyle), pw.Text('Signature du vendeur', style: smallTextStyle),
pw.SizedBox(height: 5), pw.SizedBox(height: 20),
pw.Text(client?.nomComplet ?? 'Client inconnu', pw.Container(width: 150, height: 1, color: PdfColors.black),
style: pw.TextStyle(fontSize: 12)),
if (client?.telephone != null)
pw.Text('Tél: ${client!.telephone}',
style: pw.TextStyle(
fontSize: 10, color: PdfColors.grey600)),
], ],
), ),
), pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
pw.SizedBox(height: 20), children: [
pw.Text('Signature du client', style: smallTextStyle),
// Informations personnel pw.SizedBox(height: 20),
if (commandeur != null || validateur != null) pw.Container(width: 150, height: 1, color: PdfColors.black),
pw.Container( ],
width: double.infinity,
padding: const pw.EdgeInsets.all(12),
decoration: pw.BoxDecoration(
color: PdfColors.grey100,
borderRadius: pw.BorderRadius.circular(8),
),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('PERSONNEL:', style: titleStyle),
pw.SizedBox(height: 5),
if (commandeur != null)
pw.Text('Commandeur: ${commandeur.name} ',
style: pw.TextStyle(fontSize: 12)),
if (validateur != null)
pw.Text('Validateur: ${validateur.name}',
style: pw.TextStyle(fontSize: 12)),
],
),
), ),
],
pw.SizedBox(height: 20), ),
],
// Tableau des produits );
pw.Text('DÉTAILS DE LA COMMANDE', style: titleStyle), },
pw.SizedBox(height: 10), ),
);
pw.Table(
border:
pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
children: [
pw.TableRow(
decoration:
const pw.BoxDecoration(color: PdfColors.blue900),
children: [
_buildTableCell('Produit',
titleStyle.copyWith(color: PdfColors.white)),
_buildTableCell(
'Qté', titleStyle.copyWith(color: PdfColors.white)),
_buildTableCell('Prix unit.',
titleStyle.copyWith(color: PdfColors.white)),
_buildTableCell(
'Total', titleStyle.copyWith(color: PdfColors.white)),
],
),
...details.asMap().entries.map((entry) {
final index = entry.key;
final detail = entry.value;
final isEven = index % 2 == 0;
return pw.TableRow(
decoration: pw.BoxDecoration(
color: isEven ? PdfColors.white : PdfColors.grey50,
),
children: [
_buildTableCell(detail.produitNom ?? 'Produit inconnu'),
_buildTableCell(detail.quantite.toString()),
_buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA'),
_buildTableCell('${detail.sousTotal.toStringAsFixed(2)} MGA'),
],
);
}),
],
),
pw.SizedBox(height: 20),
// Total final output = await getTemporaryDirectory();
pw.Container( final file = File('${output.path}/facture_${commande.id}.pdf');
alignment: pw.Alignment.centerRight, await file.writeAsBytes(await pdf.save());
child: pw.Container( await OpenFile.open(file.path);
padding: const pw.EdgeInsets.all(12), }
decoration: pw.BoxDecoration(
color: PdfColors.blue900,
borderRadius: pw.BorderRadius.circular(8),
),
child: pw.Text(
'TOTAL: ${commande.montantTotal.toStringAsFixed(2)} MGA',
style: pw.TextStyle(
fontSize: 16,
fontWeight: pw.FontWeight.bold,
color: PdfColors.white,
),
),
),
),
pw.Spacer(),
// Pied de page pw.Widget _buildCheckboxPointDeVente(String text, bool checked) {
pw.Container( return pw.Row(
width: double.infinity, children: [
padding: const pw.EdgeInsets.all(12), pw.Container(
decoration: pw.BoxDecoration( width: 10,
border: pw.Border( height: 10,
top: pw.BorderSide(color: PdfColors.grey400, width: 1), decoration: pw.BoxDecoration(
), border: pw.Border.all(width: 1),
), color: checked ? PdfColors.black : PdfColors.white,
child: pw.Column( ),
children: [ ),
pw.Text( pw.SizedBox(width: 5),
'Merci pour votre confiance!', pw.Text(text, style: pw.TextStyle(fontSize: 9)),
style: pw.TextStyle( ],
fontSize: 14,
fontStyle: pw.FontStyle.italic,
color: PdfColors.blue900,
),
),
pw.SizedBox(height: 5),
pw.Text(
'Cette facture est générée automatiquement par le système Youmaz Gestion',
style:
pw.TextStyle(fontSize: 8, color: PdfColors.grey600),
),
],
),
),
],
);
},
),
); );
final output = await getTemporaryDirectory();
final file = File('${output.path}/facture_${commande.id}.pdf');
await file.writeAsBytes(await pdf.save());
await OpenFile.open(file.path);
} }
String _numberToWords(int number) {
// Implémentez la conversion du nombre en lettres ici
// Exemple simplifié:
NumbersToLetters.toLetters('fr', number);
return NumbersToLetters.toLetters('fr', number);
}
Future<void> _generateReceipt(Commande commande, PaymentMethod payment) async { Future<void> _generateReceipt(Commande commande, PaymentMethod payment) async {
final details = await _database.getDetailsCommande(commande.id!); final details = await _database.getDetailsCommande(commande.id!);
final client = await _database.getClientById(commande.clientId); final client = await _database.getClientById(commande.clientId);
final commandeur = commande.commandeurId != null final commandeur = commande.commandeurId != null
? await _database.getUserById(commande.commandeurId!) ? await _database.getUserById(commande.commandeurId!)
: null; : null;
final validateur = commande.validateurId != null final validateur = commande.validateurId != null
? await _database.getUserById(commande.validateurId!) ? await _database.getUserById(commande.validateurId!)
: null; : null;
final pointDeVente = commandeur?.pointDeVenteId != null final pointDeVente = commandeur?.pointDeVenteId != null
? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!) ? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!)
: null; : null;
final pdf = pw.Document(); // Récupérer les détails complets des produits
final imageBytes = await loadImage(); final List<Map<String, dynamic>> detailsAvecProduits = [];
final image = pw.MemoryImage(imageBytes); for (final detail in details) {
final produit = await _database.getProductById(detail.produitId);
pdf.addPage( detailsAvecProduits.add({
pw.Page( 'detail': detail,
pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity), 'produit': produit,
margin: const pw.EdgeInsets.all(4), });
build: (pw.Context context) { }
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.center, final pdf = pw.Document();
children: [ final imageBytes = await loadImage();
// En-tête final image = pw.MemoryImage(imageBytes);
pw.Center(
child: pw.Container( pdf.addPage(
width: 50, pw.Page(
height: 50, pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity),
child: pw.Image(image), margin: const pw.EdgeInsets.all(4),
), build: (pw.Context context) {
), return pw.Column(
pw.SizedBox(height: 4), crossAxisAlignment: pw.CrossAxisAlignment.center,
pw.Text('TICKET DE CAISSE', children: [
style: pw.TextStyle( // En-tête avec logo
fontSize: 10, pw.Center(
fontWeight: pw.FontWeight.bold, child: pw.Container(
), width: 40,
height: 40,
child: pw.Image(image),
), ),
pw.Text('N°: ${commande.id}', ),
style: const pw.TextStyle(fontSize: 8)), pw.SizedBox(height: 4),
pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}',
// Informations de l'entreprise
pw.Text('GUYCOM MADAGASCAR',
style: pw.TextStyle(
fontSize: 10,
fontWeight: pw.FontWeight.bold,
)),
pw.Text('Tél: 033 37 808 18', style: const pw.TextStyle(fontSize: 7)),
pw.Text('www.guycom.mg', style: const pw.TextStyle(fontSize: 7)),
pw.SizedBox(height: 6),
// Titre et numéro de ticket
pw.Text('TICKET DE CAISSE',
style: pw.TextStyle(
fontSize: 10,
fontWeight: pw.FontWeight.bold,
decoration: pw.TextDecoration.underline,
)),
pw.Text('N°: ${pointDeVente?['abreviation'] ?? 'PV'}-${commande.id}',
style: const pw.TextStyle(fontSize: 8)),
pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}',
style: const pw.TextStyle(fontSize: 8)),
if (pointDeVente != null)
pw.Text('Point de vente: ${pointDeVente['designation']}',
style: const pw.TextStyle(fontSize: 8)), style: const pw.TextStyle(fontSize: 8)),
if (pointDeVente != null) pw.Divider(thickness: 0.5),
pw.Text('Point de vente: ${pointDeVente['designation']}',
style: const pw.TextStyle(fontSize: 8)), // Informations client
pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}',
pw.Divider(thickness: 0.5), style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)),
if (client?.telephone != null)
// Client pw.Text('Tél: ${client!.telephone}', style: const pw.TextStyle(fontSize: 7)),
pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}',
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)), // Personnel impliqué
if (commandeur != null || validateur != null)
// Personnel pw.Column(
if (commandeur != null) crossAxisAlignment: pw.CrossAxisAlignment.start,
pw.Text('Commandeur: ${commandeur.name} ',
style: const pw.TextStyle(fontSize: 7)),
if (validateur != null)
pw.Text('Validateur: ${validateur.name}',
style: const pw.TextStyle(fontSize: 7)),
pw.Divider(thickness: 0.5),
// Détails
pw.Table(
columnWidths: {
0: const pw.FlexColumnWidth(3),
1: const pw.FlexColumnWidth(1),
2: const pw.FlexColumnWidth(2),
},
children: [
pw.TableRow(
children: [
pw.Text('Produit', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
pw.Text('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
pw.Text('Total', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
],
),
...details.map((detail) => pw.TableRow(
children: [
pw.Text(detail.produitNom ?? 'Produit', style: const pw.TextStyle(fontSize: 7)),
pw.Text(detail.quantite.toString(), style: const pw.TextStyle(fontSize: 7)),
pw.Text('${detail.sousTotal.toStringAsFixed(2)} MGA', style: const pw.TextStyle(fontSize: 7)),
],
)),
],
),
pw.Divider(thickness: 0.5),
// Total
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [ children: [
pw.Text('TOTAL:', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), pw.Divider(thickness: 0.5),
pw.Text('${commande.montantTotal.toStringAsFixed(2)} MGA', if (commandeur != null)
style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), pw.Text('Vendeur: ${commandeur.name}', style: const pw.TextStyle(fontSize: 7)),
if (validateur != null)
pw.Text('Validateur: ${validateur.name}', style: const pw.TextStyle(fontSize: 7)),
], ],
), ),
// Paiement pw.Divider(thickness: 0.5),
pw.SizedBox(height: 8),
pw.Text('MODE DE PAIEMENT:', style: const pw.TextStyle(fontSize: 8)), // Détails des produits
pw.Text( pw.Table(
payment.type == PaymentType.cash columnWidths: {
? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(2)} MGA)' 0: const pw.FlexColumnWidth(3.5),
: 'CARTE BANCAIRE', 1: const pw.FlexColumnWidth(1),
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold), 2: const pw.FlexColumnWidth(1.5),
), },
children: [
if (payment.type == PaymentType.cash && payment.amountGiven > commande.montantTotal) // En-tête du tableau
pw.Text('Monnaie rendue: ${(payment.amountGiven - commande.montantTotal).toStringAsFixed(2)} MGA', pw.TableRow(
style: const pw.TextStyle(fontSize: 8)), children: [
pw.Text('Désignation', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
pw.SizedBox(height: 12), pw.Text('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
pw.Text('Merci pour votre achat !', pw.Text('P.U', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)), ],
pw.Text('www.guycom.mg', decoration: const pw.BoxDecoration(
style: const pw.TextStyle(fontSize: 7)), border: pw.Border(bottom: pw.BorderSide(width: 0.5)),
],
); ),),
},
), // Lignes des produits
); ...detailsAvecProduits.map( (item) {
final detail = item['detail'] as DetailCommande;
final produit = item['produit'];
return pw.TableRow(
decoration: const pw.BoxDecoration(
border: pw.Border(bottom: pw.BorderSide(width: 0.2))),
children: [
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(detail.produitNom ?? 'Produit',
style: const pw.TextStyle(fontSize: 7)),
if (produit?.reference != null)
pw.Text('Ref: ${produit!.reference}',
style: const pw.TextStyle(fontSize: 6)),
if (produit?.imei != null)
pw.Text('IMEI: ${produit!.imei}',
style: const pw.TextStyle(fontSize: 6)),
],
),
pw.Text(detail.quantite.toString(),
style: const pw.TextStyle(fontSize: 7)),
pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}',
style: const pw.TextStyle(fontSize: 7)),
],
);
}),
],
),
pw.Divider(thickness: 0.5),
// Total et paiement
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text('TOTAL:',
style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)),
pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA',
style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)),
],
),
pw.SizedBox(height: 6),
// Détails du paiement
pw.Text('MODE DE PAIEMENT:',
style: const pw.TextStyle(fontSize: 8)),
pw.Text(
payment.type == PaymentType.cash
? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(0)} MGA)'
: 'CARTE BANCAIRE',
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold),
),
if (payment.type == PaymentType.cash && payment.amountGiven > commande.montantTotal)
pw.Text('Monnaie rendue: ${(payment.amountGiven - commande.montantTotal).toStringAsFixed(0)} MGA',
style: const pw.TextStyle(fontSize: 8)),
pw.SizedBox(height: 12),
// Mentions légales et remerciements
pw.Text('Article non échangeable - Garantie selon conditions',
style: const pw.TextStyle(fontSize: 6)),
pw.Text('Ticket à conserver comme justificatif',
style: const pw.TextStyle(fontSize: 6)),
pw.SizedBox(height: 8),
pw.Text('Merci pour votre confiance !',
style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)),
],
);
},
),
);
final output = await getTemporaryDirectory(); final output = await getTemporaryDirectory();
final file = File('${output.path}/ticket_${commande.id}.pdf'); final file = File('${output.path}/ticket_${commande.id}.pdf');
await file.writeAsBytes(await pdf.save()); await file.writeAsBytes(await pdf.save());
await OpenFile.open(file.path); await OpenFile.open(file.path);
} }
pw.Widget _buildTableCell(String text, [pw.TextStyle? style]) { pw.Widget _buildTableCell(String text, [pw.TextStyle? style]) {
return pw.Padding( return pw.Padding(
@ -632,12 +762,12 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
return Colors.orange.shade100; return Colors.orange.shade100;
case StatutCommande.confirmee: case StatutCommande.confirmee:
return Colors.blue.shade100; return Colors.blue.shade100;
case StatutCommande.enPreparation: // case StatutCommande.enPreparation:
return Colors.amber.shade100; // return Colors.amber.shade100;
case StatutCommande.expediee: // case StatutCommande.expediee:
return Colors.purple.shade100; // return Colors.purple.shade100;
case StatutCommande.livree: // case StatutCommande.livree:
return Colors.green.shade100; // return Colors.green.shade100;
case StatutCommande.annulee: case StatutCommande.annulee:
return Colors.red.shade100; return Colors.red.shade100;
} }
@ -649,12 +779,12 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
return Icons.schedule; return Icons.schedule;
case StatutCommande.confirmee: case StatutCommande.confirmee:
return Icons.check_circle_outline; return Icons.check_circle_outline;
case StatutCommande.enPreparation: // case StatutCommande.enPreparation:
return Icons.settings; // return Icons.settings;
case StatutCommande.expediee: // case StatutCommande.expediee:
return Icons.local_shipping; // return Icons.local_shipping;
case StatutCommande.livree: // case StatutCommande.livree:
return Icons.check_circle; // return Icons.check_circle;
case StatutCommande.annulee: case StatutCommande.annulee:
return Icons.cancel; return Icons.cancel;
} }
@ -1209,12 +1339,12 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
return 'En attente'; return 'En attente';
case StatutCommande.confirmee: case StatutCommande.confirmee:
return 'Confirmée'; return 'Confirmée';
case StatutCommande.enPreparation: // case StatutCommande.enPreparation:
return 'En préparation'; // return 'En préparation';
case StatutCommande.expediee: // case StatutCommande.expediee:
return 'Expédiée'; // return 'Expédiée';
case StatutCommande.livree: // case StatutCommande.livree:
return 'Livrée'; // return 'Livrée';
case StatutCommande.annulee: case StatutCommande.annulee:
return 'Annulée'; return 'Annulée';
} }
@ -1424,9 +1554,9 @@ class _CommandeActions extends StatelessWidget {
break; break;
case StatutCommande.confirmee: case StatutCommande.confirmee:
case StatutCommande.enPreparation: // case StatutCommande.enPreparation:
case StatutCommande.expediee: // case StatutCommande.expediee:
case StatutCommande.livree: // case StatutCommande.livree:
buttons.add( buttons.add(
Container( Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),

918
lib/Views/historique.dart

File diff suppressed because it is too large

183
lib/Views/loginPage.dart

@ -8,7 +8,6 @@ import 'package:youmazgestion/accueil.dart';
import '../Models/users.dart'; import '../Models/users.dart';
import '../controller/userController.dart'; import '../controller/userController.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {
const LoginPage({super.key}); const LoginPage({super.key});
@ -126,8 +125,8 @@ class _LoginPageState extends State<LoginPage> {
context, context,
MaterialPageRoute(builder: (context) => const MainLayout()), MaterialPageRoute(builder: (context) => const MainLayout()),
); );
}else{ } else {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute(builder: (context) => DashboardPage()), MaterialPageRoute(builder: (context) => DashboardPage()),
); );
@ -216,88 +215,124 @@ class _LoginPageState extends State<LoginPage> {
fontSize: 16, fontSize: 16,
), ),
), ),
Container( ],
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: const Icon(
Icons.lock_outline,
size: 100.0,
color: Color.fromARGB(255, 4, 54, 95),
),
),
TextField(
controller: _usernameController,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Username',
prefixIcon: const Icon(Icons.person, color: Colors.blueAccent),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
), ),
), ),
), const SizedBox(height: 24),
const SizedBox(height: 16.0), TextField(
TextField( controller: _usernameController,
controller: _passwordController, enabled: !_isLoading,
enabled: !_isLoading, decoration: InputDecoration(
decoration: InputDecoration( labelText: 'Nom d\'utilisateur',
labelText: 'Password', labelStyle: TextStyle(
prefixIcon: const Icon(Icons.lock, color: Colors.redAccent), color: primaryColor.withOpacity(0.7),
border: OutlineInputBorder( ),
borderRadius: BorderRadius.circular(30.0), prefixIcon: Icon(Icons.person, color: accentColor),
filled: true,
fillColor: accentColor.withOpacity(0.045),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
borderSide: BorderSide(color: accentColor, width: 2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
borderSide: BorderSide(color: accentColor, width: 2),
),
), ),
), ),
obscureText: true, const SizedBox(height: 18.0),
onSubmitted: (_) => _login(), TextField(
), controller: _passwordController,
const SizedBox(height: 16.0), enabled: !_isLoading,
Visibility( obscureText: true,
visible: _isErrorVisible, decoration: InputDecoration(
child: Text( labelText: 'Mot de passe',
_errorMessage, labelStyle: TextStyle(
style: const TextStyle( color: primaryColor.withOpacity(0.7),
color: Colors.red, ),
fontSize: 14, prefixIcon: Icon(Icons.lock, color: accentColor),
filled: true,
fillColor: accentColor.withOpacity(0.045),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
borderSide: BorderSide(color: accentColor, width: 2),
),
), ),
textAlign: TextAlign.center, onSubmitted: (_) => _login(),
), ),
), if (_isErrorVisible) ...[
const SizedBox(height: 16.0), const SizedBox(height: 12.0),
ElevatedButton( Text(
onPressed: _isLoading ? null : _login, _errorMessage,
style: ElevatedButton.styleFrom( style: const TextStyle(
backgroundColor: const Color(0xFF0015B7), color: Colors.redAccent,
elevation: 5.0, fontSize: 15,
shape: RoundedRectangleBorder( fontWeight: FontWeight.w600,
borderRadius: BorderRadius.circular(30.0), ),
textAlign: TextAlign.center,
), ),
minimumSize: const Size(double.infinity, 48), ],
), const SizedBox(height: 26.0),
child: _isLoading ElevatedButton(
? const SizedBox( onPressed: _isLoading ? null : _login,
height: 20, style: ElevatedButton.styleFrom(
width: 20, backgroundColor: accentColor,
child: CircularProgressIndicator( disabledBackgroundColor: accentColor.withOpacity(0.3),
color: Colors.white, foregroundColor: Colors.white,
strokeWidth: 2, elevation: 7.0,
), shape: RoundedRectangleBorder(
) borderRadius: BorderRadius.circular(30.0),
: const Text( ),
'Se connecter', minimumSize: const Size(double.infinity, 52),
style: TextStyle( ),
color: Colors.white, child: _isLoading
fontSize: 16, ? const SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
),
)
: const Text(
'Se connecter',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
letterSpacing: .4,
),
), ),
), ),
), // Option debug, à enlever en prod
] if (_isErrorVisible) ...[
) TextButton(
) onPressed: () async {
], try {
final count =
await AppDatabase.instance.getUserCount();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$count utilisateurs trouvés')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Erreur: $e')),
);
}
},
child: const Text('Debug: Vérifier BDD'),
),
],
],
),
), ),
), ),
), ),
), ),
)
); );
} }
} }

1151
lib/Views/mobilepage.dart

File diff suppressed because it is too large

669
lib/Views/newCommand.dart

@ -26,9 +26,18 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
final TextEditingController _telephoneController = TextEditingController(); final TextEditingController _telephoneController = TextEditingController();
final TextEditingController _adresseController = TextEditingController(); final TextEditingController _adresseController = TextEditingController();
// Contrôleurs pour les filtres - NOUVEAU
final TextEditingController _searchNameController = TextEditingController();
final TextEditingController _searchImeiController = TextEditingController();
final TextEditingController _searchReferenceController = TextEditingController();
// Panier // Panier
final List<Product> _products = []; final List<Product> _products = [];
final List<Product> _filteredProducts = []; // NOUVEAU - Liste filtrée
final Map<int, int> _quantites = {}; final Map<int, int> _quantites = {};
// Variables de filtre - NOUVEAU
bool _showOnlyInStock = false;
// Utilisateurs commerciaux // Utilisateurs commerciaux
List<Users> _commercialUsers = []; List<Users> _commercialUsers = [];
@ -39,12 +48,20 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
super.initState(); super.initState();
_loadProducts(); _loadProducts();
_loadCommercialUsers(); _loadCommercialUsers();
// Listeners pour les filtres - NOUVEAU
_searchNameController.addListener(_filterProducts);
_searchImeiController.addListener(_filterProducts);
_searchReferenceController.addListener(_filterProducts);
} }
Future<void> _loadProducts() async { Future<void> _loadProducts() async {
final products = await _appDatabase.getProducts(); final products = await _appDatabase.getProducts();
setState(() { setState(() {
_products.clear();
_products.addAll(products); _products.addAll(products);
_filteredProducts.clear();
_filteredProducts.addAll(products); // Initialiser la liste filtrée
}); });
} }
@ -58,78 +75,204 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
}); });
} }
@override // NOUVELLE MÉTHODE - Filtrer les produits
Widget build(BuildContext context) { void _filterProducts() {
return Scaffold( final nameQuery = _searchNameController.text.toLowerCase();
floatingActionButton: _buildFloatingCartButton(), final imeiQuery = _searchImeiController.text.toLowerCase();
appBar: CustomAppBar(title: 'Nouvelle Commande'), final referenceQuery = _searchReferenceController.text.toLowerCase();
drawer: CustomDrawer(),
body: Column( setState(() {
children: [ _filteredProducts.clear();
// Header
Container( for (var product in _products) {
padding: const EdgeInsets.all(16.0), bool matchesName = nameQuery.isEmpty ||
decoration: BoxDecoration( product.name.toLowerCase().contains(nameQuery);
gradient: LinearGradient(
colors: [Colors.blue.shade800, Colors.blue.shade600], bool matchesImei = imeiQuery.isEmpty ||
begin: Alignment.topCenter, (product.imei?.toLowerCase().contains(imeiQuery) ?? false);
end: Alignment.bottomCenter,
), bool matchesReference = referenceQuery.isEmpty ||
boxShadow: [ (product.reference?.toLowerCase().contains(referenceQuery) ?? false);
BoxShadow(
color: Colors.black.withOpacity(0.1), bool matchesStock = !_showOnlyInStock ||
blurRadius: 6, (product.stock != null && product.stock! > 0);
offset: const Offset(0, 2),
if (matchesName && matchesImei && matchesReference && matchesStock) {
_filteredProducts.add(product);
}
}
});
}
// NOUVELLE MÉTHODE - Toggle filtre stock
void _toggleStockFilter() {
setState(() {
_showOnlyInStock = !_showOnlyInStock;
});
_filterProducts();
}
// NOUVELLE MÉTHODE - Réinitialiser les filtres
void _clearFilters() {
setState(() {
_searchNameController.clear();
_searchImeiController.clear();
_searchReferenceController.clear();
_showOnlyInStock = false;
});
_filterProducts();
}
// NOUVEAU WIDGET - Section des filtres
Widget _buildFilterSection() {
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.filter_list, color: Colors.blue.shade700),
const SizedBox(width: 8),
const Text(
'Filtres de recherche',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
),
),
const Spacer(),
TextButton.icon(
onPressed: _clearFilters,
icon: const Icon(Icons.clear, size: 18),
label: const Text('Réinitialiser'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey.shade600,
),
), ),
], ],
), ),
child: Column( const SizedBox(height: 16),
// Champ de recherche par nom
TextField(
controller: _searchNameController,
decoration: InputDecoration(
labelText: 'Rechercher par nom',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
),
const SizedBox(height: 12),
// Champs IMEI et Référence sur la même ligne
Row(
children: [ children: [
Row( Expanded(
children: [ child: TextField(
Container( controller: _searchImeiController,
width: 50, decoration: InputDecoration(
height: 50, labelText: 'IMEI',
decoration: BoxDecoration( prefixIcon: const Icon(Icons.phone_android),
color: Colors.white, border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Icon( filled: true,
Icons.shopping_cart, fillColor: Colors.grey.shade50,
color: Colors.blue,
size: 30,
),
), ),
const SizedBox(width: 12), ),
const Expanded( ),
child: Column( const SizedBox(width: 12),
crossAxisAlignment: CrossAxisAlignment.start, Expanded(
children: [ child: TextField(
Text( controller: _searchReferenceController,
'Nouvelle Commande', decoration: InputDecoration(
style: TextStyle( labelText: 'Référence',
fontSize: 20, prefixIcon: const Icon(Icons.qr_code),
fontWeight: FontWeight.bold, border: OutlineInputBorder(
color: Colors.white, borderRadius: BorderRadius.circular(8),
),
),
Text(
'Créez une nouvelle commande pour un client',
style: TextStyle(
fontSize: 14,
color: Colors.white70,
),
),
],
), ),
filled: true,
fillColor: Colors.grey.shade50,
), ),
], ),
), ),
], ],
), ),
), const SizedBox(height: 16),
// Bouton filtre stock et résultats
Row(
children: [
ElevatedButton.icon(
onPressed: _toggleStockFilter,
icon: Icon(
_showOnlyInStock ? Icons.inventory : Icons.inventory_2,
size: 20,
),
label: Text(_showOnlyInStock
? 'Afficher tous'
: 'Stock disponible'),
style: ElevatedButton.styleFrom(
backgroundColor: _showOnlyInStock
? Colors.green.shade600
: Colors.blue.shade600,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12
),
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8
),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${_filteredProducts.length} produit(s)',
style: TextStyle(
color: Colors.blue.shade700,
fontWeight: FontWeight.w600,
),
),
),
],
),
],
),
),
);
}
// Contenu principal @override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: _buildFloatingCartButton(),
appBar: CustomAppBar(title: 'Faire un commande'),
drawer: CustomDrawer(),
body: Column(
children: [
// Header
// Contenu principal MODIFIÉ - Inclut les filtres
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
@ -146,6 +289,11 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
child: const Text('Ajouter les informations client'), child: const Text('Ajouter les informations client'),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// NOUVEAU - Section des filtres
_buildFilterSection(),
// Liste des produits
_buildProductList(), _buildProductList(),
], ],
), ),
@ -171,54 +319,72 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
void _showClientFormDialog() { void _showClientFormDialog() {
Get.dialog( Get.dialog(
AlertDialog( AlertDialog(
title: const Text('Informations Client'), title: Row(
content: SingleChildScrollView( children: [
child: Form( Container(
key: _formKey, padding: const EdgeInsets.all(8),
child: Column( decoration: BoxDecoration(
mainAxisSize: MainAxisSize.min, color: Colors.blue.shade100,
children: [ borderRadius: BorderRadius.circular(8),
_buildTextFormField( ),
controller: _nomController, child: Icon(Icons.person_add, color: Colors.blue.shade700),
label: 'Nom', ),
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null, const SizedBox(width: 12),
), const Text('Informations Client'),
const SizedBox(height: 12), ],
_buildTextFormField( ),
controller: _prenomController, content: Container(
label: 'Prénom', width: 600,
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null, constraints: const BoxConstraints(maxHeight: 600),
), child: SingleChildScrollView(
const SizedBox(height: 12), child: Form(
_buildTextFormField( key: _formKey,
controller: _emailController, child: Column(
label: 'Email', mainAxisSize: MainAxisSize.min,
keyboardType: TextInputType.emailAddress, crossAxisAlignment: CrossAxisAlignment.start,
validator: (value) { children: [
if (value?.isEmpty ?? true) return 'Veuillez entrer un email'; _buildTextFormField(
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) { controller: _nomController,
return 'Email invalide'; label: 'Nom',
} validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
return null; ),
}, const SizedBox(height: 12),
), _buildTextFormField(
const SizedBox(height: 12), controller: _prenomController,
_buildTextFormField( label: 'Prénom',
controller: _telephoneController, validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null,
label: 'Téléphone', ),
keyboardType: TextInputType.phone, const SizedBox(height: 12),
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null, _buildTextFormField(
), controller: _emailController,
const SizedBox(height: 12), label: 'Email',
_buildTextFormField( keyboardType: TextInputType.emailAddress,
controller: _adresseController, validator: (value) {
label: 'Adresse', if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
maxLines: 2, if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null, return 'Email invalide';
), }
const SizedBox(height: 12), return null;
_buildCommercialDropdown(), },
], ),
const SizedBox(height: 12),
_buildTextFormField(
controller: _telephoneController,
label: 'Téléphone',
keyboardType: TextInputType.phone,
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null,
),
const SizedBox(height: 12),
_buildTextFormField(
controller: _adresseController,
label: 'Adresse',
maxLines: 2,
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null,
),
const SizedBox(height: 12),
_buildCommercialDropdown(),
],
),
), ),
), ),
), ),
@ -231,20 +397,15 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade800, backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
), ),
onPressed: () { onPressed: () {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
Get.back(); Get.back();
Get.snackbar( _submitOrder();
'Succès',
'Informations client enregistrées',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} }
}, },
child: const Text('Enregistrer'), child: const Text('Valider la commande'),
), ),
], ],
), ),
@ -306,6 +467,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
); );
} }
// WIDGET MODIFIÉ - Liste des produits (utilise maintenant _filteredProducts)
Widget _buildProductList() { Widget _buildProductList() {
return Card( return Card(
elevation: 4, elevation: 4,
@ -326,14 +488,14 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_products.isEmpty _filteredProducts.isEmpty
? const Center(child: CircularProgressIndicator()) ? _buildEmptyState()
: ListView.builder( : ListView.builder(
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
itemCount: _products.length, itemCount: _filteredProducts.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final product = _products[index]; final product = _filteredProducts[index];
final quantity = _quantites[product.id] ?? 0; final quantity = _quantites[product.id] ?? 0;
return _buildProductListItem(product, quantity); return _buildProductListItem(product, quantity);
@ -345,94 +507,171 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
); );
} }
// NOUVEAU WIDGET - État vide
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucun produit trouvé',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
Text(
'Essayez de modifier vos critères de recherche',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
],
),
),
);
}
// WIDGET MODIFIÉ - Item de produit (ajout d'informations IMEI/Référence)
Widget _buildProductListItem(Product product, int quantity) { Widget _buildProductListItem(Product product, int quantity) {
final bool isOutOfStock = product.stock != null && product.stock! <= 0;
return Card( return Card(
margin: const EdgeInsets.symmetric(vertical: 8), margin: const EdgeInsets.symmetric(vertical: 8),
elevation: 2, elevation: 2,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: ListTile( child: Container(
contentPadding: const EdgeInsets.symmetric( decoration: BoxDecoration(
horizontal: 16, borderRadius: BorderRadius.circular(8),
vertical: 8, border: isOutOfStock
? Border.all(color: Colors.red.shade200, width: 1.5)
: null,
), ),
leading: Container( child: ListTile(
width: 50, contentPadding: const EdgeInsets.symmetric(
height: 50, horizontal: 16,
decoration: BoxDecoration( vertical: 8,
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
), ),
child: const Icon(Icons.shopping_bag, color: Colors.blue), leading: Container(
), width: 50,
title: Text( height: 50,
product.name, decoration: BoxDecoration(
style: const TextStyle(fontWeight: FontWeight.bold), color: isOutOfStock
), ? Colors.red.shade50
subtitle: Column( : Colors.blue.shade50,
crossAxisAlignment: CrossAxisAlignment.start, borderRadius: BorderRadius.circular(8),
children: [ ),
const SizedBox(height: 4), child: Icon(
Text( Icons.shopping_bag,
'${product.price.toStringAsFixed(2)} DA', color: isOutOfStock ? Colors.red : Colors.blue
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( title: Text(
mainAxisSize: MainAxisSize.min, product.name,
style: TextStyle(
fontWeight: FontWeight.bold,
color: isOutOfStock ? Colors.red.shade700 : null,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
IconButton( const SizedBox(height: 4),
icon: const Icon(Icons.remove, size: 18),
onPressed: () {
if (quantity > 0) {
setState(() {
_quantites[product.id!] = quantity - 1;
});
}
},
),
Text( Text(
quantity.toString(), '${product.price.toStringAsFixed(2)} MGA',
style: const TextStyle(fontWeight: FontWeight.bold), style: TextStyle(
), color: Colors.green.shade700,
IconButton( fontWeight: FontWeight.w600,
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,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
},
), ),
if (product.stock != null)
Text(
'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}',
style: TextStyle(
fontSize: 12,
color: isOutOfStock
? Colors.red.shade600
: Colors.grey.shade600,
fontWeight: isOutOfStock ? FontWeight.w600 : FontWeight.normal,
),
),
// Affichage IMEI et Référence
if (product.imei != null && product.imei!.isNotEmpty)
Text(
'IMEI: ${product.imei}',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
fontFamily: 'monospace',
),
),
if (product.reference != null && product.reference!.isNotEmpty)
Text(
'Réf: ${product.reference}',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
),
),
], ],
), ),
trailing: Container(
decoration: BoxDecoration(
color: isOutOfStock
? Colors.grey.shade100
: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove, size: 18),
onPressed: isOutOfStock ? null : () {
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: isOutOfStock ? null : () {
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,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
},
),
],
),
),
), ),
), ),
); );
@ -537,9 +776,9 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
child: const Icon(Icons.shopping_bag, size: 20), child: const Icon(Icons.shopping_bag, size: 20),
), ),
title: Text(product.name), title: Text(product.name),
subtitle: Text('${entry.value} x ${product.price.toStringAsFixed(2)} DA'), subtitle: Text('${entry.value} x ${product.price.toStringAsFixed(2)} MGA'),
trailing: Text( trailing: Text(
'${(entry.value * product.price).toStringAsFixed(2)} DA', '${(entry.value * product.price).toStringAsFixed(2)} MGA',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.blue.shade800, color: Colors.blue.shade800,
@ -569,7 +808,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
Text( Text(
'${total.toStringAsFixed(2)} DA', '${total.toStringAsFixed(2)} MGA',
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -619,32 +858,34 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
} }
Future<void> _submitOrder() async { Future<void> _submitOrder() async {
if (_nomController.text.isEmpty || // Vérifier d'abord si le panier est vide
_prenomController.text.isEmpty || final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList();
_emailController.text.isEmpty || if (itemsInCart.isEmpty) {
_telephoneController.text.isEmpty ||
_adresseController.text.isEmpty) {
Get.back(); // Ferme le bottom sheet
Get.snackbar( Get.snackbar(
'Informations manquantes', 'Panier vide',
'Veuillez remplir les informations client', 'Veuillez ajouter des produits à votre commande',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
_showClientFormDialog(); _showCartBottomSheet(); // Ouvrir le panier pour montrer qu'il est vide
return; return;
} }
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList(); // Ensuite vérifier les informations client
if (itemsInCart.isEmpty) { if (_nomController.text.isEmpty ||
_prenomController.text.isEmpty ||
_emailController.text.isEmpty ||
_telephoneController.text.isEmpty ||
_adresseController.text.isEmpty) {
Get.snackbar( Get.snackbar(
'Panier vide', 'Informations manquantes',
'Veuillez ajouter des produits à votre commande', 'Veuillez remplir les informations client',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
_showClientFormDialog();
return; return;
} }
@ -692,14 +933,12 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
try { try {
await _appDatabase.createCommandeComplete(client, commande, details); await _appDatabase.createCommandeComplete(client, commande, details);
Get.back(); // Ferme le bottom sheet
// Afficher le dialogue de confirmation // Afficher le dialogue de confirmation
await showDialog( await showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Commande Validée'), title: const Text('Commande Validée'),
content: const Text('Votre commande a été enregistrée avec succès.'), content: const Text('Votre commande a été enregistrée et expédiée avec succès.'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
@ -714,6 +953,8 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
_quantites.clear(); _quantites.clear();
_isLoading = false; _isLoading = false;
}); });
// Recharger les produits pour mettre à jour le stock
_loadProducts();
}, },
child: const Text('OK'), child: const Text('OK'),
), ),
@ -743,6 +984,12 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
_emailController.dispose(); _emailController.dispose();
_telephoneController.dispose(); _telephoneController.dispose();
_adresseController.dispose(); _adresseController.dispose();
// Disposal des contrôleurs de filtre
_searchNameController.dispose();
_searchImeiController.dispose();
_searchReferenceController.dispose();
super.dispose(); super.dispose();
} }
} }

5
lib/Views/registrationPage.dart

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:youmazgestion/Models/users.dart'; import 'package:youmazgestion/Models/users.dart';
import 'package:youmazgestion/Models/role.dart'; import 'package:youmazgestion/Models/role.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart'; import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/Views/Dashboard.dart';
import 'package:youmazgestion/accueil.dart'; import 'package:youmazgestion/accueil.dart';
//import '../Services/app_database.dart'; // Changé de authDatabase.dart //import '../Services/app_database.dart'; // Changé de authDatabase.dart
@ -215,7 +216,7 @@ Future<void> _loadPointsDeVente() async {
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute(builder: (context) => const AccueilPage()), MaterialPageRoute(builder: (context) => DashboardPage()),
); );
}, },
child: const Text('OK'), child: const Text('OK'),
@ -416,7 +417,7 @@ _isLoadingPointsDeVente
children: [ children: [
const Icon(Icons.store, size: 20), const Icon(Icons.store, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Text(point['designation'] as String), Text(point['nom']),
], ],
), ),
); );

1
lib/main.dart

@ -17,6 +17,7 @@ void main() async {
// await ProductDatabase.instance.initDatabase(); // await ProductDatabase.instance.initDatabase();
await AppDatabase.instance.initDatabase(); await AppDatabase.instance.initDatabase();
// Afficher les informations de la base (pour debug) // Afficher les informations de la base (pour debug)
// await AppDatabase.instance.printDatabaseInfo(); // await AppDatabase.instance.printDatabaseInfo();

8
pubspec.lock

@ -640,6 +640,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
numbers_to_letters:
dependency: "direct main"
description:
name: numbers_to_letters
sha256: "70c7ed2f04c1982a299e753101fbc2d52ed5b39a2b3dd2a9c07ba131e9c0948e"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
open_file: open_file:
dependency: "direct main" dependency: "direct main"
description: description:

3
pubspec.yaml

@ -64,6 +64,7 @@ dependencies:
excel: ^2.0.1 excel: ^2.0.1
mobile_scanner: ^5.0.0 # ou la version la plus récente mobile_scanner: ^5.0.0 # ou la version la plus récente
fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons
numbers_to_letters: ^1.0.0
@ -105,6 +106,8 @@ flutter:
- assets/airtel_money.png - assets/airtel_money.png
- assets/mvola.jpg - assets/mvola.jpg
- assets/Orange_money.png - assets/Orange_money.png
- assets/fa-solid-900.ttf
- assets/fonts/Roboto-Italic.ttf
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware # https://flutter.dev/assets-and-images/#resolution-aware

Loading…
Cancel
Save