last last last update
This commit is contained in:
parent
c8fedd08e5
commit
831cce13da
BIN
assets/fa-solid-900.ttf
Normal file
BIN
assets/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Roboto-Italic.ttf
Normal file
BIN
assets/fonts/Roboto-Italic.ttf
Normal file
Binary file not shown.
@ -298,26 +298,123 @@ class CustomDrawer extends StatelessWidget {
|
||||
leading: const Icon(Icons.logout, color: Colors.red),
|
||||
title: const Text("Déconnexion"),
|
||||
onTap: () {
|
||||
Get.defaultDialog(
|
||||
title: "Déconnexion",
|
||||
content: const Text("Voulez-vous vraiment vous déconnecter ?"),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text("Non"),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
content: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.logout_rounded,
|
||||
size: 48,
|
||||
color: Colors.orange.shade600,
|
||||
),
|
||||
child: const Text("Oui"),
|
||||
onPressed: () async {
|
||||
await clearUserData();
|
||||
Get.offAll(const LoginPage());
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"Déconnexion",
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
"Êtes-vous sûr de vouloir vous déconnecter ?",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black87,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
"Vous devrez vous reconnecter pour accéder à votre compte.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Actions
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
side: BorderSide(
|
||||
color: Colors.grey.shade300,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Annuler",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await clearUserData();
|
||||
Get.offAll(const LoginPage());
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade600,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 2,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
"Se déconnecter",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
barrierDismissible: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@ -8,9 +8,10 @@ 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());
|
||||
|
||||
|
||||
CustomAppBar({
|
||||
Key? key,
|
||||
required this.title,
|
||||
@ -18,11 +19,12 @@ 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
|
||||
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 80.0);
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@ -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,
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
class Product {
|
||||
int? id;
|
||||
final int? id;
|
||||
final String name;
|
||||
final double price;
|
||||
final String? image;
|
||||
final String category;
|
||||
final int? stock;
|
||||
final int stock;
|
||||
final String? description;
|
||||
String? qrCode;
|
||||
String? qrCode;
|
||||
final String? reference;
|
||||
final int? pointDeVenteId;
|
||||
final String? marque;
|
||||
final String? ram;
|
||||
final String? memoireInterne;
|
||||
final String? imei;
|
||||
|
||||
Product({
|
||||
this.id,
|
||||
@ -17,12 +21,16 @@ class Product {
|
||||
this.image,
|
||||
required this.category,
|
||||
this.stock = 0,
|
||||
this.description = '',
|
||||
this.description,
|
||||
this.qrCode,
|
||||
this.reference,
|
||||
this.pointDeVenteId
|
||||
this.pointDeVenteId,
|
||||
this.marque,
|
||||
this.ram,
|
||||
this.memoireInterne,
|
||||
this.imei,
|
||||
|
||||
});
|
||||
// Vérifie si le stock est défini
|
||||
bool isStockDefined() {
|
||||
if (stock != null) {
|
||||
print("stock is defined : $stock $name");
|
||||
@ -31,33 +39,37 @@ class Product {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'price': price,
|
||||
'image': image ?? '',
|
||||
'category': category,
|
||||
'stock': stock ?? 0,
|
||||
'description': description ?? '',
|
||||
'qrCode': qrCode ?? '',
|
||||
'reference': reference ?? '',
|
||||
'point_de_vente_id':pointDeVenteId
|
||||
};
|
||||
}
|
||||
factory Product.fromMap(Map<String, dynamic> map) => Product(
|
||||
id: map['id'],
|
||||
name: map['name'],
|
||||
price: map['price'],
|
||||
image: map['image'],
|
||||
category: map['category'],
|
||||
stock: map['stock'],
|
||||
description: map['description'],
|
||||
qrCode: map['qrCode'],
|
||||
reference: map['reference'],
|
||||
pointDeVenteId: map['point_de_vente_id'],
|
||||
marque: map['marque'],
|
||||
ram: map['ram'],
|
||||
memoireInterne: map['memoire_interne'],
|
||||
imei: map['imei'],
|
||||
);
|
||||
|
||||
factory Product.fromMap(Map<String, dynamic> map) {
|
||||
return Product(
|
||||
id: map['id'],
|
||||
name: map['name'],
|
||||
price: map['price'],
|
||||
image: map['image'],
|
||||
category: map['category'],
|
||||
stock: map['stock'],
|
||||
description: map['description'],
|
||||
qrCode: map['qrCode'],
|
||||
reference: map['reference'],
|
||||
pointDeVenteId : map['point_de_vente_id']
|
||||
);
|
||||
}
|
||||
Map<String, dynamic> toMap() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'price': price,
|
||||
'image': image,
|
||||
'category': category,
|
||||
'stock': stock,
|
||||
'description': description,
|
||||
'qrCode': qrCode,
|
||||
'reference': reference,
|
||||
'point_de_vente_id': pointDeVenteId,
|
||||
'marque': marque,
|
||||
'ram': ram,
|
||||
'memoire_interne': memoireInterne,
|
||||
'imei': imei,
|
||||
};
|
||||
}
|
||||
@ -37,8 +37,8 @@ class AppDatabase {
|
||||
await insertDefaultMenus();
|
||||
await insertDefaultRoles();
|
||||
await insertDefaultSuperAdmin();
|
||||
await _insertDefaultClients();
|
||||
await _insertDefaultCommandes();
|
||||
// await _insertDefaultClients();
|
||||
// await _insertDefaultCommandes();
|
||||
await insertDefaultPointsDeVente(); // Ajouté ici
|
||||
}
|
||||
|
||||
@ -110,12 +110,19 @@ class AppDatabase {
|
||||
}
|
||||
|
||||
// --- POINTS DE VENTE ---
|
||||
if (!tableNames.contains('points_de_vente')) {
|
||||
await db.execute('''CREATE TABLE points_de_vente (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
designation TEXT NOT NULL UNIQUE
|
||||
)''');
|
||||
}
|
||||
if (!tableNames.contains('points_de_vente')) {
|
||||
await db.execute('''CREATE TABLE points_de_vente (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nom TEXT NOT NULL UNIQUE
|
||||
)''');
|
||||
} else {
|
||||
// Si la table existe déjà, ajouter la colonne code si elle n'existe pas
|
||||
try {
|
||||
await db.execute('ALTER TABLE points_de_vente ADD COLUMN nom TEXT UNIQUE');
|
||||
} catch (e) {
|
||||
print("La colonne nom existe déjà dans la table points_de_vente");
|
||||
}
|
||||
}
|
||||
|
||||
// --- UTILISATEURS ---
|
||||
if (!tableNames.contains('users')) {
|
||||
@ -140,29 +147,54 @@ class AppDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
// --- PRODUITS ---
|
||||
if (!tableNames.contains('products')) {
|
||||
await db.execute('''CREATE TABLE products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
price REAL NOT NULL,
|
||||
image TEXT,
|
||||
category TEXT NOT NULL,
|
||||
stock INTEGER NOT NULL DEFAULT 0,
|
||||
description TEXT,
|
||||
qrCode TEXT,
|
||||
reference TEXT UNIQUE,
|
||||
point_de_vente_id INTEGER,
|
||||
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id)
|
||||
)''');
|
||||
} else {
|
||||
// Si la table existe déjà, ajouter la colonne si elle n'existe pas
|
||||
// Dans la méthode _createDB, modifier la partie concernant la table products
|
||||
if (!tableNames.contains('products')) {
|
||||
await db.execute('''CREATE TABLE products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
price REAL NOT NULL,
|
||||
image TEXT,
|
||||
category TEXT NOT NULL,
|
||||
stock INTEGER NOT NULL DEFAULT 0,
|
||||
description TEXT,
|
||||
qrCode TEXT,
|
||||
reference TEXT,
|
||||
point_de_vente_id INTEGER,
|
||||
marque TEXT,
|
||||
ram TEXT,
|
||||
memoire_interne TEXT,
|
||||
imei TEXT UNIQUE,
|
||||
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id)
|
||||
)''');
|
||||
} else {
|
||||
// Si la table existe déjà, ajouter les colonnes si elles n'existent pas
|
||||
final columns = await db.rawQuery('PRAGMA table_info(products)');
|
||||
final columnNames = columns.map((col) => col['name'] as String).toList();
|
||||
|
||||
final newColumns = [
|
||||
'marque',
|
||||
'ram',
|
||||
'memoire_interne',
|
||||
'imei'
|
||||
];
|
||||
|
||||
for (var column in newColumns) {
|
||||
if (!columnNames.contains(column)) {
|
||||
try {
|
||||
await db.execute('ALTER TABLE products ADD COLUMN point_de_vente_id INTEGER REFERENCES points_de_vente(id)');
|
||||
await db.execute('ALTER TABLE products ADD COLUMN $column TEXT');
|
||||
} catch (e) {
|
||||
print("La colonne point_de_vente_id existe déjà dans la table products");
|
||||
print("La colonne $column existe déjà dans la table products");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier aussi point_de_vente_id au cas où
|
||||
try {
|
||||
await db.execute('ALTER TABLE products ADD COLUMN point_de_vente_id INTEGER REFERENCES points_de_vente(id)');
|
||||
} catch (e) {
|
||||
print("La colonne point_de_vente_id existe déjà dans la table products");
|
||||
}
|
||||
}
|
||||
|
||||
// --- CLIENTS ---
|
||||
if (!tableNames.contains('clients')) {
|
||||
@ -301,25 +333,61 @@ class AppDatabase {
|
||||
}/* Copier depuis ton code */ }
|
||||
|
||||
|
||||
Future<void> insertDefaultPointsDeVente() async {
|
||||
Future<void> insertDefaultPointsDeVente() async {
|
||||
final db = await database;
|
||||
final existing = await db.query('points_de_vente');
|
||||
|
||||
if (existing.isEmpty) {
|
||||
final defaultPoints = [
|
||||
{'designation': 'Behoririka'},
|
||||
{'designation': 'Antanimena'},
|
||||
{'designation': 'Analakely'},
|
||||
{'designation': 'Andravoahangy'},
|
||||
{'designation': 'Anosy'},
|
||||
{'nom': '405A'},
|
||||
{'nom': '405B'},
|
||||
{'nom': '416'},
|
||||
{'nom': 'S405A'},
|
||||
{'nom': '417'},
|
||||
];
|
||||
|
||||
for (var point in defaultPoints) {
|
||||
await db.insert('points_de_vente', point);
|
||||
try {
|
||||
await db.insert(
|
||||
'points_de_vente',
|
||||
point,
|
||||
conflictAlgorithm: ConflictAlgorithm.ignore
|
||||
);
|
||||
} catch (e) {
|
||||
print("Erreur insertion point de vente ${point['nom']}: $e");
|
||||
}
|
||||
}
|
||||
print("Points de vente par défaut insérés");
|
||||
}
|
||||
}
|
||||
Future<void> debugPointsDeVenteTable() async {
|
||||
final db = await database;
|
||||
try {
|
||||
// Vérifie si la table existe
|
||||
final tables = await db.rawQuery(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='points_de_vente'"
|
||||
);
|
||||
|
||||
if (tables.isEmpty) {
|
||||
print("La table points_de_vente n'existe pas!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Compte le nombre d'entrées
|
||||
final count = await db.rawQuery("SELECT COUNT(*) as count FROM points_de_vente");
|
||||
print("Nombre de points de vente: ${count.first['count']}");
|
||||
|
||||
// Affiche le contenu
|
||||
final content = await db.query('points_de_vente');
|
||||
print("Contenu de la table points_de_vente:");
|
||||
for (var row in content) {
|
||||
print("ID: ${row['id']}, Nom: ${row['nom']}");
|
||||
}
|
||||
} catch (e) {
|
||||
print("Erreur debug table points_de_vente: $e");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> insertDefaultSuperAdmin() async { final db = await database;
|
||||
|
||||
final existingSuperAdmin = await db.rawQuery('''
|
||||
@ -557,13 +625,17 @@ Future<Users?> getUserById(int id) async {
|
||||
Future<int> createProduct(Product product) async {
|
||||
final db = await database;
|
||||
|
||||
// Récupérer le point de vente de l'utilisateur connecté
|
||||
// Si le produit a un point_de_vente_id, on l'utilise directement
|
||||
if (product.pointDeVenteId != null && product.pointDeVenteId! > 0) {
|
||||
return await db.insert('products', product.toMap());
|
||||
}
|
||||
|
||||
// Sinon, on utilise le point de vente de l'utilisateur connecté
|
||||
final userCtrl = Get.find<UserController>();
|
||||
final currentPointDeVenteId = userCtrl.pointDeVenteId;
|
||||
|
||||
// Si le produit n’a 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');
|
||||
// // 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(),
|
||||
),
|
||||
];
|
||||
// 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');
|
||||
// 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 (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',
|
||||
});
|
||||
// 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,
|
||||
});
|
||||
// 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',
|
||||
});
|
||||
// // 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,
|
||||
});
|
||||
}
|
||||
// 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',
|
||||
});
|
||||
// // 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,
|
||||
});
|
||||
}
|
||||
// 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 */ }
|
||||
// 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]));
|
||||
}
|
||||
}
|
||||
@ -30,24 +30,29 @@ final GlobalKey _salesChartKey = GlobalKey();
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadData();
|
||||
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: 800),
|
||||
);
|
||||
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
|
||||
_animationController.forward();
|
||||
}
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadData();
|
||||
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: 800),
|
||||
);
|
||||
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||||
CurvedAnimation(
|
||||
parent: _animationController,
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
|
||||
// Démarrer l'animation après un léger délai
|
||||
Future.delayed(Duration(milliseconds: 50), () {
|
||||
if (mounted) {
|
||||
_animationController.forward();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@ -354,47 +359,51 @@ Future<void> _showCategoryProductsDialog(String category) async {
|
||||
}
|
||||
|
||||
Widget _buildSalesChart() {
|
||||
key: _salesChartKey;
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.trending_up, color: Colors.blue),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Ventes par mois',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Container(
|
||||
height: 200,
|
||||
child: FutureBuilder<List<Commande>>(
|
||||
future: _allOrdersFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) {
|
||||
return Center(child: Text('Aucune donnée disponible'));
|
||||
}
|
||||
|
||||
final salesData = _groupOrdersByMonth(snapshot.data!);
|
||||
|
||||
return BarChart(
|
||||
key: _salesChartKey,
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ... titre
|
||||
Container(
|
||||
height: 200,
|
||||
child: FutureBuilder<List<Commande>>(
|
||||
future: _allOrdersFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.trending_up_outlined, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text('Aucune donnée de vente disponible', style: TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final salesData = _groupOrdersByMonth(snapshot.data!);
|
||||
|
||||
// Vérification si salesData est vide
|
||||
if (salesData.isEmpty) {
|
||||
return Center(
|
||||
child: Text('Aucune donnée de vente disponible', style: TextStyle(color: Colors.grey)),
|
||||
);
|
||||
}
|
||||
|
||||
return BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: salesData.map((e) => e['total']).reduce((a, b) => a > b ? a : b) * 1.2,
|
||||
@ -498,99 +507,147 @@ Future<void> _showCategoryProductsDialog(String category) async {
|
||||
}
|
||||
|
||||
Widget _buildStockChart() {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.inventory, color: Colors.blue),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'État du stock',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
return Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.inventory, color: Colors.blue),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'État du stock',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Container(
|
||||
height: 200,
|
||||
child: FutureBuilder<List<Product>>(
|
||||
future: _database.getProducts(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError || !snapshot.hasData) {
|
||||
return Center(child: Text('Aucune donnée disponible'));
|
||||
}
|
||||
|
||||
final products = snapshot.data!;
|
||||
final lowStock = products.where((p) => (p.stock ?? 0) < 10).length;
|
||||
final inStock = products.length - lowStock;
|
||||
|
||||
return PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 0,
|
||||
centerSpaceRadius: 40,
|
||||
sections: [
|
||||
PieChartSectionData(
|
||||
color: Colors.orange,
|
||||
value: lowStock.toDouble(),
|
||||
title: '$lowStock',
|
||||
radius: 20,
|
||||
titleStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
PieChartSectionData(
|
||||
color: Colors.green,
|
||||
value: inStock.toDouble(),
|
||||
title: '$inStock',
|
||||
radius: 20,
|
||||
titleStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Container(
|
||||
height: 200,
|
||||
child: FutureBuilder<List<Product>>(
|
||||
future: _database.getProducts(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (snapshot.hasError || !snapshot.hasData) {
|
||||
return Center(child: Text('Aucune donnée disponible'));
|
||||
}
|
||||
|
||||
final products = snapshot.data!;
|
||||
|
||||
// Vérification si la liste est vide
|
||||
if (products.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.inventory_2_outlined, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text('Aucun produit en stock', style: TextStyle(color: Colors.grey)),
|
||||
],
|
||||
pieTouchData: PieTouchData(
|
||||
touchCallback: (FlTouchEvent event, pieTouchResponse) {},
|
||||
),
|
||||
startDegreeOffset: 180,
|
||||
borderData: FlBorderData(show: false),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
final lowStock = products.where((p) => (p.stock ?? 0) < 10).length;
|
||||
final inStock = products.length - lowStock;
|
||||
|
||||
// Vérification pour éviter les sections vides
|
||||
List<PieChartSectionData> sections = [];
|
||||
|
||||
if (lowStock > 0) {
|
||||
sections.add(
|
||||
PieChartSectionData(
|
||||
color: Colors.orange,
|
||||
value: lowStock.toDouble(),
|
||||
title: '$lowStock',
|
||||
radius: 20,
|
||||
titleStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (inStock > 0) {
|
||||
sections.add(
|
||||
PieChartSectionData(
|
||||
color: Colors.green,
|
||||
value: inStock.toDouble(),
|
||||
title: '$inStock',
|
||||
radius: 20,
|
||||
titleStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Si toutes les sections sont vides, afficher un message
|
||||
if (sections.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.info_outline, size: 64, color: Colors.grey),
|
||||
SizedBox(height: 16),
|
||||
Text('Aucune donnée de stock disponible', style: TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return PieChart(
|
||||
PieChartData(
|
||||
sectionsSpace: 0,
|
||||
centerSpaceRadius: 40,
|
||||
sections: sections,
|
||||
pieTouchData: PieTouchData(
|
||||
enabled: true, // Activé pour permettre les interactions
|
||||
touchCallback: (FlTouchEvent event, pieTouchResponse) {
|
||||
// Gestion sécurisée des interactions
|
||||
if (pieTouchResponse != null &&
|
||||
pieTouchResponse.touchedSection != null) {
|
||||
// Vous pouvez ajouter une logique ici si nécessaire
|
||||
}
|
||||
},
|
||||
),
|
||||
startDegreeOffset: 180,
|
||||
borderData: FlBorderData(show: false),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildLegendItem(Colors.orange, 'Stock faible'),
|
||||
SizedBox(width: 16),
|
||||
_buildLegendItem(Colors.green, 'En stock'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildLegendItem(Colors.orange, 'Stock faible'),
|
||||
SizedBox(width: 16),
|
||||
_buildLegendItem(Colors.green, 'En stock'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLegendItem(Color color, String text) {
|
||||
return Row(
|
||||
@ -805,8 +862,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
|
||||
}
|
||||
|
||||
Widget _buildRecentOrdersCard() {
|
||||
key: _recentOrdersKey;
|
||||
|
||||
return Card(
|
||||
key: _recentOrdersKey,
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@ -944,8 +1002,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
|
||||
|
||||
|
||||
Widget _buildRecentClientsCard() {
|
||||
key: _recentClientsKey;
|
||||
|
||||
return Card(
|
||||
key: _recentClientsKey,
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@ -1029,8 +1088,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
|
||||
}
|
||||
|
||||
Widget _buildLowStockCard() {
|
||||
key: _lowStockKey;
|
||||
|
||||
return Card(
|
||||
key: _lowStockKey,
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@ -1136,12 +1196,6 @@ Future<void> _showCategoryProductsDialog(String category) async {
|
||||
return Colors.orange;
|
||||
case StatutCommande.confirmee:
|
||||
return Colors.blue;
|
||||
case StatutCommande.enPreparation:
|
||||
return Colors.purple;
|
||||
case StatutCommande.expediee:
|
||||
return Colors.teal;
|
||||
case StatutCommande.livree:
|
||||
return Colors.green;
|
||||
case StatutCommande.annulee:
|
||||
return Colors.red;
|
||||
default:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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,395 +227,528 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
||||
},
|
||||
);
|
||||
}
|
||||
Future<pw.Widget> buildIconPhoneText() async {
|
||||
final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf'));
|
||||
return pw.Text(String.fromCharCode(0xf095), style: pw.TextStyle(font: font));
|
||||
}
|
||||
Future<pw.Widget> buildIconCheckedText() async {
|
||||
final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf'));
|
||||
return pw.Text(String.fromCharCode(0xf14a), style: pw.TextStyle(font: font));
|
||||
}
|
||||
|
||||
Future<void> _generateInvoice(Commande commande) async {
|
||||
final details = await _database.getDetailsCommande(commande.id!);
|
||||
final client = await _database.getClientById(commande.clientId);
|
||||
final commandeur = commande.commandeurId != null
|
||||
? await _database.getUserById(commande.commandeurId!)
|
||||
: null;
|
||||
final validateur = commande.validateurId != null
|
||||
? await _database.getUserById(commande.validateurId!)
|
||||
: null;
|
||||
final pointDeVente = commandeur?.pointDeVenteId != null
|
||||
? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!)
|
||||
: null;
|
||||
Future<pw.Widget> buildIconGlobeText() async {
|
||||
final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf'));
|
||||
return pw.Text(String.fromCharCode(0xf0ac), style: pw.TextStyle(font: font));
|
||||
}
|
||||
|
||||
final pdf = pw.Document();
|
||||
final imageBytes = await loadImage();
|
||||
final image = pw.MemoryImage(imageBytes);
|
||||
|
||||
final headerStyle = pw.TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColors.blue900,
|
||||
);
|
||||
|
||||
final titleStyle = pw.TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
);
|
||||
|
||||
final subtitleStyle = pw.TextStyle(
|
||||
fontSize: 12,
|
||||
color: PdfColors.grey600,
|
||||
);
|
||||
|
||||
pdf.addPage(
|
||||
pw.Page(
|
||||
margin: const pw.EdgeInsets.all(20),
|
||||
build: (pw.Context context) {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Container(
|
||||
width: 100,
|
||||
height: 80,
|
||||
decoration: pw.BoxDecoration(
|
||||
border:
|
||||
pw.Border.all(color: PdfColors.blue900, width: 2),
|
||||
borderRadius: pw.BorderRadius.circular(8),
|
||||
Future<void> _generateInvoice(Commande commande) async {
|
||||
final details = await _database.getDetailsCommande(commande.id!);
|
||||
final client = await _database.getClientById(commande.clientId);
|
||||
final pointDeVente = await _database.getPointDeVenteById(1);
|
||||
final iconPhone = await buildIconPhoneText();
|
||||
final iconChecked = await buildIconCheckedText();
|
||||
final iconGlobe = await buildIconGlobeText();
|
||||
|
||||
// IMPORTANT: Récupérer tous les détails des produits AVANT de créer le PDF
|
||||
final List<Map<String, dynamic>> detailsAvecProduits = [];
|
||||
for (final detail in details) {
|
||||
final produit = await _database.getProductById(detail.produitId);
|
||||
detailsAvecProduits.add({
|
||||
'detail': detail,
|
||||
'produit': produit,
|
||||
});
|
||||
}
|
||||
|
||||
final pdf = pw.Document();
|
||||
final imageBytes = await loadImage();
|
||||
final image = pw.MemoryImage(imageBytes);
|
||||
final italicFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf'));
|
||||
|
||||
// Styles de texte
|
||||
final smallTextStyle = pw.TextStyle(fontSize: 9);
|
||||
final smallBoldTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold);
|
||||
final normalTextStyle = pw.TextStyle(fontSize: 10);
|
||||
final boldTextStyle = pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold);
|
||||
final boldTexClienttStyle = pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold);
|
||||
final frameTextStyle = pw.TextStyle(fontSize: 10);
|
||||
final italicTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont);
|
||||
final italicTextStyleLogo = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: italicFont);
|
||||
|
||||
pdf.addPage(
|
||||
pw.Page(
|
||||
margin: const pw.EdgeInsets.all(20),
|
||||
build: (pw.Context context) {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Première ligne: Logo à gauche, informations à droite
|
||||
pw.Row(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Colonne de gauche avec logo et points de vente
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Logo
|
||||
pw.Container(
|
||||
width: 150,
|
||||
height: 150,
|
||||
child: pw.Image(image),
|
||||
),
|
||||
pw.Text(' NOTRE COMPETENCE, A VOTRE SERVICE', style: italicTextStyleLogo),
|
||||
pw.SizedBox(height: 12),
|
||||
// Liste des points de vente avec checkbox
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('REMAX by GUYCOM Andravoangy', style: smallTextStyle)]),
|
||||
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 405', style: smallTextStyle)]),
|
||||
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 416', style: smallTextStyle)]),
|
||||
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('SUPREME CENTER Behoririka box 119', style: smallTextStyle)]),
|
||||
pw.Row(children: [iconChecked, pw.SizedBox(width: 5), pw.Text('TRIPOLITSA Analakely BOX 7', style: smallTextStyle)]),
|
||||
],
|
||||
),
|
||||
|
||||
// Informations de contact
|
||||
pw.SizedBox(height: 10),
|
||||
pw.Row(children: [iconPhone, pw.SizedBox(width: 5), pw.Text('033 37 808 18', style: smallTextStyle)]),
|
||||
pw.Row(children: [iconGlobe, pw.SizedBox(width: 5), pw.Text('www.guycom.mg', style: smallTextStyle)]),
|
||||
pw.Text('Facebook: GuyCom', style: smallTextStyle),
|
||||
],
|
||||
),
|
||||
|
||||
// Colonne de droite avec cadres de texte
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
pw.Text('Date: ${DateFormat('dd/MM/yyyy').format(DateTime.now())}', style: boldTexClienttStyle),
|
||||
pw.SizedBox(height: 10),
|
||||
pw.Container(width: 200, height: 1, color: PdfColors.black),
|
||||
|
||||
// Deux petits cadres côte à côte
|
||||
pw.SizedBox(height: 10),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Container(
|
||||
width: 100,
|
||||
height: 40,
|
||||
padding: const pw.EdgeInsets.all(5),
|
||||
child: pw.Column(
|
||||
children: [
|
||||
pw.Text('Boutique:', style: frameTextStyle),
|
||||
pw.Text('${pointDeVente?['nom'] ?? 'S405A'}', style: boldTexClienttStyle),
|
||||
]
|
||||
)
|
||||
),
|
||||
child: pw.Center(child: pw.Image(image)),
|
||||
pw.SizedBox(width: 10),
|
||||
pw.Container(
|
||||
width: 100,
|
||||
height: 40,
|
||||
padding: const pw.EdgeInsets.all(5),
|
||||
child: pw.Column(
|
||||
children: [
|
||||
pw.Text('Bon de livraison N°:', style: frameTextStyle),
|
||||
pw.Text('${pointDeVente?['nom'] ?? 'S405A'}-P${commande.id}', style: boldTexClienttStyle),
|
||||
]
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Grand cadre en dessous
|
||||
pw.SizedBox(height: 20),
|
||||
pw.Container(
|
||||
width: 300,
|
||||
height: 100,
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border.all(color: PdfColors.black, width: 1),
|
||||
),
|
||||
pw.SizedBox(height: 10),
|
||||
pw.Text('guycom', style: headerStyle),
|
||||
if (pointDeVente != null)
|
||||
pw.Text('Point de vente: ${pointDeVente['designation']}', style: subtitleStyle),
|
||||
pw.Text('Tél: +213 123 456 789', style: subtitleStyle),
|
||||
],
|
||||
),
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||
padding: const pw.EdgeInsets.all(10),
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
pw.Text('ID Client: ', style: frameTextStyle),
|
||||
pw.SizedBox(height: 5),
|
||||
pw.Text('${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', style: boldTexClienttStyle),
|
||||
pw.SizedBox(height: 5),
|
||||
pw.Container(width: 200, height: 1, color: PdfColors.black),
|
||||
pw.Text(client?.nom ?? 'Non spécifié', style: boldTexClienttStyle),
|
||||
pw.SizedBox(height: 10),
|
||||
pw.Text(client?.telephone ?? 'Non spécifié', style: frameTextStyle),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 20),
|
||||
|
||||
// Tableau des produits avec plus de colonnes
|
||||
pw.Table(
|
||||
border: pw.TableBorder.all(width: 0.5),
|
||||
columnWidths: {
|
||||
0: const pw.FlexColumnWidth(3), // Désignation
|
||||
1: const pw.FlexColumnWidth(1), // Qté
|
||||
2: const pw.FlexColumnWidth(2), // Prix unitaire
|
||||
3: const pw.FlexColumnWidth(2), // Montant
|
||||
},
|
||||
children: [
|
||||
// En-tête du tableau
|
||||
pw.TableRow(
|
||||
decoration: const pw.BoxDecoration(color: PdfColors.grey200),
|
||||
children: [
|
||||
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Désignations', style: boldTextStyle)),
|
||||
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Qté', style: boldTextStyle, textAlign: pw.TextAlign.center)),
|
||||
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Prix unitaire', style: boldTextStyle, textAlign: pw.TextAlign.right)),
|
||||
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right)),
|
||||
],
|
||||
),
|
||||
|
||||
// Lignes des produits avec détails complets
|
||||
...detailsAvecProduits.map((item) {
|
||||
final detail = item['detail'] as DetailCommande;
|
||||
final produit = item['produit'];
|
||||
|
||||
return pw.TableRow(
|
||||
children: [
|
||||
pw.Container(
|
||||
padding: const pw.EdgeInsets.all(12),
|
||||
decoration: pw.BoxDecoration(
|
||||
color: PdfColors.blue50,
|
||||
borderRadius: pw.BorderRadius.circular(8),
|
||||
),
|
||||
pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4),
|
||||
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)}'),
|
||||
// Nom du produit
|
||||
pw.Text(detail.produitNom ?? 'Produit inconnu',
|
||||
style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold)),
|
||||
pw.SizedBox(height: 2),
|
||||
|
||||
|
||||
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
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 30),
|
||||
|
||||
// Informations client
|
||||
pw.Container(
|
||||
width: double.infinity,
|
||||
padding: const pw.EdgeInsets.all(12),
|
||||
decoration: pw.BoxDecoration(
|
||||
color: PdfColors.grey100,
|
||||
borderRadius: pw.BorderRadius.circular(8),
|
||||
),
|
||||
child: pw.Column(
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 10),
|
||||
|
||||
// Total
|
||||
pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.end,
|
||||
children: [
|
||||
pw.Text('TOTAL', style: boldTextStyle),
|
||||
pw.SizedBox(width: 20),
|
||||
pw.Text('${commande.montantTotal.toStringAsFixed(0)}', style: boldTextStyle),
|
||||
],
|
||||
),
|
||||
|
||||
pw.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('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('Signature du vendeur', style: smallTextStyle),
|
||||
pw.SizedBox(height: 20),
|
||||
pw.Container(width: 150, height: 1, color: PdfColors.black),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 20),
|
||||
|
||||
// Informations personnel
|
||||
if (commandeur != null || validateur != null)
|
||||
pw.Container(
|
||||
width: double.infinity,
|
||||
padding: const pw.EdgeInsets.all(12),
|
||||
decoration: pw.BoxDecoration(
|
||||
color: PdfColors.grey100,
|
||||
borderRadius: pw.BorderRadius.circular(8),
|
||||
),
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text('PERSONNEL:', style: titleStyle),
|
||||
pw.SizedBox(height: 5),
|
||||
if (commandeur != null)
|
||||
pw.Text('Commandeur: ${commandeur.name} ',
|
||||
style: pw.TextStyle(fontSize: 12)),
|
||||
if (validateur != null)
|
||||
pw.Text('Validateur: ${validateur.name}',
|
||||
style: pw.TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 20),
|
||||
|
||||
// Tableau des produits
|
||||
pw.Text('DÉTAILS DE LA COMMANDE', style: titleStyle),
|
||||
pw.SizedBox(height: 10),
|
||||
|
||||
pw.Table(
|
||||
border:
|
||||
pw.TableBorder.all(color: PdfColors.grey400, width: 0.5),
|
||||
children: [
|
||||
pw.TableRow(
|
||||
decoration:
|
||||
const pw.BoxDecoration(color: PdfColors.blue900),
|
||||
children: [
|
||||
_buildTableCell('Produit',
|
||||
titleStyle.copyWith(color: PdfColors.white)),
|
||||
_buildTableCell(
|
||||
'Qté', titleStyle.copyWith(color: PdfColors.white)),
|
||||
_buildTableCell('Prix unit.',
|
||||
titleStyle.copyWith(color: PdfColors.white)),
|
||||
_buildTableCell(
|
||||
'Total', titleStyle.copyWith(color: PdfColors.white)),
|
||||
],
|
||||
),
|
||||
...details.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final detail = entry.value;
|
||||
final isEven = index % 2 == 0;
|
||||
|
||||
return pw.TableRow(
|
||||
decoration: pw.BoxDecoration(
|
||||
color: isEven ? PdfColors.white : PdfColors.grey50,
|
||||
),
|
||||
children: [
|
||||
_buildTableCell(detail.produitNom ?? 'Produit inconnu'),
|
||||
_buildTableCell(detail.quantite.toString()),
|
||||
_buildTableCell('${detail.prixUnitaire.toStringAsFixed(2)} MGA'),
|
||||
_buildTableCell('${detail.sousTotal.toStringAsFixed(2)} MGA'),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 20),
|
||||
|
||||
// Total
|
||||
pw.Container(
|
||||
alignment: pw.Alignment.centerRight,
|
||||
child: pw.Container(
|
||||
padding: const pw.EdgeInsets.all(12),
|
||||
decoration: pw.BoxDecoration(
|
||||
color: PdfColors.blue900,
|
||||
borderRadius: pw.BorderRadius.circular(8),
|
||||
),
|
||||
child: pw.Text(
|
||||
'TOTAL: ${commande.montantTotal.toStringAsFixed(2)} MGA',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
color: PdfColors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
pw.Spacer(),
|
||||
|
||||
// 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(
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text(
|
||||
'Merci pour votre confiance!',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 14,
|
||||
fontStyle: pw.FontStyle.italic,
|
||||
color: PdfColors.blue900,
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 5),
|
||||
pw.Text(
|
||||
'Cette facture est générée automatiquement par le système Youmaz Gestion',
|
||||
style:
|
||||
pw.TextStyle(fontSize: 8, color: PdfColors.grey600),
|
||||
),
|
||||
pw.Text('Signature du client', style: smallTextStyle),
|
||||
pw.SizedBox(height: 20),
|
||||
pw.Container(width: 150, height: 1, color: PdfColors.black),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final output = await getTemporaryDirectory();
|
||||
final file = File('${output.path}/facture_${commande.id}.pdf');
|
||||
await file.writeAsBytes(await pdf.save());
|
||||
await OpenFile.open(file.path);
|
||||
final output = await getTemporaryDirectory();
|
||||
final file = File('${output.path}/facture_${commande.id}.pdf');
|
||||
await file.writeAsBytes(await pdf.save());
|
||||
await OpenFile.open(file.path);
|
||||
}
|
||||
|
||||
|
||||
pw.Widget _buildCheckboxPointDeVente(String text, bool checked) {
|
||||
return pw.Row(
|
||||
children: [
|
||||
pw.Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: pw.BoxDecoration(
|
||||
border: pw.Border.all(width: 1),
|
||||
color: checked ? PdfColors.black : PdfColors.white,
|
||||
),
|
||||
),
|
||||
pw.SizedBox(width: 5),
|
||||
pw.Text(text, style: pw.TextStyle(fontSize: 9)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _numberToWords(int number) {
|
||||
// Implémentez la conversion du nombre en lettres ici
|
||||
// Exemple simplifié:
|
||||
NumbersToLetters.toLetters('fr', number);
|
||||
return NumbersToLetters.toLetters('fr', number);
|
||||
}
|
||||
Future<void> _generateReceipt(Commande commande, PaymentMethod payment) async {
|
||||
final details = await _database.getDetailsCommande(commande.id!);
|
||||
final client = await _database.getClientById(commande.clientId);
|
||||
final commandeur = commande.commandeurId != null
|
||||
? await _database.getUserById(commande.commandeurId!)
|
||||
: null;
|
||||
final validateur = commande.validateurId != null
|
||||
? await _database.getUserById(commande.validateurId!)
|
||||
: null;
|
||||
final pointDeVente = commandeur?.pointDeVenteId != null
|
||||
? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!)
|
||||
: null;
|
||||
final details = await _database.getDetailsCommande(commande.id!);
|
||||
final client = await _database.getClientById(commande.clientId);
|
||||
final commandeur = commande.commandeurId != null
|
||||
? await _database.getUserById(commande.commandeurId!)
|
||||
: null;
|
||||
final validateur = commande.validateurId != null
|
||||
? await _database.getUserById(commande.validateurId!)
|
||||
: null;
|
||||
final pointDeVente = commandeur?.pointDeVenteId != null
|
||||
? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!)
|
||||
: null;
|
||||
|
||||
final pdf = pw.Document();
|
||||
final imageBytes = await loadImage();
|
||||
final image = pw.MemoryImage(imageBytes);
|
||||
|
||||
pdf.addPage(
|
||||
pw.Page(
|
||||
pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity),
|
||||
margin: const pw.EdgeInsets.all(4),
|
||||
build: (pw.Context context) {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
// En-tête
|
||||
pw.Center(
|
||||
child: pw.Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: pw.Image(image),
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 4),
|
||||
pw.Text('TICKET DE CAISSE',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
),
|
||||
),
|
||||
pw.Text('N°: ${commande.id}',
|
||||
style: const pw.TextStyle(fontSize: 8)),
|
||||
pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}',
|
||||
style: const pw.TextStyle(fontSize: 8)),
|
||||
|
||||
if (pointDeVente != null)
|
||||
pw.Text('Point de vente: ${pointDeVente['designation']}',
|
||||
style: const pw.TextStyle(fontSize: 8)),
|
||||
|
||||
pw.Divider(thickness: 0.5),
|
||||
|
||||
// Client
|
||||
pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}',
|
||||
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)),
|
||||
|
||||
// Personnel
|
||||
if (commandeur != null)
|
||||
pw.Text('Commandeur: ${commandeur.name} ',
|
||||
style: const pw.TextStyle(fontSize: 7)),
|
||||
if (validateur != null)
|
||||
pw.Text('Validateur: ${validateur.name}',
|
||||
style: const pw.TextStyle(fontSize: 7)),
|
||||
|
||||
pw.Divider(thickness: 0.5),
|
||||
|
||||
// Détails
|
||||
pw.Table(
|
||||
columnWidths: {
|
||||
0: const pw.FlexColumnWidth(3),
|
||||
1: const pw.FlexColumnWidth(1),
|
||||
2: const pw.FlexColumnWidth(2),
|
||||
},
|
||||
children: [
|
||||
pw.TableRow(
|
||||
children: [
|
||||
pw.Text('Produit', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
|
||||
pw.Text('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
|
||||
pw.Text('Total', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
...details.map((detail) => pw.TableRow(
|
||||
children: [
|
||||
pw.Text(detail.produitNom ?? 'Produit', style: const pw.TextStyle(fontSize: 7)),
|
||||
pw.Text(detail.quantite.toString(), style: const pw.TextStyle(fontSize: 7)),
|
||||
pw.Text('${detail.sousTotal.toStringAsFixed(2)} MGA', style: const pw.TextStyle(fontSize: 7)),
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
|
||||
pw.Divider(thickness: 0.5),
|
||||
|
||||
// Total
|
||||
pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
pw.Text('TOTAL:', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)),
|
||||
pw.Text('${commande.montantTotal.toStringAsFixed(2)} MGA',
|
||||
style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
|
||||
// Paiement
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Text('MODE DE PAIEMENT:', style: const pw.TextStyle(fontSize: 8)),
|
||||
pw.Text(
|
||||
payment.type == PaymentType.cash
|
||||
? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(2)} MGA)'
|
||||
: 'CARTE BANCAIRE',
|
||||
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
|
||||
if (payment.type == PaymentType.cash && payment.amountGiven > commande.montantTotal)
|
||||
pw.Text('Monnaie rendue: ${(payment.amountGiven - commande.montantTotal).toStringAsFixed(2)} MGA',
|
||||
style: const pw.TextStyle(fontSize: 8)),
|
||||
|
||||
pw.SizedBox(height: 12),
|
||||
pw.Text('Merci pour votre achat !',
|
||||
style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)),
|
||||
pw.Text('www.guycom.mg',
|
||||
style: const pw.TextStyle(fontSize: 7)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final output = await getTemporaryDirectory();
|
||||
final file = File('${output.path}/ticket_${commande.id}.pdf');
|
||||
await file.writeAsBytes(await pdf.save());
|
||||
await OpenFile.open(file.path);
|
||||
// Récupérer les détails complets des produits
|
||||
final List<Map<String, dynamic>> detailsAvecProduits = [];
|
||||
for (final detail in details) {
|
||||
final produit = await _database.getProductById(detail.produitId);
|
||||
detailsAvecProduits.add({
|
||||
'detail': detail,
|
||||
'produit': produit,
|
||||
});
|
||||
}
|
||||
|
||||
final pdf = pw.Document();
|
||||
final imageBytes = await loadImage();
|
||||
final image = pw.MemoryImage(imageBytes);
|
||||
|
||||
pdf.addPage(
|
||||
pw.Page(
|
||||
pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity),
|
||||
margin: const pw.EdgeInsets.all(4),
|
||||
build: (pw.Context context) {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||
children: [
|
||||
// En-tête avec logo
|
||||
pw.Center(
|
||||
child: pw.Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: pw.Image(image),
|
||||
),
|
||||
),
|
||||
pw.SizedBox(height: 4),
|
||||
|
||||
// Informations de l'entreprise
|
||||
pw.Text('GUYCOM MADAGASCAR',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
)),
|
||||
pw.Text('Tél: 033 37 808 18', style: const pw.TextStyle(fontSize: 7)),
|
||||
pw.Text('www.guycom.mg', style: const pw.TextStyle(fontSize: 7)),
|
||||
|
||||
pw.SizedBox(height: 6),
|
||||
|
||||
// Titre et numéro de ticket
|
||||
pw.Text('TICKET DE CAISSE',
|
||||
style: pw.TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: pw.FontWeight.bold,
|
||||
decoration: pw.TextDecoration.underline,
|
||||
)),
|
||||
pw.Text('N°: ${pointDeVente?['abreviation'] ?? 'PV'}-${commande.id}',
|
||||
style: const pw.TextStyle(fontSize: 8)),
|
||||
pw.Text('Date: ${DateFormat('dd/MM/yyyy HH:mm').format(commande.dateCommande)}',
|
||||
style: const pw.TextStyle(fontSize: 8)),
|
||||
|
||||
if (pointDeVente != null)
|
||||
pw.Text('Point de vente: ${pointDeVente['designation']}',
|
||||
style: const pw.TextStyle(fontSize: 8)),
|
||||
|
||||
pw.Divider(thickness: 0.5),
|
||||
|
||||
// Informations client
|
||||
pw.Text('CLIENT: ${client?.nomComplet ?? 'Non spécifié'}',
|
||||
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold)),
|
||||
if (client?.telephone != null)
|
||||
pw.Text('Tél: ${client!.telephone}', style: const pw.TextStyle(fontSize: 7)),
|
||||
|
||||
// Personnel impliqué
|
||||
if (commandeur != null || validateur != null)
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Divider(thickness: 0.5),
|
||||
if (commandeur != null)
|
||||
pw.Text('Vendeur: ${commandeur.name}', style: const pw.TextStyle(fontSize: 7)),
|
||||
if (validateur != null)
|
||||
pw.Text('Validateur: ${validateur.name}', style: const pw.TextStyle(fontSize: 7)),
|
||||
],
|
||||
),
|
||||
|
||||
pw.Divider(thickness: 0.5),
|
||||
|
||||
// Détails des produits
|
||||
pw.Table(
|
||||
columnWidths: {
|
||||
0: const pw.FlexColumnWidth(3.5),
|
||||
1: const pw.FlexColumnWidth(1),
|
||||
2: const pw.FlexColumnWidth(1.5),
|
||||
},
|
||||
children: [
|
||||
// En-tête du tableau
|
||||
pw.TableRow(
|
||||
children: [
|
||||
pw.Text('Désignation', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
|
||||
pw.Text('Qté', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
|
||||
pw.Text('P.U', style: pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold)),
|
||||
],
|
||||
decoration: const pw.BoxDecoration(
|
||||
border: pw.Border(bottom: pw.BorderSide(width: 0.5)),
|
||||
|
||||
),),
|
||||
|
||||
// Lignes des produits
|
||||
...detailsAvecProduits.map( (item) {
|
||||
final detail = item['detail'] as DetailCommande;
|
||||
final produit = item['produit'];
|
||||
|
||||
return pw.TableRow(
|
||||
decoration: const pw.BoxDecoration(
|
||||
border: pw.Border(bottom: pw.BorderSide(width: 0.2))),
|
||||
children: [
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text(detail.produitNom ?? 'Produit',
|
||||
style: const pw.TextStyle(fontSize: 7)),
|
||||
if (produit?.reference != null)
|
||||
pw.Text('Ref: ${produit!.reference}',
|
||||
style: const pw.TextStyle(fontSize: 6)),
|
||||
if (produit?.imei != null)
|
||||
pw.Text('IMEI: ${produit!.imei}',
|
||||
style: const pw.TextStyle(fontSize: 6)),
|
||||
],
|
||||
),
|
||||
pw.Text(detail.quantite.toString(),
|
||||
style: const pw.TextStyle(fontSize: 7)),
|
||||
pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}',
|
||||
style: const pw.TextStyle(fontSize: 7)),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
|
||||
pw.Divider(thickness: 0.5),
|
||||
|
||||
// Total et paiement
|
||||
pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
pw.Text('TOTAL:',
|
||||
style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)),
|
||||
pw.Text('${commande.montantTotal.toStringAsFixed(0)} MGA',
|
||||
style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold)),
|
||||
],
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 6),
|
||||
|
||||
// Détails du paiement
|
||||
pw.Text('MODE DE PAIEMENT:',
|
||||
style: const pw.TextStyle(fontSize: 8)),
|
||||
pw.Text(
|
||||
payment.type == PaymentType.cash
|
||||
? 'LIQUIDE (${payment.amountGiven.toStringAsFixed(0)} MGA)'
|
||||
: 'CARTE BANCAIRE',
|
||||
style: pw.TextStyle(fontSize: 8, fontWeight: pw.FontWeight.bold),
|
||||
),
|
||||
|
||||
if (payment.type == PaymentType.cash && payment.amountGiven > commande.montantTotal)
|
||||
pw.Text('Monnaie rendue: ${(payment.amountGiven - commande.montantTotal).toStringAsFixed(0)} MGA',
|
||||
style: const pw.TextStyle(fontSize: 8)),
|
||||
|
||||
pw.SizedBox(height: 12),
|
||||
|
||||
// Mentions légales et remerciements
|
||||
pw.Text('Article non échangeable - Garantie selon conditions',
|
||||
style: const pw.TextStyle(fontSize: 6)),
|
||||
pw.Text('Ticket à conserver comme justificatif',
|
||||
style: const pw.TextStyle(fontSize: 6)),
|
||||
pw.SizedBox(height: 8),
|
||||
pw.Text('Merci pour votre confiance !',
|
||||
style: pw.TextStyle(fontSize: 8, fontStyle: pw.FontStyle.italic)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final output = await getTemporaryDirectory();
|
||||
final file = File('${output.path}/ticket_${commande.id}.pdf');
|
||||
await file.writeAsBytes(await pdf.save());
|
||||
await OpenFile.open(file.path);
|
||||
}
|
||||
|
||||
pw.Widget _buildTableCell(String text, [pw.TextStyle? style]) {
|
||||
return pw.Padding(
|
||||
padding: const pw.EdgeInsets.all(4.0),
|
||||
@ -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),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,6 @@ import 'package:youmazgestion/accueil.dart';
|
||||
import '../Models/users.dart';
|
||||
import '../controller/userController.dart';
|
||||
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@ -126,8 +125,8 @@ class _LoginPageState extends State<LoginPage> {
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MainLayout()),
|
||||
);
|
||||
}else{
|
||||
Navigator.pushReplacement(
|
||||
} else {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => DashboardPage()),
|
||||
);
|
||||
@ -216,88 +215,124 @@ class _LoginPageState extends State<LoginPage> {
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: const Icon(
|
||||
Icons.lock_outline,
|
||||
size: 100.0,
|
||||
color: Color.fromARGB(255, 4, 54, 95),
|
||||
),
|
||||
),
|
||||
TextField(
|
||||
controller: _usernameController,
|
||||
enabled: !_isLoading,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Username',
|
||||
prefixIcon: const Icon(Icons.person, color: Colors.blueAccent),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
enabled: !_isLoading,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password',
|
||||
prefixIcon: const Icon(Icons.lock, color: Colors.redAccent),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
const SizedBox(height: 24),
|
||||
TextField(
|
||||
controller: _usernameController,
|
||||
enabled: !_isLoading,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Nom d\'utilisateur',
|
||||
labelStyle: TextStyle(
|
||||
color: primaryColor.withOpacity(0.7),
|
||||
),
|
||||
prefixIcon: Icon(Icons.person, color: accentColor),
|
||||
filled: true,
|
||||
fillColor: accentColor.withOpacity(0.045),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
borderSide: BorderSide(color: accentColor, width: 2),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
borderSide: BorderSide(color: accentColor, width: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
obscureText: true,
|
||||
onSubmitted: (_) => _login(),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Visibility(
|
||||
visible: _isErrorVisible,
|
||||
child: Text(
|
||||
_errorMessage,
|
||||
style: const TextStyle(
|
||||
color: Colors.red,
|
||||
fontSize: 14,
|
||||
const SizedBox(height: 18.0),
|
||||
TextField(
|
||||
controller: _passwordController,
|
||||
enabled: !_isLoading,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Mot de passe',
|
||||
labelStyle: TextStyle(
|
||||
color: primaryColor.withOpacity(0.7),
|
||||
),
|
||||
prefixIcon: Icon(Icons.lock, color: accentColor),
|
||||
filled: true,
|
||||
fillColor: accentColor.withOpacity(0.045),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
borderSide: BorderSide(color: accentColor, width: 2),
|
||||
),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
onSubmitted: (_) => _login(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _login,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF0015B7),
|
||||
elevation: 5.0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
if (_isErrorVisible) ...[
|
||||
const SizedBox(height: 12.0),
|
||||
Text(
|
||||
_errorMessage,
|
||||
style: const TextStyle(
|
||||
color: Colors.redAccent,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2,
|
||||
],
|
||||
const SizedBox(height: 26.0),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _login,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: accentColor,
|
||||
disabledBackgroundColor: accentColor.withOpacity(0.3),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 7.0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
),
|
||||
minimumSize: const Size(double.infinity, 52),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 2.5,
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Se connecter',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: .4,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Text(
|
||||
'Se connecter',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
// 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'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -26,9 +26,18 @@ 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 = [];
|
||||
@ -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> {
|
||||
});
|
||||
}
|
||||
|
||||
// NOUVELLE MÉTHODE - Filtrer les produits
|
||||
void _filterProducts() {
|
||||
final nameQuery = _searchNameController.text.toLowerCase();
|
||||
final imeiQuery = _searchImeiController.text.toLowerCase();
|
||||
final referenceQuery = _searchReferenceController.text.toLowerCase();
|
||||
|
||||
setState(() {
|
||||
_filteredProducts.clear();
|
||||
|
||||
for (var product in _products) {
|
||||
bool matchesName = nameQuery.isEmpty ||
|
||||
product.name.toLowerCase().contains(nameQuery);
|
||||
|
||||
bool matchesImei = imeiQuery.isEmpty ||
|
||||
(product.imei?.toLowerCase().contains(imeiQuery) ?? false);
|
||||
|
||||
bool matchesReference = referenceQuery.isEmpty ||
|
||||
(product.reference?.toLowerCase().contains(referenceQuery) ?? false);
|
||||
|
||||
bool matchesStock = !_showOnlyInStock ||
|
||||
(product.stock != null && product.stock! > 0);
|
||||
|
||||
if (matchesName && matchesImei && matchesReference && matchesStock) {
|
||||
_filteredProducts.add(product);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// NOUVELLE MÉTHODE - Toggle filtre stock
|
||||
void _toggleStockFilter() {
|
||||
setState(() {
|
||||
_showOnlyInStock = !_showOnlyInStock;
|
||||
});
|
||||
_filterProducts();
|
||||
}
|
||||
|
||||
// NOUVELLE MÉTHODE - Réinitialiser les filtres
|
||||
void _clearFilters() {
|
||||
setState(() {
|
||||
_searchNameController.clear();
|
||||
_searchImeiController.clear();
|
||||
_searchReferenceController.clear();
|
||||
_showOnlyInStock = false;
|
||||
});
|
||||
_filterProducts();
|
||||
}
|
||||
|
||||
// NOUVEAU WIDGET - Section des filtres
|
||||
Widget _buildFilterSection() {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.filter_list, color: Colors.blue.shade700),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Filtres de recherche',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color.fromARGB(255, 9, 56, 95),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
TextButton.icon(
|
||||
onPressed: _clearFilters,
|
||||
icon: const Icon(Icons.clear, size: 18),
|
||||
label: const Text('Réinitialiser'),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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: [
|
||||
Expanded(
|
||||
child: 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(width: 12),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _searchReferenceController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Référence',
|
||||
prefixIcon: const Icon(Icons.qr_code),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Bouton filtre stock et résultats
|
||||
Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _toggleStockFilter,
|
||||
icon: Icon(
|
||||
_showOnlyInStock ? Icons.inventory : Icons.inventory_2,
|
||||
size: 20,
|
||||
),
|
||||
label: Text(_showOnlyInStock
|
||||
? 'Afficher tous'
|
||||
: 'Stock disponible'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _showOnlyInStock
|
||||
? Colors.green.shade600
|
||||
: Colors.blue.shade600,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'${_filteredProducts.length} produit(s)',
|
||||
style: TextStyle(
|
||||
color: Colors.blue.shade700,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
floatingActionButton: _buildFloatingCartButton(),
|
||||
appBar: CustomAppBar(title: 'Nouvelle Commande'),
|
||||
drawer: CustomDrawer(),
|
||||
appBar: CustomAppBar(title: 'Faire un commande'),
|
||||
drawer: CustomDrawer(),
|
||||
body: Column(
|
||||
children: [
|
||||
// Header
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [Colors.blue.shade800, Colors.blue.shade600],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.shopping_cart,
|
||||
color: Colors.blue,
|
||||
size: 30,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Nouvelle Commande',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Créez une nouvelle commande pour un client',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 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,54 +319,72 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
void _showClientFormDialog() {
|
||||
Get.dialog(
|
||||
AlertDialog(
|
||||
title: const Text('Informations Client'),
|
||||
content: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildTextFormField(
|
||||
controller: _nomController,
|
||||
label: 'Nom',
|
||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextFormField(
|
||||
controller: _prenomController,
|
||||
label: 'Prénom',
|
||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextFormField(
|
||||
controller: _emailController,
|
||||
label: 'Email',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
|
||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
|
||||
return 'Email invalide';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextFormField(
|
||||
controller: _telephoneController,
|
||||
label: 'Téléphone',
|
||||
keyboardType: TextInputType.phone,
|
||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextFormField(
|
||||
controller: _adresseController,
|
||||
label: 'Adresse',
|
||||
maxLines: 2,
|
||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildCommercialDropdown(),
|
||||
],
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(Icons.person_add, color: Colors.blue.shade700),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Informations Client'),
|
||||
],
|
||||
),
|
||||
content: Container(
|
||||
width: 600,
|
||||
constraints: const BoxConstraints(maxHeight: 600),
|
||||
child: SingleChildScrollView(
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTextFormField(
|
||||
controller: _nomController,
|
||||
label: 'Nom',
|
||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextFormField(
|
||||
controller: _prenomController,
|
||||
label: 'Prénom',
|
||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextFormField(
|
||||
controller: _emailController,
|
||||
label: 'Email',
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) {
|
||||
if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
|
||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
|
||||
return 'Email invalide';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextFormField(
|
||||
controller: _telephoneController,
|
||||
label: 'Téléphone',
|
||||
keyboardType: TextInputType.phone,
|
||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildTextFormField(
|
||||
controller: _adresseController,
|
||||
label: 'Adresse',
|
||||
maxLines: 2,
|
||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildCommercialDropdown(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -231,20 +397,15 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue.shade800,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
),
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Succès',
|
||||
'Informations client enregistrées',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
_submitOrder();
|
||||
}
|
||||
},
|
||||
child: const Text('Enregistrer'),
|
||||
child: const Text('Valider la commande'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -306,6 +467,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
);
|
||||
}
|
||||
|
||||
// WIDGET MODIFIÉ - Liste des produits (utilise maintenant _filteredProducts)
|
||||
Widget _buildProductList() {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
@ -326,14 +488,14 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_products.isEmpty
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
_filteredProducts.isEmpty
|
||||
? _buildEmptyState()
|
||||
: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: _products.length,
|
||||
itemCount: _filteredProducts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final product = _products[index];
|
||||
final product = _filteredProducts[index];
|
||||
final quantity = _quantites[product.id] ?? 0;
|
||||
|
||||
return _buildProductListItem(product, quantity);
|
||||
@ -345,94 +507,171 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
);
|
||||
}
|
||||
|
||||
// NOUVEAU WIDGET - État vide
|
||||
Widget _buildEmptyState() {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search_off,
|
||||
size: 64,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Aucun produit trouvé',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Essayez de modifier vos critères de recherche',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// WIDGET MODIFIÉ - Item de produit (ajout d'informations IMEI/Référence)
|
||||
Widget _buildProductListItem(Product product, int quantity) {
|
||||
final bool isOutOfStock = product.stock != null && product.stock! <= 0;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: isOutOfStock
|
||||
? Border.all(color: Colors.red.shade200, width: 1.5)
|
||||
: null,
|
||||
),
|
||||
leading: Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: const Icon(Icons.shopping_bag, color: Colors.blue),
|
||||
),
|
||||
title: Text(
|
||||
product.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${product.price.toStringAsFixed(2)} DA',
|
||||
style: TextStyle(
|
||||
color: Colors.green.shade700,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
leading: Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: isOutOfStock
|
||||
? Colors.red.shade50
|
||||
: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
if (product.stock != null)
|
||||
child: Icon(
|
||||
Icons.shopping_bag,
|
||||
color: isOutOfStock ? Colors.red : Colors.blue
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
product.name,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isOutOfStock ? Colors.red.shade700 : null,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Stock: ${product.stock}',
|
||||
'${product.price.toStringAsFixed(2)} MGA',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
color: Colors.green.shade700,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove, size: 18),
|
||||
onPressed: () {
|
||||
if (quantity > 0) {
|
||||
setState(() {
|
||||
_quantites[product.id!] = quantity - 1;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
Text(
|
||||
quantity.toString(),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
onPressed: () {
|
||||
if (product.stock == null || quantity < product.stock!) {
|
||||
setState(() {
|
||||
_quantites[product.id!] = quantity + 1;
|
||||
});
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Stock insuffisant',
|
||||
'Quantité demandée non disponible',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (product.stock != null)
|
||||
Text(
|
||||
'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isOutOfStock
|
||||
? Colors.red.shade600
|
||||
: Colors.grey.shade600,
|
||||
fontWeight: isOutOfStock ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
// Affichage IMEI et Référence
|
||||
if (product.imei != null && product.imei!.isNotEmpty)
|
||||
Text(
|
||||
'IMEI: ${product.imei}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade600,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
if (product.reference != null && product.reference!.isNotEmpty)
|
||||
Text(
|
||||
'Réf: ${product.reference}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isOutOfStock
|
||||
? Colors.grey.shade100
|
||||
: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove, size: 18),
|
||||
onPressed: isOutOfStock ? null : () {
|
||||
if (quantity > 0) {
|
||||
setState(() {
|
||||
_quantites[product.id!] = quantity - 1;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
Text(
|
||||
quantity.toString(),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
onPressed: isOutOfStock ? null : () {
|
||||
if (product.stock == null || quantity < product.stock!) {
|
||||
setState(() {
|
||||
_quantites[product.id!] = quantity + 1;
|
||||
});
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Stock insuffisant',
|
||||
'Quantité demandée non disponible',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -537,9 +776,9 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
child: const Icon(Icons.shopping_bag, size: 20),
|
||||
),
|
||||
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,23 +858,7 @@ 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
|
||||
Get.snackbar(
|
||||
'Informations manquantes',
|
||||
'Veuillez remplir les informations client',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
_showClientFormDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier d'abord si le panier est vide
|
||||
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList();
|
||||
if (itemsInCart.isEmpty) {
|
||||
Get.snackbar(
|
||||
@ -645,6 +868,24 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
_showCartBottomSheet(); // Ouvrir le panier pour montrer qu'il est vide
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensuite vérifier les informations client
|
||||
if (_nomController.text.isEmpty ||
|
||||
_prenomController.text.isEmpty ||
|
||||
_emailController.text.isEmpty ||
|
||||
_telephoneController.text.isEmpty ||
|
||||
_adresseController.text.isEmpty) {
|
||||
Get.snackbar(
|
||||
'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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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']),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -17,6 +17,7 @@ void main() async {
|
||||
|
||||
// await ProductDatabase.instance.initDatabase();
|
||||
await AppDatabase.instance.initDatabase();
|
||||
|
||||
|
||||
// Afficher les informations de la base (pour debug)
|
||||
// await AppDatabase.instance.printDatabaseInfo();
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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…
Reference in New Issue
Block a user