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. 6
      lib/Components/app_bar.dart
  5. 15
      lib/Models/Client.dart
  6. 80
      lib/Models/produit.dart
  7. 655
      lib/Services/stock_managementDatabase.dart
  8. 356
      lib/Views/Dashboard.dart
  9. 2939
      lib/Views/HandleProduct.dart
  10. 860
      lib/Views/commandManagement.dart
  11. 914
      lib/Views/historique.dart
  12. 181
      lib/Views/loginPage.dart
  13. 1149
      lib/Views/mobilepage.dart
  14. 667
      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),
title: const Text("Déconnexion"),
onTap: () {
Get.defaultDialog(
title: "Déconnexion",
content: const Text("Voulez-vous vraiment vous déconnecter ?"),
actions: [
TextButton(
child: const Text("Non"),
onPressed: () => Get.back(),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
Get.dialog(
AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
contentPadding: EdgeInsets.zero,
content: Container(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
mainAxisSize: MainAxisSize.min,
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"),
onPressed: () async {
await clearUserData();
Get.offAll(const LoginPage());
},
),
],
);
const SizedBox(height: 16),
const Text(
"Déconnexion",
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,
);
},
),
);

6
lib/Components/app_bar.dart

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

15
lib/Models/Client.dart

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

80
lib/Models/produit.dart

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

655
lib/Services/stock_managementDatabase.dart

@ -37,8 +37,8 @@ class AppDatabase {
await insertDefaultMenus();
await insertDefaultRoles();
await insertDefaultSuperAdmin();
await _insertDefaultClients();
await _insertDefaultCommandes();
// await _insertDefaultClients();
// await _insertDefaultCommandes();
await insertDefaultPointsDeVente(); // Ajouté ici
}
@ -110,12 +110,19 @@ class AppDatabase {
}
// --- POINTS DE VENTE ---
if (!tableNames.contains('points_de_vente')) {
await db.execute('''CREATE TABLE points_de_vente (
id INTEGER PRIMARY KEY AUTOINCREMENT,
designation TEXT NOT NULL UNIQUE
)''');
}
if (!tableNames.contains('points_de_vente')) {
await db.execute('''CREATE TABLE points_de_vente (
id INTEGER PRIMARY KEY AUTOINCREMENT,
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 ---
if (!tableNames.contains('users')) {
@ -140,29 +147,54 @@ class AppDatabase {
}
}
// --- PRODUITS ---
if (!tableNames.contains('products')) {
await db.execute('''CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
image TEXT,
category TEXT NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
description TEXT,
qrCode TEXT,
reference TEXT UNIQUE,
point_de_vente_id INTEGER,
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id)
)''');
} else {
// Si la table existe déjà, ajouter la colonne si elle n'existe pas
// Dans la méthode _createDB, modifier la partie concernant la table products
if (!tableNames.contains('products')) {
await db.execute('''CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
image TEXT,
category TEXT NOT NULL,
stock INTEGER NOT NULL DEFAULT 0,
description TEXT,
qrCode TEXT,
reference TEXT,
point_de_vente_id INTEGER,
marque TEXT,
ram TEXT,
memoire_interne TEXT,
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 {
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) {
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 ---
if (!tableNames.contains('clients')) {
@ -301,25 +333,61 @@ class AppDatabase {
}/* Copier depuis ton code */ }
Future<void> insertDefaultPointsDeVente() async {
Future<void> insertDefaultPointsDeVente() async {
final db = await database;
final existing = await db.query('points_de_vente');
if (existing.isEmpty) {
final defaultPoints = [
{'designation': 'Behoririka'},
{'designation': 'Antanimena'},
{'designation': 'Analakely'},
{'designation': 'Andravoahangy'},
{'designation': 'Anosy'},
{'nom': '405A'},
{'nom': '405B'},
{'nom': '416'},
{'nom': 'S405A'},
{'nom': '417'},
];
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");
}
}
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;
final existingSuperAdmin = await db.rawQuery('''
@ -557,13 +625,17 @@ Future<Users?> getUserById(int id) async {
Future<int> createProduct(Product product) async {
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 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();
if (currentPointDeVenteId > 0 && (product.pointDeVenteId == null || product.pointDeVenteId == 0)) {
if (currentPointDeVenteId > 0) {
productData['point_de_vente_id'] = currentPointDeVenteId;
}
@ -592,6 +664,19 @@ Future<int> updateProduct(Product product) async {
// where: 'id = ?',
// whereArgs: [product.id],
// );/* 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;
return await db.delete(
'products',
@ -739,6 +824,21 @@ Future<int> deleteCommande(int id) async {
}
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
// Créer un détail de commande
Future<int> createDetailCommande(DetailCommande detail) async {
@ -835,110 +935,110 @@ Future<int> updateStock(int productId, int newStock) async {
);
}
// Données par défaut
Future<void> _insertDefaultClients() async {final db = await database;
final existingClients = await db.query('clients');
if (existingClients.isEmpty) {
final defaultClients = [
Client(
nom: 'Dupont',
prenom: 'Jean',
email: 'jean.dupont@email.com',
telephone: '0123456789',
adresse: '123 Rue de la Paix, Paris',
dateCreation: DateTime.now(),
),
Client(
nom: 'Martin',
prenom: 'Marie',
email: 'marie.martin@email.com',
telephone: '0987654321',
adresse: '456 Avenue des Champs, Lyon',
dateCreation: DateTime.now(),
),
Client(
nom: 'Bernard',
prenom: 'Pierre',
email: 'pierre.bernard@email.com',
telephone: '0456789123',
adresse: '789 Boulevard Saint-Michel, Marseille',
dateCreation: DateTime.now(),
),
];
for (var client in defaultClients) {
await db.insert('clients', client.toMap());
}
print("Clients par défaut insérés");
} /* Copier depuis ton code */ }
Future<void> _insertDefaultCommandes() async { final db = await database;
final existingCommandes = await db.query('commandes');
if (existingCommandes.isEmpty) {
// Récupérer quelques produits pour créer des commandes
final produits = await db.query('products', limit: 3);
final clients = await db.query('clients', limit: 3);
if (produits.isNotEmpty && clients.isNotEmpty) {
// Commande 1
final commande1Id = await db.insert('commandes', {
'clientId': clients[0]['id'],
'dateCommande': DateTime.now().subtract(Duration(days: 5)).toIso8601String(),
'statut': StatutCommande.livree.index,
'montantTotal': 150.0,
'notes': 'Commande urgente',
});
await db.insert('details_commandes', {
'commandeId': commande1Id,
'produitId': produits[0]['id'],
'quantite': 2,
'prixUnitaire': 75.0,
'sousTotal': 150.0,
});
// Commande 2
final commande2Id = await db.insert('commandes', {
'clientId': clients[1]['id'],
'dateCommande': DateTime.now().subtract(Duration(days: 2)).toIso8601String(),
'statut': StatutCommande.enPreparation.index,
'montantTotal': 225.0,
'notes': 'Livraison prévue demain',
});
if (produits.length > 1) {
await db.insert('details_commandes', {
'commandeId': commande2Id,
'produitId': produits[1]['id'],
'quantite': 3,
'prixUnitaire': 75.0,
'sousTotal': 225.0,
});
}
// Commande 3
final commande3Id = await db.insert('commandes', {
'clientId': clients[2]['id'],
'dateCommande': DateTime.now().subtract(Duration(hours: 6)).toIso8601String(),
'statut': StatutCommande.confirmee.index,
'montantTotal': 300.0,
'notes': 'Commande standard',
});
if (produits.length > 2) {
await db.insert('details_commandes', {
'commandeId': commande3Id,
'produitId': produits[2]['id'],
'quantite': 4,
'prixUnitaire': 75.0,
'sousTotal': 300.0,
});
}
print("Commandes par défaut insérées");
}
}/* Copier depuis ton code */ }
// // Données par défaut
// Future<void> _insertDefaultClients() async {final db = await database;
// final existingClients = await db.query('clients');
// if (existingClients.isEmpty) {
// final defaultClients = [
// Client(
// nom: 'Dupont',
// prenom: 'Jean',
// email: 'jean.dupont@email.com',
// telephone: '0123456789',
// adresse: '123 Rue de la Paix, Paris',
// dateCreation: DateTime.now(),
// ),
// Client(
// nom: 'Martin',
// prenom: 'Marie',
// email: 'marie.martin@email.com',
// telephone: '0987654321',
// adresse: '456 Avenue des Champs, Lyon',
// dateCreation: DateTime.now(),
// ),
// Client(
// nom: 'Bernard',
// prenom: 'Pierre',
// email: 'pierre.bernard@email.com',
// telephone: '0456789123',
// adresse: '789 Boulevard Saint-Michel, Marseille',
// dateCreation: DateTime.now(),
// ),
// ];
// for (var client in defaultClients) {
// await db.insert('clients', client.toMap());
// }
// print("Clients par défaut insérés");
// } /* Copier depuis ton code */ }
// Future<void> _insertDefaultCommandes() async { final db = await database;
// final existingCommandes = await db.query('commandes');
// if (existingCommandes.isEmpty) {
// // Récupérer quelques produits pour créer des commandes
// final produits = await db.query('products', limit: 3);
// final clients = await db.query('clients', limit: 3);
// if (produits.isNotEmpty && clients.isNotEmpty) {
// // Commande 1
// final commande1Id = await db.insert('commandes', {
// 'clientId': clients[0]['id'],
// 'dateCommande': DateTime.now().subtract(Duration(days: 5)).toIso8601String(),
// 'statut': StatutCommande.livree.index,
// 'montantTotal': 150.0,
// 'notes': 'Commande urgente',
// });
// await db.insert('details_commandes', {
// 'commandeId': commande1Id,
// 'produitId': produits[0]['id'],
// 'quantite': 2,
// 'prixUnitaire': 75.0,
// 'sousTotal': 150.0,
// });
// // Commande 2
// final commande2Id = await db.insert('commandes', {
// 'clientId': clients[1]['id'],
// 'dateCommande': DateTime.now().subtract(Duration(days: 2)).toIso8601String(),
// 'statut': StatutCommande.enPreparation.index,
// 'montantTotal': 225.0,
// 'notes': 'Livraison prévue demain',
// });
// if (produits.length > 1) {
// await db.insert('details_commandes', {
// 'commandeId': commande2Id,
// 'produitId': produits[1]['id'],
// 'quantite': 3,
// 'prixUnitaire': 75.0,
// 'sousTotal': 225.0,
// });
// }
// // Commande 3
// final commande3Id = await db.insert('commandes', {
// 'clientId': clients[2]['id'],
// 'dateCommande': DateTime.now().subtract(Duration(hours: 6)).toIso8601String(),
// 'statut': StatutCommande.confirmee.index,
// 'montantTotal': 300.0,
// 'notes': 'Commande standard',
// });
// if (produits.length > 2) {
// await db.insert('details_commandes', {
// 'commandeId': commande3Id,
// 'produitId': produits[2]['id'],
// 'quantite': 4,
// 'prixUnitaire': 75.0,
// 'sousTotal': 300.0,
// });
// }
// print("Commandes par défaut insérées");
// }
// }/* Copier depuis ton code */ }
// Statistiques
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");
}/* Copier depuis ton code */ }
// 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;
return await db.insert('points_de_vente', {
'designation': designation
},
conflictAlgorithm: ConflictAlgorithm.ignore
);
'designation': designation,
'code': code
}, conflictAlgorithm: ConflictAlgorithm.ignore);
}
Future<List<Map<String, dynamic>>> getPointsDeVente() async {
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;
return await db.update(
'points_de_vente',
{'designation': newDesignation},
{
'designation': newDesignation,
'code': newCode
},
where: 'id = ?',
whereArgs: [id],
);
@ -1139,6 +1260,8 @@ Future<Map<String, int>> getProductCountByCategory() async {
return Map.fromEntries(result.map((e) =>
MapEntry(e['category'] as String, e['count'] as int)));
}
Future<Map<String, dynamic>?> getPointDeVenteById(int id) async {
final db = await database;
final result = await db.query(
@ -1148,4 +1271,238 @@ Future<Map<String, dynamic>?> getPointDeVenteById(int id) async {
);
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]));
}
}

356
lib/Views/Dashboard.dart

@ -30,24 +30,29 @@ final GlobalKey _salesChartKey = GlobalKey();
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_loadData();
@override
void initState() {
super.initState();
_loadData();
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 800),
);
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
),
);
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 800),
);
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _animationController,
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
void dispose() {
@ -354,47 +359,51 @@ Future<void> _showCategoryProductsDialog(String category) async {
}
Widget _buildSalesChart() {
key: _salesChartKey;
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.trending_up, color: Colors.blue),
SizedBox(width: 8),
Text(
'Ventes par mois',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
SizedBox(height: 16),
Container(
height: 200,
child: FutureBuilder<List<Commande>>(
future: _allOrdersFuture,
builder: (context, snapshot) {
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!);
return BarChart(
key: _salesChartKey,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ... titre
Container(
height: 200,
child: FutureBuilder<List<Commande>>(
future: _allOrdersFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.trending_up_outlined, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('Aucune donnée de vente disponible', style: TextStyle(color: Colors.grey)),
],
),
);
}
final salesData = _groupOrdersByMonth(snapshot.data!);
// Vérification si salesData est vide
if (salesData.isEmpty) {
return Center(
child: Text('Aucune donnée de vente disponible', style: TextStyle(color: Colors.grey)),
);
}
return BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
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() {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.inventory, color: Colors.blue),
SizedBox(width: 8),
Text(
'État du stock',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.inventory, color: Colors.blue),
SizedBox(width: 8),
Text(
'État du stock',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
],
),
SizedBox(height: 16),
Container(
height: 200,
child: FutureBuilder<List<Product>>(
future: _database.getProducts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
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 inStock = products.length - lowStock;
return PieChart(
PieChartData(
sectionsSpace: 0,
centerSpaceRadius: 40,
sections: [
PieChartSectionData(
color: Colors.orange,
value: lowStock.toDouble(),
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,
),
),
),
],
),
SizedBox(height: 16),
Container(
height: 200,
child: FutureBuilder<List<Product>>(
future: _database.getProducts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasError || !snapshot.hasData) {
return Center(child: Text('Aucune donnée disponible'));
}
final products = snapshot.data!;
// Vérification si la liste est vide
if (products.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.inventory_2_outlined, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('Aucun produit en stock', style: TextStyle(color: Colors.grey)),
],
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),
),
);
},
),
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildLegendItem(Colors.orange, 'Stock faible'),
SizedBox(width: 16),
_buildLegendItem(Colors.green, 'En stock'),
],
}
if (inStock > 0) {
sections.add(
PieChartSectionData(
color: Colors.green,
value: inStock.toDouble(),
title: '$inStock',
radius: 20,
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) {
return Row(
@ -805,8 +862,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
}
Widget _buildRecentOrdersCard() {
key: _recentOrdersKey;
return Card(
key: _recentOrdersKey,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@ -944,8 +1002,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
Widget _buildRecentClientsCard() {
key: _recentClientsKey;
return Card(
key: _recentClientsKey,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@ -1029,8 +1088,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
}
Widget _buildLowStockCard() {
key: _lowStockKey;
return Card(
key: _lowStockKey,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@ -1136,12 +1196,6 @@ Future<void> _showCategoryProductsDialog(String category) async {
return Colors.orange;
case StatutCommande.confirmee:
return Colors.blue;
case StatutCommande.enPreparation:
return Colors.purple;
case StatutCommande.expediee:
return Colors.teal;
case StatutCommande.livree:
return Colors.green;
case StatutCommande.annulee:
return Colors.red;
default:

2939
lib/Views/HandleProduct.dart

File diff suppressed because it is too large

860
lib/Views/commandManagement.dart

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:numbers_to_letters/numbers_to_letters.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:path_provider/path_provider.dart';
@ -118,10 +119,6 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
message = 'Commande annulée avec succès';
backgroundColor = Colors.orange;
break;
case StatutCommande.livree:
message = 'Commande marquée comme livrée';
backgroundColor = Colors.green;
break;
case StatutCommande.confirmee:
message = 'Commande confirmée';
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 {
final details = await _database.getDetailsCommande(commande.id!);
final client = await _database.getClientById(commande.clientId);
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,
);
Future<pw.Widget> buildIconGlobeText() async {
final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf'));
return pw.Text(String.fromCharCode(0xf0ac), style: pw.TextStyle(font: font));
}
final titleStyle = pw.TextStyle(
fontSize: 14,
fontWeight: pw.FontWeight.bold,
);
final subtitleStyle = pw.TextStyle(
fontSize: 12,
color: PdfColors.grey600,
);
pdf.addPage(
pw.Page(
margin: const pw.EdgeInsets.all(20),
build: (pw.Context context) {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Container(
width: 100,
height: 80,
decoration: pw.BoxDecoration(
border:
pw.Border.all(color: PdfColors.blue900, width: 2),
borderRadius: pw.BorderRadius.circular(8),
),
child: pw.Center(child: pw.Image(image)),
),
pw.SizedBox(height: 10),
pw.Text('guycom', style: headerStyle),
if (pointDeVente != null)
pw.Text('Point de vente: ${pointDeVente['designation']}', style: subtitleStyle),
pw.Text('Tél: +213 123 456 789', style: subtitleStyle),
],
),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.end,
children: [
pw.Container(
padding: const pw.EdgeInsets.all(12),
decoration: pw.BoxDecoration(
color: PdfColors.blue50,
borderRadius: pw.BorderRadius.circular(8),
Future<void> _generateInvoice(Commande commande) async {
final details = await _database.getDetailsCommande(commande.id!);
final client = await _database.getClientById(commande.clientId);
final pointDeVente = await _database.getPointDeVenteById(1);
final iconPhone = await buildIconPhoneText();
final iconChecked = await buildIconCheckedText();
final iconGlobe = await buildIconGlobeText();
// IMPORTANT: Récupérer tous les détails des produits AVANT de créer le PDF
final List<Map<String, dynamic>> detailsAvecProduits = [];
for (final detail in details) {
final produit = await _database.getProductById(detail.produitId);
detailsAvecProduits.add({
'detail': detail,
'produit': produit,
});
}
final pdf = pw.Document();
final imageBytes = await loadImage();
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),
]
)
),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'FACTURE',
style: pw.TextStyle(
fontSize: 20,
fontWeight: pw.FontWeight.bold,
color: PdfColors.blue900,
),
),
pw.SizedBox(height: 8),
pw.Text('N°: ${commande.id}', style: titleStyle),
pw.Text(
'Date: ${DateFormat('dd/MM/yyyy').format(commande.dateCommande)}'),
],
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),
]
)
),
],
),
// 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),
),
],
),
],
),
padding: const pw.EdgeInsets.all(10),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
pw.Text('ID Client: ', style: frameTextStyle),
pw.SizedBox(height: 5),
pw.Text('${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', style: boldTexClienttStyle),
pw.SizedBox(height: 5),
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: 30),
pw.SizedBox(height: 20),
// Informations client
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,
// 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.Text('FACTURÉ À:', style: titleStyle),
pw.SizedBox(height: 5),
pw.Text(client?.nomComplet ?? 'Client inconnu',
style: pw.TextStyle(fontSize: 12)),
if (client?.telephone != null)
pw.Text('Tél: ${client!.telephone}',
style: pw.TextStyle(
fontSize: 10, color: PdfColors.grey600)),
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)),
],
),
),
pw.SizedBox(height: 20),
// Lignes des produits avec détails complets
...detailsAvecProduits.map((item) {
final detail = item['detail'] as DetailCommande;
final produit = item['produit'];
// Informations personnel
if (commandeur != null || validateur != null)
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,
return pw.TableRow(
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.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
// Nom du produit
pw.Text(detail.produitNom ?? 'Produit inconnu',
style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold)),
pw.SizedBox(height: 2),
pw.SizedBox(height: 20),
// Tableau des produits
pw.Text('DÉTAILS DE LA COMMANDE', style: titleStyle),
pw.SizedBox(height: 10),
if (produit?.category != null && produit!.category.isNotEmpty && produit?.marque != null && produit!.marque.isNotEmpty)
pw.Text('${produit.category} ${produit.marque}', style: smallTextStyle),
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;
// IMEI
if (produit?.imei != null && produit!.imei!.isNotEmpty)
pw.Text('${produit.imei}', style: smallTextStyle),
return pw.TableRow(
decoration: pw.BoxDecoration(
color: isEven ? PdfColors.white : PdfColors.grey50,
// 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
],
),
),
children: [
_buildTableCell(detail.produitNom ?? 'Produit inconnu'),
_buildTableCell(detail.quantite.toString()),
_buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA'),
_buildTableCell('${detail.sousTotal.toStringAsFixed(2)} MGA'),
],
);
}),
],
),
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: 20),
pw.SizedBox(height: 10),
// Total
pw.Container(
alignment: pw.Alignment.centerRight,
child: pw.Container(
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,
),
),
),
),
// Total
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.end,
children: [
pw.Text('TOTAL', style: boldTextStyle),
pw.SizedBox(width: 20),
pw.Text('${commande.montantTotal.toStringAsFixed(0)}', style: boldTextStyle),
],
),
pw.Spacer(),
pw.SizedBox(height: 10),
// Pied de page
pw.Container(
width: double.infinity,
padding: const pw.EdgeInsets.all(12),
decoration: pw.BoxDecoration(
border: pw.Border(
top: pw.BorderSide(color: PdfColors.grey400, width: 1),
),
// 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,
children: [
pw.Text('Signature du vendeur', style: smallTextStyle),
pw.SizedBox(height: 20),
pw.Container(width: 150, height: 1, color: PdfColors.black),
],
),
child: pw.Column(
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text(
'Merci pour votre confiance!',
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),
),
pw.Text('Signature du client', style: smallTextStyle),
pw.SizedBox(height: 20),
pw.Container(width: 150, height: 1, color: PdfColors.black),
],
),
),
],
);
},
),
);
],
),
],
);
},
),
);
final output = await getTemporaryDirectory();
final file = File('${output.path}/facture_${commande.id}.pdf');
await file.writeAsBytes(await pdf.save());
await OpenFile.open(file.path);
}
final output = await getTemporaryDirectory();
final file = File('${output.path}/facture_${commande.id}.pdf');
await file.writeAsBytes(await pdf.save());
await OpenFile.open(file.path);
pw.Widget _buildCheckboxPointDeVente(String text, bool checked) {
return pw.Row(
children: [
pw.Container(
width: 10,
height: 10,
decoration: pw.BoxDecoration(
border: pw.Border.all(width: 1),
color: checked ? PdfColors.black : PdfColors.white,
),
),
pw.SizedBox(width: 5),
pw.Text(text, style: pw.TextStyle(fontSize: 9)),
],
);
}
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 {
final details = await _database.getDetailsCommande(commande.id!);
final client = await _database.getClientById(commande.clientId);
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);
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity),
margin: const pw.EdgeInsets.all(4),
build: (pw.Context context) {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
// En-tête
pw.Center(
child: pw.Container(
width: 50,
height: 50,
child: pw.Image(image),
),
),
pw.SizedBox(height: 4),
pw.Text('TICKET DE CAISSE',
style: pw.TextStyle(
fontSize: 10,
fontWeight: pw.FontWeight.bold,
),
final details = await _database.getDetailsCommande(commande.id!);
final client = await _database.getClientById(commande.clientId);
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;
// Récupérer les détails complets des produits
final List<Map<String, dynamic>> detailsAvecProduits = [];
for (final detail in details) {
final produit = await _database.getProductById(detail.produitId);
detailsAvecProduits.add({
'detail': detail,
'produit': produit,
});
}
final pdf = pw.Document();
final imageBytes = await loadImage();
final image = pw.MemoryImage(imageBytes);
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity),
margin: const pw.EdgeInsets.all(4),
build: (pw.Context context) {
return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
// En-tête avec logo
pw.Center(
child: pw.Container(
width: 40,
height: 40,
child: pw.Image(image),
),
pw.Text('N°: ${commande.id}',
style: const pw.TextStyle(fontSize: 8)),
pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}',
),
pw.SizedBox(height: 4),
// 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)),
if (pointDeVente != null)
pw.Text('Point de vente: ${pointDeVente['designation']}',
style: const pw.TextStyle(fontSize: 8)),
pw.Divider(thickness: 0.5),
pw.Divider(thickness: 0.5),
// Informations client
pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}',
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)),
if (client?.telephone != null)
pw.Text('Tél: ${client!.telephone}', style: const pw.TextStyle(fontSize: 7)),
// Client
pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}',
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)),
// Personnel impliqué
if (commandeur != null || validateur != null)
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Divider(thickness: 0.5),
if (commandeur != null)
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)),
],
),
// Personnel
if (commandeur != null)
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),
pw.Divider(thickness: 0.5),
// Détails des produits
pw.Table(
columnWidths: {
0: const pw.FlexColumnWidth(3.5),
1: const pw.FlexColumnWidth(1),
2: const pw.FlexColumnWidth(1.5),
},
children: [
// En-tête du tableau
pw.TableRow(
children: [
pw.Text('Désignation', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
pw.Text('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
pw.Text('P.U', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
],
decoration: const pw.BoxDecoration(
border: pw.Border(bottom: pw.BorderSide(width: 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(
),),
// 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.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.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),
pw.Divider(thickness: 0.5),
// Total
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text('TOTAL:', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)),
pw.Text('${commande.montantTotal.toStringAsFixed(2)} MGA',
style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)),
],
),
// 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)),
],
),
// Paiement
pw.SizedBox(height: 8),
pw.Text('MODE DE PAIEMENT:', style: const pw.TextStyle(fontSize: 8)),
pw.Text(
payment.type == PaymentType.cash
? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(2)} MGA)'
: 'CARTE BANCAIRE',
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold),
),
pw.SizedBox(height: 6),
if (payment.type == PaymentType.cash && payment.amountGiven > commande.montantTotal)
pw.Text('Monnaie rendue: ${(payment.amountGiven - commande.montantTotal).toStringAsFixed(2)} MGA',
style: const pw.TextStyle(fontSize: 8)),
// 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),
),
pw.SizedBox(height: 12),
pw.Text('Merci pour votre achat !',
style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)),
pw.Text('www.guycom.mg',
style: const pw.TextStyle(fontSize: 7)),
],
);
},
),
);
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)),
final output = await getTemporaryDirectory();
final file = File('${output.path}/ticket_${commande.id}.pdf');
await file.writeAsBytes(await pdf.save());
await OpenFile.open(file.path);
}
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 file = File('${output.path}/ticket_${commande.id}.pdf');
await file.writeAsBytes(await pdf.save());
await OpenFile.open(file.path);
}
pw.Widget _buildTableCell(String text, [pw.TextStyle? style]) {
return pw.Padding(
@ -632,12 +762,12 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
return Colors.orange.shade100;
case StatutCommande.confirmee:
return Colors.blue.shade100;
case StatutCommande.enPreparation:
return Colors.amber.shade100;
case StatutCommande.expediee:
return Colors.purple.shade100;
case StatutCommande.livree:
return Colors.green.shade100;
// case StatutCommande.enPreparation:
// return Colors.amber.shade100;
// case StatutCommande.expediee:
// return Colors.purple.shade100;
// case StatutCommande.livree:
// return Colors.green.shade100;
case StatutCommande.annulee:
return Colors.red.shade100;
}
@ -649,12 +779,12 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
return Icons.schedule;
case StatutCommande.confirmee:
return Icons.check_circle_outline;
case StatutCommande.enPreparation:
return Icons.settings;
case StatutCommande.expediee:
return Icons.local_shipping;
case StatutCommande.livree:
return Icons.check_circle;
// case StatutCommande.enPreparation:
// return Icons.settings;
// case StatutCommande.expediee:
// return Icons.local_shipping;
// case StatutCommande.livree:
// return Icons.check_circle;
case StatutCommande.annulee:
return Icons.cancel;
}
@ -1209,12 +1339,12 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
return 'En attente';
case StatutCommande.confirmee:
return 'Confirmée';
case StatutCommande.enPreparation:
return 'En préparation';
case StatutCommande.expediee:
return 'Expédiée';
case StatutCommande.livree:
return 'Livrée';
// case StatutCommande.enPreparation:
// return 'En préparation';
// case StatutCommande.expediee:
// return 'Expédiée';
// case StatutCommande.livree:
// return 'Livrée';
case StatutCommande.annulee:
return 'Annulée';
}
@ -1424,9 +1554,9 @@ class _CommandeActions extends StatelessWidget {
break;
case StatutCommande.confirmee:
case StatutCommande.enPreparation:
case StatutCommande.expediee:
case StatutCommande.livree:
// case StatutCommande.enPreparation:
// case StatutCommande.expediee:
// case StatutCommande.livree:
buttons.add(
Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),

914
lib/Views/historique.dart

File diff suppressed because it is too large

181
lib/Views/loginPage.dart

@ -8,7 +8,6 @@ import 'package:youmazgestion/accueil.dart';
import '../Models/users.dart';
import '../controller/userController.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@ -126,8 +125,8 @@ class _LoginPageState extends State<LoginPage> {
context,
MaterialPageRoute(builder: (context) => const MainLayout()),
);
}else{
Navigator.pushReplacement(
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => DashboardPage()),
);
@ -216,88 +215,124 @@ class _LoginPageState extends State<LoginPage> {
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: 16.0),
TextField(
controller: _passwordController,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: const Icon(Icons.lock, color: Colors.redAccent),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
const SizedBox(height: 24),
TextField(
controller: _usernameController,
enabled: !_isLoading,
decoration: InputDecoration(
labelText: 'Nom d\'utilisateur',
labelStyle: TextStyle(
color: primaryColor.withOpacity(0.7),
),
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,
onSubmitted: (_) => _login(),
),
const SizedBox(height: 16.0),
Visibility(
visible: _isErrorVisible,
child: Text(
_errorMessage,
style: const TextStyle(
color: Colors.red,
fontSize: 14,
const SizedBox(height: 18.0),
TextField(
controller: _passwordController,
enabled: !_isLoading,
obscureText: true,
decoration: InputDecoration(
labelText: 'Mot de passe',
labelStyle: TextStyle(
color: primaryColor.withOpacity(0.7),
),
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(),
),
),
const SizedBox(height: 16.0),
ElevatedButton(
onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0015B7),
elevation: 5.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0),
if (_isErrorVisible) ...[
const SizedBox(height: 12.0),
Text(
_errorMessage,
style: const TextStyle(
color: Colors.redAccent,
fontSize: 15,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
minimumSize: const Size(double.infinity, 48),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Text(
'Se connecter',
style: TextStyle(
color: Colors.white,
fontSize: 16,
],
const SizedBox(height: 26.0),
ElevatedButton(
onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom(
backgroundColor: accentColor,
disabledBackgroundColor: accentColor.withOpacity(0.3),
foregroundColor: Colors.white,
elevation: 7.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0),
),
minimumSize: const Size(double.infinity, 52),
),
child: _isLoading
? 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'),
),
],
],
),
),
),
),
),
)
);
}
}

1149
lib/Views/mobilepage.dart

File diff suppressed because it is too large

667
lib/Views/newCommand.dart

@ -26,10 +26,19 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
final TextEditingController _telephoneController = TextEditingController();
final TextEditingController _adresseController = TextEditingController();
// Contrôleurs pour les filtres - NOUVEAU
final TextEditingController _searchNameController = TextEditingController();
final TextEditingController _searchImeiController = TextEditingController();
final TextEditingController _searchReferenceController = TextEditingController();
// Panier
final List<Product> _products = [];
final List<Product> _filteredProducts = []; // NOUVEAU - Liste filtrée
final Map<int, int> _quantites = {};
// Variables de filtre - NOUVEAU
bool _showOnlyInStock = false;
// Utilisateurs commerciaux
List<Users> _commercialUsers = [];
Users? _selectedCommercialUser;
@ -39,12 +48,20 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
super.initState();
_loadProducts();
_loadCommercialUsers();
// Listeners pour les filtres - NOUVEAU
_searchNameController.addListener(_filterProducts);
_searchImeiController.addListener(_filterProducts);
_searchReferenceController.addListener(_filterProducts);
}
Future<void> _loadProducts() async {
final products = await _appDatabase.getProducts();
setState(() {
_products.clear();
_products.addAll(products);
_filteredProducts.clear();
_filteredProducts.addAll(products); // Initialiser la liste filtrée
});
}
@ -58,78 +75,204 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: _buildFloatingCartButton(),
appBar: CustomAppBar(title: 'Nouvelle Commande'),
drawer: CustomDrawer(),
body: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade800, Colors.blue.shade600],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 2),
// NOUVELLE MÉTHODE - Filtrer les produits
void _filterProducts() {
final nameQuery = _searchNameController.text.toLowerCase();
final imeiQuery = _searchImeiController.text.toLowerCase();
final referenceQuery = _searchReferenceController.text.toLowerCase();
setState(() {
_filteredProducts.clear();
for (var product in _products) {
bool matchesName = nameQuery.isEmpty ||
product.name.toLowerCase().contains(nameQuery);
bool matchesImei = imeiQuery.isEmpty ||
(product.imei?.toLowerCase().contains(imeiQuery) ?? false);
bool matchesReference = referenceQuery.isEmpty ||
(product.reference?.toLowerCase().contains(referenceQuery) ?? false);
bool matchesStock = !_showOnlyInStock ||
(product.stock != null && product.stock! > 0);
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: [
Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.white,
Expanded(
child: TextField(
controller: _searchImeiController,
decoration: InputDecoration(
labelText: 'IMEI',
prefixIcon: const Icon(Icons.phone_android),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.shopping_cart,
color: Colors.blue,
size: 30,
),
filled: true,
fillColor: Colors.grey.shade50,
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Nouvelle Commande',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'Créez une nouvelle commande pour un client',
style: TextStyle(
fontSize: 14,
color: Colors.white70,
),
),
],
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _searchReferenceController,
decoration: InputDecoration(
labelText: 'Référence',
prefixIcon: const Icon(Icons.qr_code),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
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(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
@ -146,6 +289,11 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
child: const Text('Ajouter les informations client'),
),
const SizedBox(height: 20),
// NOUVEAU - Section des filtres
_buildFilterSection(),
// Liste des produits
_buildProductList(),
],
),
@ -171,54 +319,72 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
void _showClientFormDialog() {
Get.dialog(
AlertDialog(
title: const Text('Informations Client'),
content: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildTextFormField(
controller: _nomController,
label: 'Nom',
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
),
const SizedBox(height: 12),
_buildTextFormField(
controller: _prenomController,
label: 'Prénom',
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null,
),
const SizedBox(height: 12),
_buildTextFormField(
controller: _emailController,
label: 'Email',
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
return 'Email invalide';
}
return null;
},
),
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(),
],
title: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.person_add, color: Colors.blue.shade700),
),
const SizedBox(width: 12),
const Text('Informations Client'),
],
),
content: Container(
width: 600,
constraints: const BoxConstraints(maxHeight: 600),
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTextFormField(
controller: _nomController,
label: 'Nom',
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
),
const SizedBox(height: 12),
_buildTextFormField(
controller: _prenomController,
label: 'Prénom',
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null,
),
const SizedBox(height: 12),
_buildTextFormField(
controller: _emailController,
label: 'Email',
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
return 'Email invalide';
}
return null;
},
),
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(
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
onPressed: () {
if (_formKey.currentState!.validate()) {
Get.back();
Get.snackbar(
'Succès',
'Informations client enregistrées',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
_submitOrder();
}
},
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() {
return Card(
elevation: 4,
@ -326,14 +488,14 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
),
),
const SizedBox(height: 16),
_products.isEmpty
? const Center(child: CircularProgressIndicator())
_filteredProducts.isEmpty
? _buildEmptyState()
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _products.length,
itemCount: _filteredProducts.length,
itemBuilder: (context, index) {
final product = _products[index];
final product = _filteredProducts[index];
final quantity = _quantites[product.id] ?? 0;
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) {
final bool isOutOfStock = product.stock != null && product.stock! <= 0;
return Card(
margin: const EdgeInsets.symmetric(vertical: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: isOutOfStock
? Border.all(color: Colors.red.shade200, width: 1.5)
: null,
),
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: const Icon(Icons.shopping_bag, color: Colors.blue),
),
title: Text(
product.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
'${product.price.toStringAsFixed(2)} DA',
style: TextStyle(
color: Colors.green.shade700,
fontWeight: FontWeight.w600,
),
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: isOutOfStock
? Colors.red.shade50
: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.shopping_bag,
color: isOutOfStock ? Colors.red : Colors.blue
),
if (product.stock != null)
Text(
'Stock: ${product.stock}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
trailing: Container(
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
title: Text(
product.name,
style: TextStyle(
fontWeight: FontWeight.bold,
color: isOutOfStock ? Colors.red.shade700 : null,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconButton(
icon: const Icon(Icons.remove, size: 18),
onPressed: () {
if (quantity > 0) {
setState(() {
_quantites[product.id!] = quantity - 1;
});
}
},
),
const SizedBox(height: 4),
Text(
quantity.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.add, size: 18),
onPressed: () {
if (product.stock == null || quantity < product.stock!) {
setState(() {
_quantites[product.id!] = quantity + 1;
});
} else {
Get.snackbar(
'Stock insuffisant',
'Quantité demandée non disponible',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
},
'${product.price.toStringAsFixed(2)} MGA',
style: TextStyle(
color: Colors.green.shade700,
fontWeight: FontWeight.w600,
),
),
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),
),
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(
'${(entry.value * product.price).toStringAsFixed(2)} DA',
'${(entry.value * product.price).toStringAsFixed(2)} MGA',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
@ -569,7 +808,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
'${total.toStringAsFixed(2)} DA',
'${total.toStringAsFixed(2)} MGA',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -619,32 +858,34 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
}
Future<void> _submitOrder() async {
if (_nomController.text.isEmpty ||
_prenomController.text.isEmpty ||
_emailController.text.isEmpty ||
_telephoneController.text.isEmpty ||
_adresseController.text.isEmpty) {
Get.back(); // Ferme le bottom sheet
// Vérifier d'abord si le panier est vide
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList();
if (itemsInCart.isEmpty) {
Get.snackbar(
'Informations manquantes',
'Veuillez remplir les informations client',
'Panier vide',
'Veuillez ajouter des produits à votre commande',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
_showClientFormDialog();
_showCartBottomSheet(); // Ouvrir le panier pour montrer qu'il est vide
return;
}
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList();
if (itemsInCart.isEmpty) {
// Ensuite vérifier les informations client
if (_nomController.text.isEmpty ||
_prenomController.text.isEmpty ||
_emailController.text.isEmpty ||
_telephoneController.text.isEmpty ||
_adresseController.text.isEmpty) {
Get.snackbar(
'Panier vide',
'Veuillez ajouter des produits à votre commande',
'Informations manquantes',
'Veuillez remplir les informations client',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
_showClientFormDialog();
return;
}
@ -692,14 +933,12 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
try {
await _appDatabase.createCommandeComplete(client, commande, details);
Get.back(); // Ferme le bottom sheet
// Afficher le dialogue de confirmation
await showDialog(
context: context,
builder: (context) => AlertDialog(
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: [
TextButton(
onPressed: () {
@ -714,6 +953,8 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
_quantites.clear();
_isLoading = false;
});
// Recharger les produits pour mettre à jour le stock
_loadProducts();
},
child: const Text('OK'),
),
@ -743,6 +984,12 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
_emailController.dispose();
_telephoneController.dispose();
_adresseController.dispose();
// Disposal des contrôleurs de filtre
_searchNameController.dispose();
_searchImeiController.dispose();
_searchReferenceController.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/role.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
import 'package:youmazgestion/Views/Dashboard.dart';
import 'package:youmazgestion/accueil.dart';
//import '../Services/app_database.dart'; // Changé de authDatabase.dart
@ -215,7 +216,7 @@ Future<void> _loadPointsDeVente() async {
Navigator.of(context).pop();
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const AccueilPage()),
MaterialPageRoute(builder: (context) => DashboardPage()),
);
},
child: const Text('OK'),
@ -416,7 +417,7 @@ _isLoadingPointsDeVente
children: [
const Icon(Icons.store, size: 20),
const SizedBox(width: 8),
Text(point['designation'] as String),
Text(point['nom']),
],
),
);

1
lib/main.dart

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

8
pubspec.lock

@ -640,6 +640,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:

3
pubspec.yaml

@ -64,6 +64,7 @@ dependencies:
excel: ^2.0.1
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
numbers_to_letters: ^1.0.0
@ -105,6 +106,8 @@ flutter:
- assets/airtel_money.png
- assets/mvola.jpg
- 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
# https://flutter.dev/assets-and-images/#resolution-aware

Loading…
Cancel
Save