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. 117
      lib/Components/appDrawer.dart
  4. 6
      lib/Components/app_bar.dart
  5. 15
      lib/Models/Client.dart
  6. 60
      lib/Models/produit.dart
  7. 611
      lib/Services/stock_managementDatabase.dart
  8. 116
      lib/Views/Dashboard.dart
  9. 2075
      lib/Views/HandleProduct.dart
  10. 582
      lib/Views/commandManagement.dart
  11. 828
      lib/Views/historique.dart
  12. 99
      lib/Views/loginPage.dart
  13. 639
      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.

117
lib/Components/appDrawer.dart

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

6
lib/Components/app_bar.dart

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

15
lib/Models/Client.dart

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

60
lib/Models/produit.dart

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

611
lib/Services/stock_managementDatabase.dart

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

116
lib/Views/Dashboard.dart

@ -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();
}
});
}
@override
void dispose() {
@ -354,8 +359,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
}
Widget _buildSalesChart() {
key: _salesChartKey;
return Card(
key: _salesChartKey,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@ -365,20 +371,7 @@ Future<void> _showCategoryProductsDialog(String category) async {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.trending_up, color: Colors.blue),
SizedBox(width: 8),
Text(
'Ventes par mois',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
SizedBox(height: 16),
// ... titre
Container(
height: 200,
child: FutureBuilder<List<Commande>>(
@ -389,11 +382,27 @@ Future<void> _showCategoryProductsDialog(String category) async {
}
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!);
// Vérification si salesData est vide
if (salesData.isEmpty) {
return Center(
child: Text('Aucune donnée de vente disponible', style: TextStyle(color: Colors.grey)),
);
}
return BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
@ -536,14 +545,29 @@ Future<void> _showCategoryProductsDialog(String category) async {
}
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 inStock = products.length - lowStock;
return PieChart(
PieChartData(
sectionsSpace: 0,
centerSpaceRadius: 40,
sections: [
// Vérification pour éviter les sections vides
List<PieChartSectionData> sections = [];
if (lowStock > 0) {
sections.add(
PieChartSectionData(
color: Colors.orange,
value: lowStock.toDouble(),
@ -555,6 +579,11 @@ Future<void> _showCategoryProductsDialog(String category) async {
color: Colors.white,
),
),
);
}
if (inStock > 0) {
sections.add(
PieChartSectionData(
color: Colors.green,
value: inStock.toDouble(),
@ -566,9 +595,37 @@ Future<void> _showCategoryProductsDialog(String category) async {
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(
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,
borderData: FlBorderData(show: false),
@ -805,8 +862,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
}
Widget _buildRecentOrdersCard() {
key: _recentOrdersKey;
return Card(
key: _recentOrdersKey,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@ -944,8 +1002,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
Widget _buildRecentClientsCard() {
key: _recentClientsKey;
return Card(
key: _recentClientsKey,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@ -1029,8 +1088,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
}
Widget _buildLowStockCard() {
key: _lowStockKey;
return Card(
key: _lowStockKey,
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
@ -1136,12 +1196,6 @@ Future<void> _showCategoryProductsDialog(String category) async {
return Colors.orange;
case StatutCommande.confirmee:
return Colors.blue;
case StatutCommande.enPreparation:
return Colors.purple;
case StatutCommande.expediee:
return Colors.teal;
case StatutCommande.livree:
return Colors.green;
case StatutCommande.annulee:
return Colors.red;
default:

2075
lib/Views/HandleProduct.dart

File diff suppressed because it is too large

582
lib/Views/commandManagement.dart

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

828
lib/Views/historique.dart

File diff suppressed because it is too large

99
lib/Views/loginPage.dart

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

639
lib/Views/mobilepage.dart

@ -96,7 +96,6 @@ class _MainLayoutState extends State<MainLayout> {
}
}
// Votre code existant pour NouvelleCommandePage reste inchangé
class NouvelleCommandePage extends StatefulWidget {
const NouvelleCommandePage({super.key});
@ -116,10 +115,19 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
final TextEditingController _telephoneController = TextEditingController();
final TextEditingController _adresseController = TextEditingController();
// Contrôleurs pour les filtres
final TextEditingController _searchNameController = TextEditingController();
final TextEditingController _searchImeiController = TextEditingController();
final TextEditingController _searchReferenceController = TextEditingController();
// Panier
final List<Product> _products = [];
final List<Product> _filteredProducts = [];
final Map<int, int> _quantites = {};
// Variables de filtre
bool _showOnlyInStock = false;
// Utilisateurs commerciaux
List<Users> _commercialUsers = [];
Users? _selectedCommercialUser;
@ -129,12 +137,20 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
super.initState();
_loadProducts();
_loadCommercialUsers();
// Listeners pour les filtres
_searchNameController.addListener(_filterProducts);
_searchImeiController.addListener(_filterProducts);
_searchReferenceController.addListener(_filterProducts);
}
Future<void> _loadProducts() async {
final products = await _appDatabase.getProducts();
setState(() {
_products.clear();
_products.addAll(products);
_filteredProducts.clear();
_filteredProducts.addAll(products);
});
}
@ -148,116 +164,350 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: _buildFloatingCartButton(),
drawer: MediaQuery.of(context).size.width > 600 ? null : CustomDrawer(),
body: Column(
children: [
// Header
Container(
void _filterProducts() {
final nameQuery = _searchNameController.text.toLowerCase();
final imeiQuery = _searchImeiController.text.toLowerCase();
final referenceQuery = _searchReferenceController.text.toLowerCase();
setState(() {
_filteredProducts.clear();
for (var product in _products) {
bool matchesName = nameQuery.isEmpty ||
product.name.toLowerCase().contains(nameQuery);
bool matchesImei = imeiQuery.isEmpty ||
(product.imei?.toLowerCase().contains(imeiQuery) ?? false);
bool matchesReference = referenceQuery.isEmpty ||
(product.reference?.toLowerCase().contains(referenceQuery) ?? false);
bool matchesStock = !_showOnlyInStock ||
(product.stock != null && product.stock! > 0);
if (matchesName && matchesImei && matchesReference && matchesStock) {
_filteredProducts.add(product);
}
}
});
}
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),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade800, Colors.blue.shade600],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.filter_list, color: Colors.blue.shade700),
const SizedBox(width: 8),
const Text(
'Filtres de recherche',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
),
),
const Spacer(),
TextButton.icon(
onPressed: _clearFilters,
icon: const Icon(Icons.clear, size: 18),
label: 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(
children: [
const SizedBox(height: 16),
// Champ de recherche par nom
TextField(
controller: _searchNameController,
decoration: InputDecoration(
labelText: 'Rechercher par nom',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
),
const SizedBox(height: 12),
if (!isMobile) ...[
// Version desktop - champs sur la même ligne
Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.white,
Expanded(
child: TextField(
controller: _searchImeiController,
decoration: InputDecoration(
labelText: 'IMEI',
prefixIcon: const Icon(Icons.phone_android),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.shopping_cart,
color: Colors.blue,
size: 30,
filled: true,
fillColor: Colors.grey.shade50,
),
),
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Nouvelle Commande',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
Expanded(
child: TextField(
controller: _searchReferenceController,
decoration: InputDecoration(
labelText: 'Référence',
prefixIcon: const Icon(Icons.qr_code),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
Text(
'Créez une nouvelle commande pour un client',
style: TextStyle(
fontSize: 14,
color: Colors.white70,
),
),
],
),
] else ...[
// 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
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600;
return Scaffold(
floatingActionButton: _buildFloatingCartButton(),
drawer: isMobile ? CustomDrawer() : null,
body: Column(
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(
padding: const EdgeInsets.symmetric(vertical: 16),
padding: EdgeInsets.symmetric(
vertical: isMobile ? 12 : 16
),
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
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),
),
const SizedBox(height: 20),
_buildProductList(),
],
),
),
),
// 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,
),
),
),
),
],
// Liste des produits
Expanded(
child: _buildProductList(),
),
],
),
);
}
Widget _buildFloatingCartButton() {
final isMobile = MediaQuery.of(context).size.width < 600;
final cartItemCount = _quantites.values.where((q) => q > 0).length;
return FloatingActionButton.extended(
onPressed: () {
_showCartBottomSheet();
},
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,
foregroundColor: Colors.white,
);
}
void _showClientFormDialog() {
final isMobile = MediaQuery.of(context).size.width < 600;
Get.dialog(
AlertDialog(
title: Row(
@ -271,12 +521,19 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
child: Icon(Icons.person_add, color: Colors.blue.shade700),
),
const SizedBox(width: 12),
const Text('Informations Client'),
Expanded(
child: Text(
isMobile ? 'Client' : 'Informations Client',
style: TextStyle(fontSize: isMobile ? 16 : 18),
),
),
],
),
content: Container(
width: 600,
constraints: const BoxConstraints(maxHeight: 600),
width: isMobile ? double.maxFinite : 600,
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.7,
),
child: SingleChildScrollView(
child: Form(
key: _formKey,
@ -338,16 +595,21 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 16 : 20,
vertical: isMobile ? 10 : 12
),
),
onPressed: () {
if (_formKey.currentState!.validate()) {
Get.back();
// Au lieu d'afficher juste un message, on valide directement la commande
_submitOrder();
}
},
child: const Text('Valider la commande'), // Changement de texte ici
child: Text(
isMobile ? 'Valider' : 'Valider la commande',
style: TextStyle(fontSize: isMobile ? 12 : 14),
),
),
],
),
@ -410,37 +672,49 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
}
Widget _buildProductList() {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
final isMobile = MediaQuery.of(context).size.width < 600;
return _filteredProducts.isEmpty
? _buildEmptyState()
: ListView.builder(
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Produits Disponibles',
Icon(
Icons.search_off,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucun produit trouvé',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 16),
_products.isEmpty
? const Center(child: CircularProgressIndicator())
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _products.length,
itemBuilder: (context, index) {
final product = _products[index];
final quantity = _quantites[product.id] ?? 0;
return _buildProductListItem(product, quantity);
},
const SizedBox(height: 8),
Text(
'Modifiez vos critères de recherche',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade500,
),
),
],
),
@ -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(
margin: const EdgeInsets.symmetric(vertical: 8),
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: isOutOfStock
? Border.all(color: Colors.red.shade200, width: 1.5)
: null,
),
leading: Container(
width: 50,
height: 50,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
Container(
width: isMobile ? 40 : 50,
height: isMobile ? 40 : 50,
decoration: BoxDecoration(
color: Colors.blue.shade50,
color: isOutOfStock
? Colors.red.shade50
: Colors.blue.shade50,
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,
children: [
Text(
product.name,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: isMobile ? 14 : 16,
color: isOutOfStock ? Colors.red.shade700 : null,
),
),
const SizedBox(height: 4),
Text(
'${product.price.toStringAsFixed(2)} MGA',
style: TextStyle(
color: Colors.green.shade700,
fontWeight: FontWeight.w600,
fontSize: isMobile ? 12 : 14,
),
),
if (product.stock != null)
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: 12,
fontSize: isMobile ? 9 : 11,
color: Colors.grey.shade600,
fontFamily: 'monospace',
),
),
if (product.reference != null && product.reference!.isNotEmpty)
Text(
'Réf: ${product.reference}',
style: TextStyle(
fontSize: isMobile ? 9 : 11,
color: Colors.grey.shade600,
),
),
],
),
trailing: Container(
),
Container(
decoration: BoxDecoration(
color: Colors.blue.shade50,
color: isOutOfStock
? Colors.grey.shade100
: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove, size: 18),
onPressed: () {
icon: Icon(
Icons.remove,
size: isMobile ? 16 : 18
),
onPressed: isOutOfStock ? null : () {
if (quantity > 0) {
setState(() {
_quantites[product.id!] = quantity - 1;
@ -514,11 +836,17 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
),
Text(
quantity.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: isMobile ? 12 : 14,
),
),
IconButton(
icon: const Icon(Icons.add, size: 18),
onPressed: () {
icon: Icon(
Icons.add,
size: isMobile ? 16 : 18
),
onPressed: isOutOfStock ? null : () {
if (product.stock == null || quantity < product.stock!) {
setState(() {
_quantites[product.id!] = quantity + 1;
@ -537,14 +865,19 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
],
),
),
],
),
),
),
);
}
void _showCartBottomSheet() {
final isMobile = MediaQuery.of(context).size.width < 600;
Get.bottomSheet(
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),
decoration: const BoxDecoration(
color: Colors.white,
@ -555,9 +888,12 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
Text(
'Votre Panier',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: isMobile ? 18 : 20,
fontWeight: FontWeight.bold
),
),
IconButton(
icon: const Icon(Icons.close),
@ -691,11 +1027,15 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
}
Widget _buildSubmitButton() {
final isMobile = MediaQuery.of(context).size.width < 600;
return SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
padding: EdgeInsets.symmetric(
vertical: isMobile ? 12 : 16
),
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
@ -705,7 +1045,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
),
onPressed: _submitOrder,
child: _isLoading
? const SizedBox(
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
@ -713,9 +1053,9 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
color: Colors.white,
),
)
: const Text(
'Valider la Commande',
style: TextStyle(fontSize: 16),
: Text(
isMobile ? 'Valider' : 'Valider la Commande',
style: TextStyle(fontSize: isMobile ? 14 : 16),
),
),
);
@ -797,14 +1137,49 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
try {
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(
context: context,
builder: (context) => AlertDialog(
title: const Text('Commande Validée'),
content: const Text('Votre commande a été enregistrée et expédiée avec succès.'),
title: Row(
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: [
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: () {
Navigator.pop(context);
// Réinitialiser le formulaire
@ -817,8 +1192,14 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
_quantites.clear();
_isLoading = false;
});
// Recharger les produits pour mettre à jour le stock
_loadProducts();
},
child: const Text('OK'),
child: Text(
'OK',
style: TextStyle(fontSize: isMobile ? 14 : 16),
),
),
),
],
),
@ -846,6 +1227,12 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
_emailController.dispose();
_telephoneController.dispose();
_adresseController.dispose();
// Disposal des contrôleurs de filtre
_searchNameController.dispose();
_searchImeiController.dispose();
_searchReferenceController.dispose();
super.dispose();
}
}

423
lib/Views/newCommand.dart

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

5
lib/Views/registrationPage.dart

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

1
lib/main.dart

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

8
pubspec.lock

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

3
pubspec.yaml

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

Loading…
Cancel
Save