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. 119
      lib/Components/appDrawer.dart
  4. 6
      lib/Components/app_bar.dart
  5. 15
      lib/Models/Client.dart
  6. 60
      lib/Models/produit.dart
  7. 619
      lib/Services/stock_managementDatabase.dart
  8. 120
      lib/Views/Dashboard.dart
  9. 2065
      lib/Views/HandleProduct.dart
  10. 584
      lib/Views/commandManagement.dart
  11. 828
      lib/Views/historique.dart
  12. 101
      lib/Views/loginPage.dart
  13. 643
      lib/Views/mobilepage.dart
  14. 423
      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.

119
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,
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,
),
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(), 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,
), ),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
), ),
child: const Text("Oui"), child: const Text(
"Annuler",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () async { onPressed: () async {
await clearUserData(); await clearUserData();
Get.offAll(const LoginPage()); 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 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());
@ -18,6 +19,7 @@ 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
@ -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:

60
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,23 +39,7 @@ class Product {
return false; return false;
} }
} }
Map<String, dynamic> toMap() { factory Product.fromMap(Map<String, dynamic> map) => Product(
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) {
return Product(
id: map['id'], id: map['id'],
name: map['name'], name: map['name'],
price: map['price'], price: map['price'],
@ -57,7 +49,27 @@ class Product {
description: map['description'], description: map['description'],
qrCode: map['qrCode'], qrCode: map['qrCode'],
reference: map['reference'], reference: map['reference'],
pointDeVenteId : map['point_de_vente_id'] pointDeVenteId: map['point_de_vente_id'],
marque: map['marque'],
ram: map['ram'],
memoireInterne: map['memoire_interne'],
imei: map['imei'],
); );
}
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,
};
} }

619
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
} }
@ -113,9 +113,16 @@ class AppDatabase {
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,8 +147,8 @@ 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,
@ -151,18 +158,43 @@ class AppDatabase {
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,
marque TEXT,
ram TEXT,
memoire_interne TEXT,
imei TEXT UNIQUE,
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id) FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id)
)'''); )''');
} else { } else {
// Si la table existe déjà, ajouter la colonne si elle n'existe pas // 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 $column TEXT');
} catch (e) {
print("La colonne $column existe déjà dans la table products");
}
}
}
// Vérifier aussi point_de_vente_id au cas
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 point_de_vente_id INTEGER REFERENCES points_de_vente(id)');
} catch (e) { } catch (e) {
print("La colonne point_de_vente_id existe déjà dans la table products"); 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]));
}
} }

120
lib/Views/Dashboard.dart

@ -31,7 +31,7 @@ final GlobalKey _salesChartKey = GlobalKey();
late Animation<double> _fadeAnimation; late Animation<double> _fadeAnimation;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadData(); _loadData();
@ -46,8 +46,13 @@ final GlobalKey _salesChartKey = GlobalKey();
), ),
); );
// Démarrer l'animation après un léger délai
Future.delayed(Duration(milliseconds: 50), () {
if (mounted) {
_animationController.forward(); _animationController.forward();
} }
});
}
@override @override
void dispose() { void dispose() {
@ -354,8 +359,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
} }
Widget _buildSalesChart() { Widget _buildSalesChart() {
key: _salesChartKey;
return Card( return Card(
key: _salesChartKey,
elevation: 4, elevation: 4,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -365,20 +371,7 @@ Future<void> _showCategoryProductsDialog(String category) async {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( // ... titre
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( Container(
height: 200, height: 200,
child: FutureBuilder<List<Commande>>( child: FutureBuilder<List<Commande>>(
@ -389,11 +382,27 @@ Future<void> _showCategoryProductsDialog(String category) async {
} }
if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) { if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('Aucune donnée disponible')); 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!); 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( return BarChart(
BarChartData( BarChartData(
alignment: BarChartAlignment.spaceAround, alignment: BarChartAlignment.spaceAround,
@ -536,14 +545,29 @@ Future<void> _showCategoryProductsDialog(String category) async {
} }
final products = snapshot.data!; 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)),
],
),
);
}
final lowStock = products.where((p) => (p.stock ?? 0) < 10).length; final lowStock = products.where((p) => (p.stock ?? 0) < 10).length;
final inStock = products.length - lowStock; final inStock = products.length - lowStock;
return PieChart( // Vérification pour éviter les sections vides
PieChartData( List<PieChartSectionData> sections = [];
sectionsSpace: 0,
centerSpaceRadius: 40, if (lowStock > 0) {
sections: [ sections.add(
PieChartSectionData( PieChartSectionData(
color: Colors.orange, color: Colors.orange,
value: lowStock.toDouble(), value: lowStock.toDouble(),
@ -555,6 +579,11 @@ Future<void> _showCategoryProductsDialog(String category) async {
color: Colors.white, color: Colors.white,
), ),
), ),
);
}
if (inStock > 0) {
sections.add(
PieChartSectionData( PieChartSectionData(
color: Colors.green, color: Colors.green,
value: inStock.toDouble(), value: inStock.toDouble(),
@ -566,9 +595,37 @@ Future<void> _showCategoryProductsDialog(String category) async {
color: Colors.white, 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( pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event, pieTouchResponse) {}, 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, startDegreeOffset: 180,
borderData: FlBorderData(show: false), borderData: FlBorderData(show: false),
@ -590,7 +647,7 @@ Future<void> _showCategoryProductsDialog(String category) async {
), ),
), ),
); );
} }
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:

2065
lib/Views/HandleProduct.dart

File diff suppressed because it is too large

584
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,39 +227,54 @@ 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<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));
}
Future<void> _generateInvoice(Commande commande) async { Future<void> _generateInvoice(Commande commande) 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 pointDeVente = await _database.getPointDeVenteById(1);
? await _database.getUserById(commande.commandeurId!) final iconPhone = await buildIconPhoneText();
: null; final iconChecked = await buildIconCheckedText();
final validateur = commande.validateurId != null final iconGlobe = await buildIconGlobeText();
? await _database.getUserById(commande.validateurId!)
: null; // IMPORTANT: Récupérer tous les détails des produits AVANT de créer le PDF
final pointDeVente = commandeur?.pointDeVenteId != null final List<Map<String, dynamic>> detailsAvecProduits = [];
? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!) for (final detail in details) {
: null; final produit = await _database.getProductById(detail.produitId);
detailsAvecProduits.add({
'detail': detail,
'produit': produit,
});
}
final pdf = pw.Document(); final pdf = pw.Document();
final imageBytes = await loadImage(); final imageBytes = await loadImage();
final image = pw.MemoryImage(imageBytes); final image = pw.MemoryImage(imageBytes);
final italicFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf'));
final headerStyle = pw.TextStyle(
fontSize: 18, // Styles de texte
fontWeight: pw.FontWeight.bold, final smallTextStyle = pw.TextStyle(fontSize: 9);
color: PdfColors.blue900, 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 titleStyle = pw.TextStyle( final boldTexClienttStyle = pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold);
fontSize: 14, final frameTextStyle = pw.TextStyle(fontSize: 10);
fontWeight: pw.FontWeight.bold, final italicTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont);
); final italicTextStyleLogo = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: italicFont);
final subtitleStyle = pw.TextStyle(
fontSize: 12,
color: PdfColors.grey600,
);
pdf.addPage( pdf.addPage(
pw.Page( pw.Page(
@ -271,208 +283,237 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
return pw.Column( return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start, crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [ children: [
// Première ligne: Logo à gauche, informations à droite
pw.Row( pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
crossAxisAlignment: pw.CrossAxisAlignment.start, crossAxisAlignment: pw.CrossAxisAlignment.start,
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [ children: [
// Colonne de gauche avec logo et points de vente
pw.Column( pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start, crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [ children: [
// Logo
pw.Container( pw.Container(
width: 100, width: 150,
height: 80, height: 150,
decoration: pw.BoxDecoration( child: pw.Image(image),
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.Text(' NOTRE COMPETENCE, A VOTRE SERVICE', style: italicTextStyleLogo),
pw.SizedBox(height: 12),
// Liste des points de vente avec checkbox
pw.Column( 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),
),
child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start, crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [ children: [
pw.Text( pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('REMAX by GUYCOM Andravoangy', style: smallTextStyle)]),
'FACTURE', pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 405', style: smallTextStyle)]),
style: pw.TextStyle( pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 416', style: smallTextStyle)]),
fontSize: 20, pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 119', style: smallTextStyle)]),
fontWeight: pw.FontWeight.bold, pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('TRIPOLITSA Analakely BOX 7', style: smallTextStyle)]),
color: PdfColors.blue900,
),
),
pw.SizedBox(height: 8),
pw.Text('N°: ${commande.id}', style: titleStyle),
pw.Text(
'Date: ${DateFormat('dd/MM/yyyy').format(commande.dateCommande)}'),
],
),
),
], ],
), ),
// 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),
], ],
), ),
pw.SizedBox(height: 30), // 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),
// Informations client // Deux petits cadres côte à côte
pw.SizedBox(height: 10),
pw.Row(
children: [
pw.Container( pw.Container(
width: double.infinity, width: 100,
padding: const pw.EdgeInsets.all(12), height: 40,
decoration: pw.BoxDecoration( padding: const pw.EdgeInsets.all(5),
color: PdfColors.grey100, child: pw.Column(
borderRadius: pw.BorderRadius.circular(8), 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( child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [ children: [
pw.Text('FACTURÉ À:', style: titleStyle), pw.Text('Bon de livraison N°:', style: frameTextStyle),
pw.SizedBox(height: 5), pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTexClienttStyle),
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)),
],
), ),
],
), ),
// Grand cadre en dessous
pw.SizedBox(height: 20), pw.SizedBox(height: 20),
// Informations personnel
if (commandeur != null || validateur != null)
pw.Container( pw.Container(
width: double.infinity, width: 300,
padding: const pw.EdgeInsets.all(12), height: 100,
decoration: pw.BoxDecoration( decoration: pw.BoxDecoration(
color: PdfColors.grey100, border: pw.Border.all(color: PdfColors.black, width: 1),
borderRadius: pw.BorderRadius.circular(8),
), ),
padding: const pw.EdgeInsets.all(10),
child: pw.Column( child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start, crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [ children: [
pw.Text('PERSONNEL:', style: titleStyle), pw.Text('ID Client: ', style: frameTextStyle),
pw.SizedBox(height: 5), pw.SizedBox(height: 5),
if (commandeur != null) pw.Text('${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', style: boldTexClienttStyle),
pw.Text('Commandeur: ${commandeur.name} ', pw.SizedBox(height: 5),
style: pw.TextStyle(fontSize: 12)), pw.Container(width: 200, height: 1, color: PdfColors.black),
if (validateur != null) pw.Text(client?.nom ?? 'Non spécifié', style: boldTexClienttStyle),
pw.Text('Validateur: ${validateur.name}', pw.SizedBox(height: 10),
style: pw.TextStyle(fontSize: 12)), pw.Text(client?.telephone ?? 'Non spécifié', style: frameTextStyle),
], ],
), ),
), ),
],
),
],
),
pw.SizedBox(height: 20), pw.SizedBox(height: 20),
// Tableau des produits // Tableau des produits avec plus de colonnes
pw.Text('DÉTAILS DE LA COMMANDE', style: titleStyle),
pw.SizedBox(height: 10),
pw.Table( pw.Table(
border: border: pw.TableBorder.all(width: 0.5),
pw.TableBorder.all(color: PdfColors.grey400, 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: [ children: [
// En-tête du tableau
pw.TableRow( pw.TableRow(
decoration: decoration: const pw.BoxDecoration(color: PdfColors.grey200),
const pw.BoxDecoration(color: PdfColors.blue900),
children: [ children: [
_buildTableCell('Produit', pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Désignations', style: boldTextStyle)),
titleStyle.copyWith(color: PdfColors.white)), pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Qté', style: boldTextStyle, textAlign: pw.TextAlign.center)),
_buildTableCell( pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Prix unitaire', style: boldTextStyle, textAlign: pw.TextAlign.right)),
'Qté', titleStyle.copyWith(color: PdfColors.white)), pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right)),
_buildTableCell('Prix unit.',
titleStyle.copyWith(color: PdfColors.white)),
_buildTableCell(
'Total', titleStyle.copyWith(color: PdfColors.white)),
], ],
), ),
...details.asMap().entries.map((entry) {
final index = entry.key; // Lignes des produits avec détails complets
final detail = entry.value; ...detailsAvecProduits.map((item) {
final isEven = index % 2 == 0; final detail = item['detail'] as DetailCommande;
final produit = item['produit'];
return pw.TableRow( return pw.TableRow(
decoration: pw.BoxDecoration(
color: isEven ? PdfColors.white : PdfColors.grey50,
),
children: [ children: [
_buildTableCell(detail.produitNom ?? 'Produit inconnu'), pw.Padding(
_buildTableCell(detail.quantite.toString()), padding: const pw.EdgeInsets.all(4),
_buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA'), child: pw.Column(
_buildTableCell('${detail.sousTotal.toStringAsFixed(2)} MGA'), 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),
// Total if (produit?.category != null && produit!.category.isNotEmpty && produit?.marque != null && produit!.marque.isNotEmpty)
pw.Container( pw.Text('${produit.category} ${produit.marque}', style: smallTextStyle),
alignment: pw.Alignment.centerRight,
child: pw.Container( // IMEI
padding: const pw.EdgeInsets.all(12), if (produit?.imei != null && produit!.imei!.isNotEmpty)
decoration: pw.BoxDecoration( pw.Text('${produit.imei}', style: smallTextStyle),
color: PdfColors.blue900,
borderRadius: pw.BorderRadius.circular(8),
// 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
],
), ),
child: pw.Text(
'TOTAL: ${commande.montantTotal.toStringAsFixed(2)} MGA',
style: pw.TextStyle(
fontSize: 16,
fontWeight: pw.FontWeight.bold,
color: PdfColors.white,
), ),
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.Spacer(), pw.SizedBox(height: 10),
// Pied de page // Total
pw.Container( pw.Row(
width: double.infinity, mainAxisAlignment: pw.MainAxisAlignment.end,
padding: const pw.EdgeInsets.all(12),
decoration: pw.BoxDecoration(
border: pw.Border(
top: pw.BorderSide(color: PdfColors.grey400, width: 1),
),
),
child: pw.Column(
children: [ children: [
pw.Text( pw.Text('TOTAL', style: boldTextStyle),
'Merci pour votre confiance!', pw.SizedBox(width: 20),
style: pw.TextStyle( pw.Text('${commande.montantTotal.toStringAsFixed(0)}', style: boldTextStyle),
fontSize: 14, ],
fontStyle: pw.FontStyle.italic,
color: PdfColors.blue900,
),
), ),
pw.SizedBox(height: 5),
pw.Text( pw.SizedBox(height: 10),
'Cette facture est générée automatiquement par le système Youmaz Gestion',
style: // Montant en lettres
pw.TextStyle(fontSize: 8, color: PdfColors.grey600), 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),
],
), ),
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('Signature du client', style: smallTextStyle),
pw.SizedBox(height: 20),
pw.Container(width: 150, height: 1, color: PdfColors.black),
], ],
), ),
],
), ),
], ],
); );
@ -484,8 +525,32 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
final file = File('${output.path}/facture_${commande.id}.pdf'); final file = File('${output.path}/facture_${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 _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 { 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);
@ -499,6 +564,16 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!) ? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!)
: null; : 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 pdf = pw.Document();
final imageBytes = await loadImage(); final imageBytes = await loadImage();
final image = pw.MemoryImage(imageBytes); final image = pw.MemoryImage(imageBytes);
@ -511,22 +586,35 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
return pw.Column( return pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.center, crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [ children: [
// En-tête // En-tête avec logo
pw.Center( pw.Center(
child: pw.Container( child: pw.Container(
width: 50, width: 40,
height: 50, height: 40,
child: pw.Image(image), child: pw.Image(image),
), ),
), ),
pw.SizedBox(height: 4), 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', pw.Text('TICKET DE CAISSE',
style: pw.TextStyle( style: pw.TextStyle(
fontSize: 10, fontSize: 10,
fontWeight: pw.FontWeight.bold, fontWeight: pw.FontWeight.bold,
), decoration: pw.TextDecoration.underline,
), )),
pw.Text('N°: ${commande.id}', pw.Text('N°: ${pointDeVente?['abreviation'] ?? 'PV'}-${commande.id}',
style: const pw.TextStyle(fontSize: 8)), style: const pw.TextStyle(fontSize: 8)),
pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}', pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}',
style: const pw.TextStyle(fontSize: 8)), style: const pw.TextStyle(fontSize: 8)),
@ -537,76 +625,118 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
pw.Divider(thickness: 0.5), pw.Divider(thickness: 0.5),
// Client // Informations client
pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}', pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}',
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)), 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)),
// Personnel // Personnel impliqué
if (commandeur != null || validateur != null)
pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Divider(thickness: 0.5),
if (commandeur != null) if (commandeur != null)
pw.Text('Commandeur: ${commandeur.name} ', pw.Text('Vendeur: ${commandeur.name}', style: const pw.TextStyle(fontSize: 7)),
style: const pw.TextStyle(fontSize: 7)),
if (validateur != null) if (validateur != null)
pw.Text('Validateur: ${validateur.name}', pw.Text('Validateur: ${validateur.name}', style: const pw.TextStyle(fontSize: 7)),
style: const pw.TextStyle(fontSize: 7)), ],
),
pw.Divider(thickness: 0.5), pw.Divider(thickness: 0.5),
// Détails // Détails des produits
pw.Table( pw.Table(
columnWidths: { columnWidths: {
0: const pw.FlexColumnWidth(3), 0: const pw.FlexColumnWidth(3.5),
1: const pw.FlexColumnWidth(1), 1: const pw.FlexColumnWidth(1),
2: const pw.FlexColumnWidth(2), 2: const pw.FlexColumnWidth(1.5),
}, },
children: [ children: [
// En-tête du tableau
pw.TableRow( pw.TableRow(
children: [ children: [
pw.Text('Produit', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)), 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('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
pw.Text('Total', 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(
...details.map((detail) => pw.TableRow( 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: [ children: [
pw.Text(detail.produitNom ?? 'Produit', style: const pw.TextStyle(fontSize: 7)), pw.Column(
pw.Text(detail.quantite.toString(), style: const pw.TextStyle(fontSize: 7)), crossAxisAlignment: pw.CrossAxisAlignment.start,
pw.Text('${detail.sousTotal.toStringAsFixed(2)} MGA', style: const pw.TextStyle(fontSize: 7)), 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 // Total et paiement
pw.Row( pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [ children: [
pw.Text('TOTAL:', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)), pw.Text('TOTAL:',
pw.Text('${commande.montantTotal.toStringAsFixed(2)} MGA', 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)), style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)),
], ],
), ),
// Paiement pw.SizedBox(height: 6),
pw.SizedBox(height: 8),
pw.Text('MODE DE PAIEMENT:', style: const pw.TextStyle(fontSize: 8)), // Détails du paiement
pw.Text('MODE DE PAIEMENT:',
style: const pw.TextStyle(fontSize: 8)),
pw.Text( pw.Text(
payment.type == PaymentType.cash payment.type == PaymentType.cash
? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(2)} MGA)' ? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(0)} MGA)'
: 'CARTE BANCAIRE', : 'CARTE BANCAIRE',
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold), style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold),
), ),
if (payment.type == PaymentType.cash && payment.amountGiven > commande.montantTotal) if (payment.type == PaymentType.cash && payment.amountGiven > commande.montantTotal)
pw.Text('Monnaie rendue: ${(payment.amountGiven - commande.montantTotal).toStringAsFixed(2)} MGA', pw.Text('Monnaie rendue: ${(payment.amountGiven - commande.montantTotal).toStringAsFixed(0)} MGA',
style: const pw.TextStyle(fontSize: 8)), style: const pw.TextStyle(fontSize: 8)),
pw.SizedBox(height: 12), pw.SizedBox(height: 12),
pw.Text('Merci pour votre achat !',
// 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)), style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)),
pw.Text('www.guycom.mg',
style: const pw.TextStyle(fontSize: 7)),
], ],
); );
}, },
@ -617,7 +747,7 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
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),

828
lib/Views/historique.dart

File diff suppressed because it is too large

101
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,7 +125,7 @@ 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),
), ),
), ),
const SizedBox(height: 24),
TextField( TextField(
controller: _usernameController, controller: _usernameController,
enabled: !_isLoading, enabled: !_isLoading,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Username', labelText: 'Nom d\'utilisateur',
prefixIcon: const Icon(Icons.person, color: Colors.blueAccent), labelStyle: TextStyle(
color: primaryColor.withOpacity(0.7),
),
prefixIcon: Icon(Icons.person, color: accentColor),
filled: true,
fillColor: accentColor.withOpacity(0.045),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0), borderRadius: BorderRadius.circular(30.0),
borderSide: BorderSide(color: accentColor, width: 2),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
borderSide: BorderSide(color: accentColor, width: 2),
), ),
), ),
), ),
const SizedBox(height: 16.0), const SizedBox(height: 18.0),
TextField( TextField(
controller: _passwordController, controller: _passwordController,
enabled: !_isLoading, enabled: !_isLoading,
obscureText: true,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Password', labelText: 'Mot de passe',
prefixIcon: const Icon(Icons.lock, color: Colors.redAccent), labelStyle: TextStyle(
color: primaryColor.withOpacity(0.7),
),
prefixIcon: Icon(Icons.lock, color: accentColor),
filled: true,
fillColor: accentColor.withOpacity(0.045),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0), borderRadius: BorderRadius.circular(30.0),
), ),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(30.0),
borderSide: BorderSide(color: accentColor, width: 2),
),
), ),
obscureText: true,
onSubmitted: (_) => _login(), onSubmitted: (_) => _login(),
), ),
const SizedBox(height: 16.0), if (_isErrorVisible) ...[
Visibility( const SizedBox(height: 12.0),
visible: _isErrorVisible, Text(
child: Text(
_errorMessage, _errorMessage,
style: const TextStyle( style: const TextStyle(
color: Colors.red, color: Colors.redAccent,
fontSize: 14, fontSize: 15,
fontWeight: FontWeight.w600,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ],
const SizedBox(height: 16.0), const SizedBox(height: 26.0),
ElevatedButton( ElevatedButton(
onPressed: _isLoading ? null : _login, onPressed: _isLoading ? null : _login,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0015B7), backgroundColor: accentColor,
elevation: 5.0, disabledBackgroundColor: accentColor.withOpacity(0.3),
foregroundColor: Colors.white,
elevation: 7.0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30.0), borderRadius: BorderRadius.circular(30.0),
), ),
minimumSize: const Size(double.infinity, 48), minimumSize: const Size(double.infinity, 52),
), ),
child: _isLoading child: _isLoading
? const SizedBox( ? const SizedBox(
height: 20, height: 24,
width: 20, width: 24,
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: Colors.white, color: Colors.white,
strokeWidth: 2, strokeWidth: 2.5,
), ),
) )
: const Text( : const Text(
'Se connecter', 'Se connecter',
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 16, 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'),
),
],
], ],
), ),
), ),
), ),
), ),
) ),
); );
} }
} }

643
lib/Views/mobilepage.dart

@ -96,7 +96,6 @@ class _MainLayoutState extends State<MainLayout> {
} }
} }
// Votre code existant pour NouvelleCommandePage reste inchangé
class NouvelleCommandePage extends StatefulWidget { class NouvelleCommandePage extends StatefulWidget {
const NouvelleCommandePage({super.key}); const NouvelleCommandePage({super.key});
@ -116,10 +115,19 @@ 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
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 = [];
final Map<int, int> _quantites = {}; final Map<int, int> _quantites = {};
// Variables de filtre
bool _showOnlyInStock = false;
// Utilisateurs commerciaux // Utilisateurs commerciaux
List<Users> _commercialUsers = []; List<Users> _commercialUsers = [];
Users? _selectedCommercialUser; Users? _selectedCommercialUser;
@ -129,12 +137,20 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
super.initState(); super.initState();
_loadProducts(); _loadProducts();
_loadCommercialUsers(); _loadCommercialUsers();
// Listeners pour les filtres
_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);
}); });
} }
@ -148,116 +164,350 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
}); });
} }
@override void _filterProducts() {
Widget build(BuildContext context) { final nameQuery = _searchNameController.text.toLowerCase();
return Scaffold( final imeiQuery = _searchImeiController.text.toLowerCase();
floatingActionButton: _buildFloatingCartButton(), final referenceQuery = _searchReferenceController.text.toLowerCase();
drawer: MediaQuery.of(context).size.width > 600 ? null : CustomDrawer(),
body: Column( setState(() {
children: [ _filteredProducts.clear();
// Header
Container( 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);
}
}
});
}
void _toggleStockFilter() {
setState(() {
_showOnlyInStock = !_showOnlyInStock;
});
_filterProducts();
}
void _clearFilters() {
setState(() {
_searchNameController.clear();
_searchImeiController.clear();
_searchReferenceController.clear();
_showOnlyInStock = false;
});
_filterProducts();
}
// Section des filtres adaptée pour mobile
Widget _buildFilterSection() {
final isMobile = MediaQuery.of(context).size.width < 600;
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration( child: Column(
gradient: LinearGradient( crossAxisAlignment: CrossAxisAlignment.start,
colors: [Colors.blue.shade800, Colors.blue.shade600], children: [
begin: Alignment.topCenter, Row(
end: Alignment.bottomCenter, 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: isMobile ? const SizedBox() : const Text('Réinitialiser'),
style: TextButton.styleFrom(
foregroundColor: Colors.grey.shade600,
), ),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 2),
), ),
], ],
), ),
child: Column( const SizedBox(height: 16),
children: [
// 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),
if (!isMobile) ...[
// Version desktop - champs sur la même ligne
Row( Row(
children: [ children: [
Container( Expanded(
width: 50, child: TextField(
height: 50, controller: _searchImeiController,
decoration: BoxDecoration( decoration: InputDecoration(
color: Colors.white, labelText: 'IMEI',
prefixIcon: const Icon(Icons.phone_android),
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 SizedBox(width: 12),
const Expanded( Expanded(
child: Column( child: TextField(
crossAxisAlignment: CrossAxisAlignment.start, controller: _searchReferenceController,
children: [ decoration: InputDecoration(
Text( labelText: 'Référence',
'Nouvelle Commande', prefixIcon: const Icon(Icons.qr_code),
style: TextStyle( border: OutlineInputBorder(
fontSize: 20, borderRadius: BorderRadius.circular(8),
fontWeight: FontWeight.bold, ),
color: Colors.white, filled: true,
fillColor: Colors.grey.shade50,
), ),
), ),
Text( ),
'Créez une nouvelle commande pour un client', ],
style: TextStyle( ),
fontSize: 14, ] else ...[
color: Colors.white70, // Version mobile - champs empilés
TextField(
controller: _searchImeiController,
decoration: InputDecoration(
labelText: 'IMEI',
prefixIcon: const Icon(Icons.phone_android),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
),
const SizedBox(height: 12),
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),
// Boutons de filtre adaptés pour mobile
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _toggleStockFilter,
icon: Icon(
_showOnlyInStock ? Icons.inventory : Icons.inventory_2,
size: 20,
),
label: Text(_showOnlyInStock
? isMobile ? 'Tous' : 'Afficher tous'
: isMobile ? 'En stock' : 'Stock disponible'),
style: ElevatedButton.styleFrom(
backgroundColor: _showOnlyInStock
? Colors.green.shade600
: Colors.blue.shade600,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 12 : 16,
vertical: 8
),
), ),
), ),
], ],
), ),
const SizedBox(height: 8),
// Compteur de résultats
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,
fontSize: isMobile ? 12 : 14,
),
),
),
], ],
), ),
), ),
);
}
// Contenu principal @override
Expanded( Widget build(BuildContext context) {
child: SingleChildScrollView( final isMobile = MediaQuery.of(context).size.width < 600;
padding: const EdgeInsets.all(16.0),
child: Column( return Scaffold(
crossAxisAlignment: CrossAxisAlignment.stretch, floatingActionButton: _buildFloatingCartButton(),
drawer: isMobile ? CustomDrawer() : null,
body: Column(
children: [ children: [
ElevatedButton( // Bouton client - version compacte pour mobile
Padding(
padding: const EdgeInsets.all(16.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), padding: EdgeInsets.symmetric(
vertical: isMobile ? 12 : 16
),
backgroundColor: Colors.blue.shade800, backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
), ),
onPressed: _showClientFormDialog, onPressed: _showClientFormDialog,
child: const Text('Ajouter les informations client'), icon: const Icon(Icons.person_add),
label: Text(
isMobile ? 'Client' : 'Ajouter les informations client',
style: TextStyle(fontSize: isMobile ? 14 : 16),
),
),
),
),
// Section des filtres - adaptée comme dans HistoriquePage
if (!isMobile)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: _buildFilterSection(),
),
// Sur mobile, bouton pour afficher les filtres dans un modal
if (isMobile) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(Icons.filter_alt),
label: const Text('Filtres produits'),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) => SingleChildScrollView(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: _buildFilterSection(),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade700,
foregroundColor: Colors.white,
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
),
// Compteur de résultats visible en haut sur mobile
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, 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,
), ),
const SizedBox(height: 20),
_buildProductList(),
],
), ),
), ),
), ),
], ],
// Liste des produits
Expanded(
child: _buildProductList(),
),
],
), ),
); );
} }
Widget _buildFloatingCartButton() { Widget _buildFloatingCartButton() {
final isMobile = MediaQuery.of(context).size.width < 600;
final cartItemCount = _quantites.values.where((q) => q > 0).length;
return FloatingActionButton.extended( return FloatingActionButton.extended(
onPressed: () { onPressed: () {
_showCartBottomSheet(); _showCartBottomSheet();
}, },
icon: const Icon(Icons.shopping_cart), icon: const Icon(Icons.shopping_cart),
label: Text('Panier (${_quantites.values.where((q) => q > 0).length})'), label: Text(
isMobile ? 'Panier ($cartItemCount)' : 'Panier ($cartItemCount)',
style: TextStyle(fontSize: isMobile ? 12 : 14),
),
backgroundColor: Colors.blue.shade800, backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white, foregroundColor: Colors.white,
); );
} }
void _showClientFormDialog() { void _showClientFormDialog() {
final isMobile = MediaQuery.of(context).size.width < 600;
Get.dialog( Get.dialog(
AlertDialog( AlertDialog(
title: Row( title: Row(
@ -271,12 +521,19 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
child: Icon(Icons.person_add, color: Colors.blue.shade700), child: Icon(Icons.person_add, color: Colors.blue.shade700),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
const Text('Informations Client'), Expanded(
child: Text(
isMobile ? 'Client' : 'Informations Client',
style: TextStyle(fontSize: isMobile ? 16 : 18),
),
),
], ],
), ),
content: Container( content: Container(
width: 600, width: isMobile ? double.maxFinite : 600,
constraints: const BoxConstraints(maxHeight: 600), constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7,
),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Form( child: Form(
key: _formKey, key: _formKey,
@ -338,21 +595,26 @@ 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), padding: EdgeInsets.symmetric(
horizontal: isMobile ? 16 : 20,
vertical: isMobile ? 10 : 12
),
), ),
onPressed: () { onPressed: () {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
Get.back(); Get.back();
// Au lieu d'afficher juste un message, on valide directement la commande
_submitOrder(); _submitOrder();
} }
}, },
child: const Text('Valider la commande'), // Changement de texte ici child: Text(
isMobile ? 'Valider' : 'Valider la commande',
style: TextStyle(fontSize: isMobile ? 12 : 14),
),
), ),
], ],
), ),
); );
} }
Widget _buildTextFormField({ Widget _buildTextFormField({
required TextEditingController controller, required TextEditingController controller,
@ -410,37 +672,49 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
} }
Widget _buildProductList() { Widget _buildProductList() {
return Card( final isMobile = MediaQuery.of(context).size.width < 600;
elevation: 4,
shape: RoundedRectangleBorder( return _filteredProducts.isEmpty
borderRadius: BorderRadius.circular(12), ? _buildEmptyState()
), : ListView.builder(
child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
itemCount: _filteredProducts.length,
itemBuilder: (context, index) {
final product = _filteredProducts[index];
final quantity = _quantites[product.id] ?? 0;
return _buildProductListItem(product, quantity, isMobile);
},
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Icon(
'Produits Disponibles', Icons.search_off,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucun produit trouvé',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w500,
color: Color.fromARGB(255, 9, 56, 95), color: Colors.grey.shade600,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 8),
_products.isEmpty Text(
? const Center(child: CircularProgressIndicator()) 'Modifiez vos critères de recherche',
: ListView.builder( style: TextStyle(
shrinkWrap: true, fontSize: 14,
physics: const NeverScrollableScrollPhysics(), color: Colors.grey.shade500,
itemCount: _products.length, ),
itemBuilder: (context, index) {
final product = _products[index];
final quantity = _quantites[product.id] ?? 0;
return _buildProductListItem(product, quantity);
},
), ),
], ],
), ),
@ -448,63 +722,111 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
); );
} }
Widget _buildProductListItem(Product product, int quantity) { Widget _buildProductListItem(Product product, int quantity, bool isMobile) {
final bool isOutOfStock = product.stock != null && product.stock! <= 0;
return Card( return Card(
margin: const EdgeInsets.symmetric(vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
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: Padding(
width: 50, padding: const EdgeInsets.all(12.0),
height: 50, child: Row(
children: [
Container(
width: isMobile ? 40 : 50,
height: isMobile ? 40 : 50,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue.shade50, color: isOutOfStock
? Colors.red.shade50
: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Icon(Icons.shopping_bag, color: Colors.blue), child: Icon(
Icons.shopping_bag,
size: isMobile ? 20 : 24,
color: isOutOfStock ? Colors.red : Colors.blue,
), ),
title: Text(
product.name,
style: const TextStyle(fontWeight: FontWeight.bold),
), ),
subtitle: Column( const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(
product.name,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: isMobile ? 14 : 16,
color: isOutOfStock ? Colors.red.shade700 : null,
),
),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'${product.price.toStringAsFixed(2)} MGA', '${product.price.toStringAsFixed(2)} MGA',
style: TextStyle( style: TextStyle(
color: Colors.green.shade700, color: Colors.green.shade700,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: isMobile ? 12 : 14,
), ),
), ),
if (product.stock != null) if (product.stock != null)
Text( Text(
'Stock: ${product.stock}', 'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}',
style: TextStyle(
fontSize: isMobile ? 10 : 12,
color: isOutOfStock
? Colors.red.shade600
: Colors.grey.shade600,
fontWeight: isOutOfStock ? FontWeight.w600 : FontWeight.normal,
),
),
// Affichage IMEI et Référence - plus compact sur mobile
if (product.imei != null && product.imei!.isNotEmpty)
Text(
'IMEI: ${product.imei}',
style: TextStyle(
fontSize: isMobile ? 9 : 11,
color: Colors.grey.shade600,
fontFamily: 'monospace',
),
),
if (product.reference != null && product.reference!.isNotEmpty)
Text(
'Réf: ${product.reference}',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: isMobile ? 9 : 11,
color: Colors.grey.shade600, color: Colors.grey.shade600,
), ),
), ),
], ],
), ),
trailing: Container( ),
Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue.shade50, color: isOutOfStock
? Colors.grey.shade100
: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.remove, size: 18), icon: Icon(
onPressed: () { Icons.remove,
size: isMobile ? 16 : 18
),
onPressed: isOutOfStock ? null : () {
if (quantity > 0) { if (quantity > 0) {
setState(() { setState(() {
_quantites[product.id!] = quantity - 1; _quantites[product.id!] = quantity - 1;
@ -514,11 +836,17 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
), ),
Text( Text(
quantity.toString(), quantity.toString(),
style: const TextStyle(fontWeight: FontWeight.bold), style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: isMobile ? 12 : 14,
),
), ),
IconButton( IconButton(
icon: const Icon(Icons.add, size: 18), icon: Icon(
onPressed: () { Icons.add,
size: isMobile ? 16 : 18
),
onPressed: isOutOfStock ? null : () {
if (product.stock == null || quantity < product.stock!) { if (product.stock == null || quantity < product.stock!) {
setState(() { setState(() {
_quantites[product.id!] = quantity + 1; _quantites[product.id!] = quantity + 1;
@ -537,14 +865,19 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
], ],
), ),
), ),
],
),
),
), ),
); );
} }
void _showCartBottomSheet() { void _showCartBottomSheet() {
final isMobile = MediaQuery.of(context).size.width < 600;
Get.bottomSheet( Get.bottomSheet(
Container( Container(
height: MediaQuery.of(context).size.height * 0.7, height: MediaQuery.of(context).size.height * (isMobile ? 0.85 : 0.7),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Colors.white, color: Colors.white,
@ -555,9 +888,12 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text( Text(
'Votre Panier', 'Votre Panier',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: TextStyle(
fontSize: isMobile ? 18 : 20,
fontWeight: FontWeight.bold
),
), ),
IconButton( IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
@ -691,11 +1027,15 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
} }
Widget _buildSubmitButton() { Widget _buildSubmitButton() {
final isMobile = MediaQuery.of(context).size.width < 600;
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), padding: EdgeInsets.symmetric(
vertical: isMobile ? 12 : 16
),
backgroundColor: Colors.blue.shade800, backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@ -705,7 +1045,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
), ),
onPressed: _submitOrder, onPressed: _submitOrder,
child: _isLoading child: _isLoading
? const SizedBox( ? SizedBox(
width: 20, width: 20,
height: 20, height: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
@ -713,9 +1053,9 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
color: Colors.white, color: Colors.white,
), ),
) )
: const Text( : Text(
'Valider la Commande', isMobile ? 'Valider' : 'Valider la Commande',
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: isMobile ? 14 : 16),
), ),
), ),
); );
@ -797,14 +1137,49 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
try { try {
await _appDatabase.createCommandeComplete(client, commande, details); await _appDatabase.createCommandeComplete(client, commande, details);
// Afficher le dialogue de confirmation // Afficher le dialogue de confirmation - adapté pour mobile
final isMobile = MediaQuery.of(context).size.width < 600;
await showDialog( await showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Commande Validée'), title: Row(
content: const Text('Votre commande a été enregistrée et expédiée avec succès.'), children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.check_circle, color: Colors.green.shade700),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'Commande Validée',
style: TextStyle(fontSize: isMobile ? 16 : 18),
),
),
],
),
content: Text(
'Votre commande a été enregistrée et expédiée avec succès.',
style: TextStyle(fontSize: isMobile ? 14 : 16),
),
actions: [ actions: [
TextButton( SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade700,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
vertical: isMobile ? 12 : 16
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
// Réinitialiser le formulaire // Réinitialiser le formulaire
@ -817,8 +1192,14 @@ 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: Text(
'OK',
style: TextStyle(fontSize: isMobile ? 14 : 16),
),
),
), ),
], ],
), ),
@ -837,7 +1218,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
colorText: Colors.white, colorText: Colors.white,
); );
} }
} }
@override @override
void dispose() { void dispose() {
@ -846,6 +1227,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();
} }
} }

423
lib/Views/newCommand.dart

@ -26,10 +26,19 @@ 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 = [];
Users? _selectedCommercialUser; Users? _selectedCommercialUser;
@ -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) {
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), padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration( child: Column(
gradient: LinearGradient( crossAxisAlignment: CrossAxisAlignment.start,
colors: [Colors.blue.shade800, Colors.blue.shade600], children: [
begin: Alignment.topCenter, Row(
end: Alignment.bottomCenter, 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,
), ),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 2),
), ),
], ],
), ),
child: Column( const SizedBox(height: 16),
children: [
// 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( Row(
children: [ children: [
Container( Expanded(
width: 50, child: TextField(
height: 50, controller: _searchImeiController,
decoration: BoxDecoration( decoration: InputDecoration(
color: Colors.white, labelText: 'IMEI',
prefixIcon: const Icon(Icons.phone_android),
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 SizedBox(width: 12),
const Expanded( Expanded(
child: Column( child: TextField(
crossAxisAlignment: CrossAxisAlignment.start, controller: _searchReferenceController,
children: [ decoration: InputDecoration(
Text( labelText: 'Référence',
'Nouvelle Commande', prefixIcon: const Icon(Icons.qr_code),
style: TextStyle( border: OutlineInputBorder(
fontSize: 20, borderRadius: BorderRadius.circular(8),
fontWeight: FontWeight.bold,
color: Colors.white,
), ),
filled: true,
fillColor: Colors.grey.shade50,
), ),
Text(
'Créez une nouvelle commande pour un client',
style: TextStyle(
fontSize: 14,
color: Colors.white70,
), ),
), ),
], ],
), ),
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,
),
),
), ),
], ],
), ),
], ],
), ),
), ),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: _buildFloatingCartButton(),
appBar: CustomAppBar(title: 'Faire un commande'),
drawer: CustomDrawer(),
body: Column(
children: [
// Header
// Contenu principal // 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,12 +319,29 @@ 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: [
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( child: Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildTextFormField( _buildTextFormField(
controller: _nomController, controller: _nomController,
@ -222,6 +387,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
), ),
), ),
), ),
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Get.back(), onPressed: () => Get.back(),
@ -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,13 +507,58 @@ 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: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: isOutOfStock
? Border.all(color: Colors.red.shade200, width: 1.5)
: null,
),
child: ListTile( child: ListTile(
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
@ -361,21 +568,29 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
width: 50, width: 50,
height: 50, height: 50,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue.shade50, color: isOutOfStock
? Colors.red.shade50
: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Icon(Icons.shopping_bag, color: Colors.blue), child: Icon(
Icons.shopping_bag,
color: isOutOfStock ? Colors.red : Colors.blue
),
), ),
title: Text( title: Text(
product.name, product.name,
style: const TextStyle(fontWeight: FontWeight.bold), style: TextStyle(
fontWeight: FontWeight.bold,
color: isOutOfStock ? Colors.red.shade700 : null,
),
), ),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'${product.price.toStringAsFixed(2)} DA', '${product.price.toStringAsFixed(2)} MGA',
style: TextStyle( style: TextStyle(
color: Colors.green.shade700, color: Colors.green.shade700,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -383,9 +598,30 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
), ),
if (product.stock != null) if (product.stock != null)
Text( Text(
'Stock: ${product.stock}', 'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}',
style: TextStyle( style: TextStyle(
fontSize: 12, 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, color: Colors.grey.shade600,
), ),
), ),
@ -393,7 +629,9 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
), ),
trailing: Container( trailing: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue.shade50, color: isOutOfStock
? Colors.grey.shade100
: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
), ),
child: Row( child: Row(
@ -401,7 +639,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.remove, size: 18), icon: const Icon(Icons.remove, size: 18),
onPressed: () { onPressed: isOutOfStock ? null : () {
if (quantity > 0) { if (quantity > 0) {
setState(() { setState(() {
_quantites[product.id!] = quantity - 1; _quantites[product.id!] = quantity - 1;
@ -415,7 +653,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
), ),
IconButton( IconButton(
icon: const Icon(Icons.add, size: 18), icon: const Icon(Icons.add, size: 18),
onPressed: () { onPressed: isOutOfStock ? null : () {
if (product.stock == null || quantity < product.stock!) { if (product.stock == null || quantity < product.stock!) {
setState(() { setState(() {
_quantites[product.id!] = quantity + 1; _quantites[product.id!] = quantity + 1;
@ -435,6 +673,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
), ),
), ),
), ),
),
); );
} }
@ -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

@ -18,6 +18,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();
Get.put( Get.put(

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