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),
|
leading: const Icon(Icons.logout, color: Colors.red),
|
||||||
title: const Text("Déconnexion"),
|
title: const Text("Déconnexion"),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Get.defaultDialog(
|
Get.dialog(
|
||||||
title: "Déconnexion",
|
AlertDialog(
|
||||||
content: const Text("Voulez-vous vraiment vous déconnecter ?"),
|
shape: RoundedRectangleBorder(
|
||||||
actions: [
|
borderRadius: BorderRadius.circular(16),
|
||||||
TextButton(
|
),
|
||||||
child: const Text("Non"),
|
contentPadding: EdgeInsets.zero,
|
||||||
onPressed: () => Get.back(),
|
content: Container(
|
||||||
),
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
ElevatedButton(
|
child: Column(
|
||||||
style: ElevatedButton.styleFrom(
|
mainAxisSize: MainAxisSize.min,
|
||||||
backgroundColor: Colors.red,
|
children: [
|
||||||
|
// Header
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.logout_rounded,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.orange.shade600,
|
||||||
),
|
),
|
||||||
child: const Text("Oui"),
|
const SizedBox(height: 16),
|
||||||
onPressed: () async {
|
const Text(
|
||||||
await clearUserData();
|
"Déconnexion",
|
||||||
Get.offAll(const LoginPage());
|
style: TextStyle(
|
||||||
},
|
fontSize: 20,
|
||||||
),
|
fontWeight: FontWeight.w600,
|
||||||
],
|
color: Colors.black87,
|
||||||
);
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
"Êtes-vous sûr de vouloir vous déconnecter ?",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.black87,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
"Vous devrez vous reconnecter pour accéder à votre compte.",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Actions
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
side: BorderSide(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
width: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
"Annuler",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await clearUserData();
|
||||||
|
Get.offAll(const LoginPage());
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red.shade600,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 2,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
"Se déconnecter",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
barrierDismissible: true,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -8,6 +8,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
final List<Widget>? actions;
|
final List<Widget>? actions;
|
||||||
final bool automaticallyImplyLeading;
|
final bool automaticallyImplyLeading;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
|
final bool isDesktop; // Add this parameter
|
||||||
|
|
||||||
final UserController userController = Get.put(UserController());
|
final UserController userController = Get.put(UserController());
|
||||||
|
|
||||||
@ -18,6 +19,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
this.actions,
|
this.actions,
|
||||||
this.automaticallyImplyLeading = true,
|
this.automaticallyImplyLeading = true,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
|
this.isDesktop = false, // Add this parameter with default value
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -78,7 +80,9 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Obx(() => Text(
|
Obx(() => Text(
|
||||||
userController.role!='Super Admin'?'Point de vente: ${userController.pointDeVenteDesignation}':'',
|
userController.role != 'Super Admin'
|
||||||
|
? 'Point de vente: ${userController.pointDeVenteDesignation}'
|
||||||
|
: '',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
|
|||||||
@ -52,9 +52,6 @@ class Client {
|
|||||||
enum StatutCommande {
|
enum StatutCommande {
|
||||||
enAttente,
|
enAttente,
|
||||||
confirmee,
|
confirmee,
|
||||||
enPreparation,
|
|
||||||
expediee,
|
|
||||||
livree,
|
|
||||||
annulee
|
annulee
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,12 +125,12 @@ class Commande {
|
|||||||
return 'En attente';
|
return 'En attente';
|
||||||
case StatutCommande.confirmee:
|
case StatutCommande.confirmee:
|
||||||
return 'Confirmée';
|
return 'Confirmée';
|
||||||
case StatutCommande.enPreparation:
|
// case StatutCommande.enPreparation:
|
||||||
return 'En préparation';
|
// return 'En préparation';
|
||||||
case StatutCommande.expediee:
|
// case StatutCommande.expediee:
|
||||||
return 'Expédiée';
|
// return 'Expédiée';
|
||||||
case StatutCommande.livree:
|
// case StatutCommande.livree:
|
||||||
return 'Livrée';
|
// return 'Livrée';
|
||||||
case StatutCommande.annulee:
|
case StatutCommande.annulee:
|
||||||
return 'Annulée';
|
return 'Annulée';
|
||||||
default:
|
default:
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
class Product {
|
class Product {
|
||||||
int? id;
|
final int? id;
|
||||||
final String name;
|
final String name;
|
||||||
final double price;
|
final double price;
|
||||||
final String? image;
|
final String? image;
|
||||||
final String category;
|
final String category;
|
||||||
final int? stock;
|
final int stock;
|
||||||
final String? description;
|
final String? description;
|
||||||
String? qrCode;
|
String? qrCode;
|
||||||
final String? reference;
|
final String? reference;
|
||||||
final int? pointDeVenteId;
|
final int? pointDeVenteId;
|
||||||
|
final String? marque;
|
||||||
|
final String? ram;
|
||||||
|
final String? memoireInterne;
|
||||||
|
final String? imei;
|
||||||
|
|
||||||
Product({
|
Product({
|
||||||
this.id,
|
this.id,
|
||||||
@ -17,12 +21,16 @@ class Product {
|
|||||||
this.image,
|
this.image,
|
||||||
required this.category,
|
required this.category,
|
||||||
this.stock = 0,
|
this.stock = 0,
|
||||||
this.description = '',
|
this.description,
|
||||||
this.qrCode,
|
this.qrCode,
|
||||||
this.reference,
|
this.reference,
|
||||||
this.pointDeVenteId
|
this.pointDeVenteId,
|
||||||
|
this.marque,
|
||||||
|
this.ram,
|
||||||
|
this.memoireInterne,
|
||||||
|
this.imei,
|
||||||
|
|
||||||
});
|
});
|
||||||
// Vérifie si le stock est défini
|
|
||||||
bool isStockDefined() {
|
bool isStockDefined() {
|
||||||
if (stock != null) {
|
if (stock != null) {
|
||||||
print("stock is defined : $stock $name");
|
print("stock is defined : $stock $name");
|
||||||
@ -31,33 +39,37 @@ class Product {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Map<String, dynamic> toMap() {
|
factory Product.fromMap(Map<String, dynamic> map) => Product(
|
||||||
return {
|
id: map['id'],
|
||||||
'id': id,
|
name: map['name'],
|
||||||
'name': name,
|
price: map['price'],
|
||||||
'price': price,
|
image: map['image'],
|
||||||
'image': image ?? '',
|
category: map['category'],
|
||||||
'category': category,
|
stock: map['stock'],
|
||||||
'stock': stock ?? 0,
|
description: map['description'],
|
||||||
'description': description ?? '',
|
qrCode: map['qrCode'],
|
||||||
'qrCode': qrCode ?? '',
|
reference: map['reference'],
|
||||||
'reference': reference ?? '',
|
pointDeVenteId: map['point_de_vente_id'],
|
||||||
'point_de_vente_id':pointDeVenteId
|
marque: map['marque'],
|
||||||
};
|
ram: map['ram'],
|
||||||
}
|
memoireInterne: map['memoire_interne'],
|
||||||
|
imei: map['imei'],
|
||||||
|
);
|
||||||
|
|
||||||
factory Product.fromMap(Map<String, dynamic> map) {
|
Map<String, dynamic> toMap() => {
|
||||||
return Product(
|
'id': id,
|
||||||
id: map['id'],
|
'name': name,
|
||||||
name: map['name'],
|
'price': price,
|
||||||
price: map['price'],
|
'image': image,
|
||||||
image: map['image'],
|
'category': category,
|
||||||
category: map['category'],
|
'stock': stock,
|
||||||
stock: map['stock'],
|
'description': description,
|
||||||
description: map['description'],
|
'qrCode': qrCode,
|
||||||
qrCode: map['qrCode'],
|
'reference': reference,
|
||||||
reference: map['reference'],
|
'point_de_vente_id': pointDeVenteId,
|
||||||
pointDeVenteId : map['point_de_vente_id']
|
'marque': marque,
|
||||||
);
|
'ram': ram,
|
||||||
}
|
'memoire_interne': memoireInterne,
|
||||||
|
'imei': imei,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@ -37,8 +37,8 @@ class AppDatabase {
|
|||||||
await insertDefaultMenus();
|
await insertDefaultMenus();
|
||||||
await insertDefaultRoles();
|
await insertDefaultRoles();
|
||||||
await insertDefaultSuperAdmin();
|
await insertDefaultSuperAdmin();
|
||||||
await _insertDefaultClients();
|
// await _insertDefaultClients();
|
||||||
await _insertDefaultCommandes();
|
// await _insertDefaultCommandes();
|
||||||
await insertDefaultPointsDeVente(); // Ajouté ici
|
await insertDefaultPointsDeVente(); // Ajouté ici
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,12 +110,19 @@ class AppDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- POINTS DE VENTE ---
|
// --- POINTS DE VENTE ---
|
||||||
if (!tableNames.contains('points_de_vente')) {
|
if (!tableNames.contains('points_de_vente')) {
|
||||||
await db.execute('''CREATE TABLE points_de_vente (
|
await db.execute('''CREATE TABLE points_de_vente (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
designation TEXT NOT NULL UNIQUE
|
nom TEXT NOT NULL UNIQUE
|
||||||
)''');
|
)''');
|
||||||
}
|
} else {
|
||||||
|
// Si la table existe déjà, ajouter la colonne code si elle n'existe pas
|
||||||
|
try {
|
||||||
|
await db.execute('ALTER TABLE points_de_vente ADD COLUMN nom TEXT UNIQUE');
|
||||||
|
} catch (e) {
|
||||||
|
print("La colonne nom existe déjà dans la table points_de_vente");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- UTILISATEURS ---
|
// --- UTILISATEURS ---
|
||||||
if (!tableNames.contains('users')) {
|
if (!tableNames.contains('users')) {
|
||||||
@ -140,29 +147,54 @@ class AppDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- PRODUITS ---
|
// Dans la méthode _createDB, modifier la partie concernant la table products
|
||||||
if (!tableNames.contains('products')) {
|
if (!tableNames.contains('products')) {
|
||||||
await db.execute('''CREATE TABLE products (
|
await db.execute('''CREATE TABLE products (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
price REAL NOT NULL,
|
price REAL NOT NULL,
|
||||||
image TEXT,
|
image TEXT,
|
||||||
category TEXT NOT NULL,
|
category TEXT NOT NULL,
|
||||||
stock INTEGER NOT NULL DEFAULT 0,
|
stock INTEGER NOT NULL DEFAULT 0,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
qrCode TEXT,
|
qrCode TEXT,
|
||||||
reference TEXT UNIQUE,
|
reference TEXT,
|
||||||
point_de_vente_id INTEGER,
|
point_de_vente_id INTEGER,
|
||||||
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id)
|
marque TEXT,
|
||||||
)''');
|
ram TEXT,
|
||||||
} else {
|
memoire_interne TEXT,
|
||||||
// Si la table existe déjà, ajouter la colonne si elle n'existe pas
|
imei TEXT UNIQUE,
|
||||||
|
FOREIGN KEY (point_de_vente_id) REFERENCES points_de_vente(id)
|
||||||
|
)''');
|
||||||
|
} else {
|
||||||
|
// Si la table existe déjà, ajouter les colonnes si elles n'existent pas
|
||||||
|
final columns = await db.rawQuery('PRAGMA table_info(products)');
|
||||||
|
final columnNames = columns.map((col) => col['name'] as String).toList();
|
||||||
|
|
||||||
|
final newColumns = [
|
||||||
|
'marque',
|
||||||
|
'ram',
|
||||||
|
'memoire_interne',
|
||||||
|
'imei'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (var column in newColumns) {
|
||||||
|
if (!columnNames.contains(column)) {
|
||||||
try {
|
try {
|
||||||
await db.execute('ALTER TABLE products ADD COLUMN point_de_vente_id INTEGER REFERENCES points_de_vente(id)');
|
await db.execute('ALTER TABLE products ADD COLUMN $column TEXT');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("La colonne point_de_vente_id existe déjà dans la table products");
|
print("La colonne $column existe déjà dans la table products");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier aussi point_de_vente_id au cas 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 ---
|
// --- CLIENTS ---
|
||||||
if (!tableNames.contains('clients')) {
|
if (!tableNames.contains('clients')) {
|
||||||
@ -301,25 +333,61 @@ class AppDatabase {
|
|||||||
}/* Copier depuis ton code */ }
|
}/* Copier depuis ton code */ }
|
||||||
|
|
||||||
|
|
||||||
Future<void> insertDefaultPointsDeVente() async {
|
Future<void> insertDefaultPointsDeVente() async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
final existing = await db.query('points_de_vente');
|
final existing = await db.query('points_de_vente');
|
||||||
|
|
||||||
if (existing.isEmpty) {
|
if (existing.isEmpty) {
|
||||||
final defaultPoints = [
|
final defaultPoints = [
|
||||||
{'designation': 'Behoririka'},
|
{'nom': '405A'},
|
||||||
{'designation': 'Antanimena'},
|
{'nom': '405B'},
|
||||||
{'designation': 'Analakely'},
|
{'nom': '416'},
|
||||||
{'designation': 'Andravoahangy'},
|
{'nom': 'S405A'},
|
||||||
{'designation': 'Anosy'},
|
{'nom': '417'},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (var point in defaultPoints) {
|
for (var point in defaultPoints) {
|
||||||
await db.insert('points_de_vente', point);
|
try {
|
||||||
|
await db.insert(
|
||||||
|
'points_de_vente',
|
||||||
|
point,
|
||||||
|
conflictAlgorithm: ConflictAlgorithm.ignore
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print("Erreur insertion point de vente ${point['nom']}: $e");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
print("Points de vente par défaut insérés");
|
print("Points de vente par défaut insérés");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Future<void> debugPointsDeVenteTable() async {
|
||||||
|
final db = await database;
|
||||||
|
try {
|
||||||
|
// Vérifie si la table existe
|
||||||
|
final tables = await db.rawQuery(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name='points_de_vente'"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tables.isEmpty) {
|
||||||
|
print("La table points_de_vente n'existe pas!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compte le nombre d'entrées
|
||||||
|
final count = await db.rawQuery("SELECT COUNT(*) as count FROM points_de_vente");
|
||||||
|
print("Nombre de points de vente: ${count.first['count']}");
|
||||||
|
|
||||||
|
// Affiche le contenu
|
||||||
|
final content = await db.query('points_de_vente');
|
||||||
|
print("Contenu de la table points_de_vente:");
|
||||||
|
for (var row in content) {
|
||||||
|
print("ID: ${row['id']}, Nom: ${row['nom']}");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Erreur debug table points_de_vente: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> insertDefaultSuperAdmin() async { final db = await database;
|
Future<void> insertDefaultSuperAdmin() async { final db = await database;
|
||||||
|
|
||||||
final existingSuperAdmin = await db.rawQuery('''
|
final existingSuperAdmin = await db.rawQuery('''
|
||||||
@ -557,13 +625,17 @@ Future<Users?> getUserById(int id) async {
|
|||||||
Future<int> createProduct(Product product) async {
|
Future<int> createProduct(Product product) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
|
|
||||||
// Récupérer le point de vente de l'utilisateur connecté
|
// Si le produit a un point_de_vente_id, on l'utilise directement
|
||||||
|
if (product.pointDeVenteId != null && product.pointDeVenteId! > 0) {
|
||||||
|
return await db.insert('products', product.toMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sinon, on utilise le point de vente de l'utilisateur connecté
|
||||||
final userCtrl = Get.find<UserController>();
|
final userCtrl = Get.find<UserController>();
|
||||||
final currentPointDeVenteId = userCtrl.pointDeVenteId;
|
final currentPointDeVenteId = userCtrl.pointDeVenteId;
|
||||||
|
|
||||||
// Si le produit n’a pas de point_de_vente_id, on lui assigne celui de l'utilisateur connecté
|
|
||||||
final Map<String, dynamic> productData = product.toMap();
|
final Map<String, dynamic> productData = product.toMap();
|
||||||
if (currentPointDeVenteId > 0 && (product.pointDeVenteId == null || product.pointDeVenteId == 0)) {
|
if (currentPointDeVenteId > 0) {
|
||||||
productData['point_de_vente_id'] = currentPointDeVenteId;
|
productData['point_de_vente_id'] = currentPointDeVenteId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -592,6 +664,19 @@ Future<int> updateProduct(Product product) async {
|
|||||||
// where: 'id = ?',
|
// where: 'id = ?',
|
||||||
// whereArgs: [product.id],
|
// whereArgs: [product.id],
|
||||||
// );/* Copier depuis ton code */ }
|
// );/* Copier depuis ton code */ }
|
||||||
|
Future<Product?> getProductById(int id) async {
|
||||||
|
final db = await database;
|
||||||
|
final maps = await db.query(
|
||||||
|
'products',
|
||||||
|
where: 'id = ?',
|
||||||
|
whereArgs: [id],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (maps.isNotEmpty) {
|
||||||
|
return Product.fromMap(maps.first);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
Future<int> deleteProduct(int? id) async { final db = await database;
|
Future<int> deleteProduct(int? id) async { final db = await database;
|
||||||
return await db.delete(
|
return await db.delete(
|
||||||
'products',
|
'products',
|
||||||
@ -739,6 +824,21 @@ Future<int> deleteCommande(int id) async {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Product?> getProductByIMEI(String imei) async {
|
||||||
|
final db = await database;
|
||||||
|
final maps = await db.query(
|
||||||
|
'products',
|
||||||
|
where: 'imei = ?',
|
||||||
|
whereArgs: [imei],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (maps.isNotEmpty) {
|
||||||
|
return Product.fromMap(maps.first);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Détails commandes
|
// Détails commandes
|
||||||
// Créer un détail de commande
|
// Créer un détail de commande
|
||||||
Future<int> createDetailCommande(DetailCommande detail) async {
|
Future<int> createDetailCommande(DetailCommande detail) async {
|
||||||
@ -835,110 +935,110 @@ Future<int> updateStock(int productId, int newStock) async {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Données par défaut
|
// // Données par défaut
|
||||||
Future<void> _insertDefaultClients() async {final db = await database;
|
// Future<void> _insertDefaultClients() async {final db = await database;
|
||||||
final existingClients = await db.query('clients');
|
// final existingClients = await db.query('clients');
|
||||||
|
|
||||||
if (existingClients.isEmpty) {
|
// if (existingClients.isEmpty) {
|
||||||
final defaultClients = [
|
// final defaultClients = [
|
||||||
Client(
|
// Client(
|
||||||
nom: 'Dupont',
|
// nom: 'Dupont',
|
||||||
prenom: 'Jean',
|
// prenom: 'Jean',
|
||||||
email: 'jean.dupont@email.com',
|
// email: 'jean.dupont@email.com',
|
||||||
telephone: '0123456789',
|
// telephone: '0123456789',
|
||||||
adresse: '123 Rue de la Paix, Paris',
|
// adresse: '123 Rue de la Paix, Paris',
|
||||||
dateCreation: DateTime.now(),
|
// dateCreation: DateTime.now(),
|
||||||
),
|
// ),
|
||||||
Client(
|
// Client(
|
||||||
nom: 'Martin',
|
// nom: 'Martin',
|
||||||
prenom: 'Marie',
|
// prenom: 'Marie',
|
||||||
email: 'marie.martin@email.com',
|
// email: 'marie.martin@email.com',
|
||||||
telephone: '0987654321',
|
// telephone: '0987654321',
|
||||||
adresse: '456 Avenue des Champs, Lyon',
|
// adresse: '456 Avenue des Champs, Lyon',
|
||||||
dateCreation: DateTime.now(),
|
// dateCreation: DateTime.now(),
|
||||||
),
|
// ),
|
||||||
Client(
|
// Client(
|
||||||
nom: 'Bernard',
|
// nom: 'Bernard',
|
||||||
prenom: 'Pierre',
|
// prenom: 'Pierre',
|
||||||
email: 'pierre.bernard@email.com',
|
// email: 'pierre.bernard@email.com',
|
||||||
telephone: '0456789123',
|
// telephone: '0456789123',
|
||||||
adresse: '789 Boulevard Saint-Michel, Marseille',
|
// adresse: '789 Boulevard Saint-Michel, Marseille',
|
||||||
dateCreation: DateTime.now(),
|
// dateCreation: DateTime.now(),
|
||||||
),
|
// ),
|
||||||
];
|
// ];
|
||||||
|
|
||||||
for (var client in defaultClients) {
|
// for (var client in defaultClients) {
|
||||||
await db.insert('clients', client.toMap());
|
// await db.insert('clients', client.toMap());
|
||||||
}
|
// }
|
||||||
print("Clients par défaut insérés");
|
// print("Clients par défaut insérés");
|
||||||
} /* Copier depuis ton code */ }
|
// } /* Copier depuis ton code */ }
|
||||||
Future<void> _insertDefaultCommandes() async { final db = await database;
|
// Future<void> _insertDefaultCommandes() async { final db = await database;
|
||||||
final existingCommandes = await db.query('commandes');
|
// final existingCommandes = await db.query('commandes');
|
||||||
|
|
||||||
if (existingCommandes.isEmpty) {
|
// if (existingCommandes.isEmpty) {
|
||||||
// Récupérer quelques produits pour créer des commandes
|
// // Récupérer quelques produits pour créer des commandes
|
||||||
final produits = await db.query('products', limit: 3);
|
// final produits = await db.query('products', limit: 3);
|
||||||
final clients = await db.query('clients', limit: 3);
|
// final clients = await db.query('clients', limit: 3);
|
||||||
|
|
||||||
if (produits.isNotEmpty && clients.isNotEmpty) {
|
// if (produits.isNotEmpty && clients.isNotEmpty) {
|
||||||
// Commande 1
|
// // Commande 1
|
||||||
final commande1Id = await db.insert('commandes', {
|
// final commande1Id = await db.insert('commandes', {
|
||||||
'clientId': clients[0]['id'],
|
// 'clientId': clients[0]['id'],
|
||||||
'dateCommande': DateTime.now().subtract(Duration(days: 5)).toIso8601String(),
|
// 'dateCommande': DateTime.now().subtract(Duration(days: 5)).toIso8601String(),
|
||||||
'statut': StatutCommande.livree.index,
|
// 'statut': StatutCommande.livree.index,
|
||||||
'montantTotal': 150.0,
|
// 'montantTotal': 150.0,
|
||||||
'notes': 'Commande urgente',
|
// 'notes': 'Commande urgente',
|
||||||
});
|
// });
|
||||||
|
|
||||||
await db.insert('details_commandes', {
|
// await db.insert('details_commandes', {
|
||||||
'commandeId': commande1Id,
|
// 'commandeId': commande1Id,
|
||||||
'produitId': produits[0]['id'],
|
// 'produitId': produits[0]['id'],
|
||||||
'quantite': 2,
|
// 'quantite': 2,
|
||||||
'prixUnitaire': 75.0,
|
// 'prixUnitaire': 75.0,
|
||||||
'sousTotal': 150.0,
|
// 'sousTotal': 150.0,
|
||||||
});
|
// });
|
||||||
|
|
||||||
// Commande 2
|
// // Commande 2
|
||||||
final commande2Id = await db.insert('commandes', {
|
// final commande2Id = await db.insert('commandes', {
|
||||||
'clientId': clients[1]['id'],
|
// 'clientId': clients[1]['id'],
|
||||||
'dateCommande': DateTime.now().subtract(Duration(days: 2)).toIso8601String(),
|
// 'dateCommande': DateTime.now().subtract(Duration(days: 2)).toIso8601String(),
|
||||||
'statut': StatutCommande.enPreparation.index,
|
// 'statut': StatutCommande.enPreparation.index,
|
||||||
'montantTotal': 225.0,
|
// 'montantTotal': 225.0,
|
||||||
'notes': 'Livraison prévue demain',
|
// 'notes': 'Livraison prévue demain',
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (produits.length > 1) {
|
// if (produits.length > 1) {
|
||||||
await db.insert('details_commandes', {
|
// await db.insert('details_commandes', {
|
||||||
'commandeId': commande2Id,
|
// 'commandeId': commande2Id,
|
||||||
'produitId': produits[1]['id'],
|
// 'produitId': produits[1]['id'],
|
||||||
'quantite': 3,
|
// 'quantite': 3,
|
||||||
'prixUnitaire': 75.0,
|
// 'prixUnitaire': 75.0,
|
||||||
'sousTotal': 225.0,
|
// 'sousTotal': 225.0,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Commande 3
|
// // Commande 3
|
||||||
final commande3Id = await db.insert('commandes', {
|
// final commande3Id = await db.insert('commandes', {
|
||||||
'clientId': clients[2]['id'],
|
// 'clientId': clients[2]['id'],
|
||||||
'dateCommande': DateTime.now().subtract(Duration(hours: 6)).toIso8601String(),
|
// 'dateCommande': DateTime.now().subtract(Duration(hours: 6)).toIso8601String(),
|
||||||
'statut': StatutCommande.confirmee.index,
|
// 'statut': StatutCommande.confirmee.index,
|
||||||
'montantTotal': 300.0,
|
// 'montantTotal': 300.0,
|
||||||
'notes': 'Commande standard',
|
// 'notes': 'Commande standard',
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (produits.length > 2) {
|
// if (produits.length > 2) {
|
||||||
await db.insert('details_commandes', {
|
// await db.insert('details_commandes', {
|
||||||
'commandeId': commande3Id,
|
// 'commandeId': commande3Id,
|
||||||
'produitId': produits[2]['id'],
|
// 'produitId': produits[2]['id'],
|
||||||
'quantite': 4,
|
// 'quantite': 4,
|
||||||
'prixUnitaire': 75.0,
|
// 'prixUnitaire': 75.0,
|
||||||
'sousTotal': 300.0,
|
// 'sousTotal': 300.0,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
print("Commandes par défaut insérées");
|
// print("Commandes par défaut insérées");
|
||||||
}
|
// }
|
||||||
}/* Copier depuis ton code */ }
|
// }/* Copier depuis ton code */ }
|
||||||
|
|
||||||
// Statistiques
|
// Statistiques
|
||||||
Future<Map<String, dynamic>> getStatistiques() async { final db = await database;
|
Future<Map<String, dynamic>> getStatistiques() async { final db = await database;
|
||||||
@ -1094,25 +1194,46 @@ Future<bool> hasPermission(String username, String permissionName, String menuRo
|
|||||||
print("Base de données product supprimée");
|
print("Base de données product supprimée");
|
||||||
}/* Copier depuis ton code */ }
|
}/* Copier depuis ton code */ }
|
||||||
// CRUD Points de vente
|
// CRUD Points de vente
|
||||||
Future<int> createPointDeVente(String designation) async {
|
// CRUD Points de vente
|
||||||
|
Future<int> createPointDeVente(String designation, String code) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
return await db.insert('points_de_vente', {
|
return await db.insert('points_de_vente', {
|
||||||
'designation': designation
|
'designation': designation,
|
||||||
},
|
'code': code
|
||||||
conflictAlgorithm: ConflictAlgorithm.ignore
|
}, conflictAlgorithm: ConflictAlgorithm.ignore);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>> getPointsDeVente() async {
|
Future<List<Map<String, dynamic>>> getPointsDeVente() async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
return await db.query('points_de_vente', orderBy: 'designation ASC');
|
try {
|
||||||
|
final result = await db.query(
|
||||||
|
'points_de_vente',
|
||||||
|
orderBy: 'nom ASC',
|
||||||
|
where: 'nom IS NOT NULL AND nom != ""' // Filtre les noms vides
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.isEmpty) {
|
||||||
|
print("Aucun point de vente trouvé dans la base de données");
|
||||||
|
// Optionnel: Insérer les points de vente par défaut si table vide
|
||||||
|
await insertDefaultPointsDeVente();
|
||||||
|
return await db.query('points_de_vente', orderBy: 'nom ASC');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
print("Erreur lors de la récupération des points de vente: $e");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> updatePointDeVente(int id, String newDesignation) async {
|
Future<int> updatePointDeVente(int id, String newDesignation, String newCode) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
return await db.update(
|
return await db.update(
|
||||||
'points_de_vente',
|
'points_de_vente',
|
||||||
{'designation': newDesignation},
|
{
|
||||||
|
'designation': newDesignation,
|
||||||
|
'code': newCode
|
||||||
|
},
|
||||||
where: 'id = ?',
|
where: 'id = ?',
|
||||||
whereArgs: [id],
|
whereArgs: [id],
|
||||||
);
|
);
|
||||||
@ -1139,6 +1260,8 @@ Future<Map<String, int>> getProductCountByCategory() async {
|
|||||||
return Map.fromEntries(result.map((e) =>
|
return Map.fromEntries(result.map((e) =>
|
||||||
MapEntry(e['category'] as String, e['count'] as int)));
|
MapEntry(e['category'] as String, e['count'] as int)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>?> getPointDeVenteById(int id) async {
|
Future<Map<String, dynamic>?> getPointDeVenteById(int id) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
final result = await db.query(
|
final result = await db.query(
|
||||||
@ -1148,4 +1271,238 @@ Future<Map<String, dynamic>?> getPointDeVenteById(int id) async {
|
|||||||
);
|
);
|
||||||
return result.isNotEmpty ? result.first : null;
|
return result.isNotEmpty ? result.first : null;
|
||||||
}
|
}
|
||||||
|
Future<int?> getOrCreatePointDeVenteByNom(String nom) async {
|
||||||
|
final db = await database;
|
||||||
|
|
||||||
|
// Vérifier si le point de vente existe déjà
|
||||||
|
final existing = await db.query(
|
||||||
|
'points_de_vente',
|
||||||
|
where: 'nom = ?',
|
||||||
|
whereArgs: [nom.trim()],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing.isNotEmpty) {
|
||||||
|
return existing.first['id'] as int;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer le point de vente s'il n'existe pas
|
||||||
|
try {
|
||||||
|
final id = await db.insert('points_de_vente', {
|
||||||
|
'nom': nom.trim()
|
||||||
|
});
|
||||||
|
print("Point de vente créé: $nom (ID: $id)");
|
||||||
|
return id;
|
||||||
|
} catch (e) {
|
||||||
|
print("Erreur lors de la création du point de vente $nom: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> getPointDeVenteNomById(int id) async {
|
||||||
|
if (id == 0 || id == null) return null;
|
||||||
|
|
||||||
|
final db = await database;
|
||||||
|
try {
|
||||||
|
final result = await db.query(
|
||||||
|
'points_de_vente',
|
||||||
|
where: 'id = ?',
|
||||||
|
whereArgs: [id],
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.isNotEmpty ? result.first['nom'] as String : null;
|
||||||
|
} catch (e) {
|
||||||
|
print("Erreur getPointDeVenteNomById: $e");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Future<List<Product>> searchProducts({
|
||||||
|
String? name,
|
||||||
|
String? imei,
|
||||||
|
String? reference,
|
||||||
|
bool onlyInStock = false,
|
||||||
|
String? category,
|
||||||
|
int? pointDeVenteId,
|
||||||
|
}) async {
|
||||||
|
final db = await database;
|
||||||
|
|
||||||
|
List<String> whereConditions = [];
|
||||||
|
List<dynamic> whereArgs = [];
|
||||||
|
|
||||||
|
if (name != null && name.isNotEmpty) {
|
||||||
|
whereConditions.add('name LIKE ?');
|
||||||
|
whereArgs.add('%$name%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imei != null && imei.isNotEmpty) {
|
||||||
|
whereConditions.add('imei LIKE ?');
|
||||||
|
whereArgs.add('%$imei%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reference != null && reference.isNotEmpty) {
|
||||||
|
whereConditions.add('reference LIKE ?');
|
||||||
|
whereArgs.add('%$reference%');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onlyInStock) {
|
||||||
|
whereConditions.add('stock > 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category != null && category.isNotEmpty) {
|
||||||
|
whereConditions.add('category = ?');
|
||||||
|
whereArgs.add(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointDeVenteId != null && pointDeVenteId > 0) {
|
||||||
|
whereConditions.add('point_de_vente_id = ?');
|
||||||
|
whereArgs.add(pointDeVenteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
String whereClause = whereConditions.isNotEmpty
|
||||||
|
? whereConditions.join(' AND ')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
final maps = await db.query(
|
||||||
|
'products',
|
||||||
|
where: whereClause.isNotEmpty ? whereClause : null,
|
||||||
|
whereArgs: whereArgs.isNotEmpty ? whereArgs : null,
|
||||||
|
orderBy: 'name ASC',
|
||||||
|
);
|
||||||
|
|
||||||
|
return List.generate(maps.length, (i) => Product.fromMap(maps[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtenir le nombre de produits en stock par catégorie
|
||||||
|
Future<Map<String, Map<String, int>>> getStockStatsByCategory() async {
|
||||||
|
final db = await database;
|
||||||
|
final result = await db.rawQuery('''
|
||||||
|
SELECT
|
||||||
|
category,
|
||||||
|
COUNT(*) as total_products,
|
||||||
|
SUM(CASE WHEN stock > 0 THEN 1 ELSE 0 END) as in_stock,
|
||||||
|
SUM(CASE WHEN stock = 0 OR stock IS NULL THEN 1 ELSE 0 END) as out_of_stock,
|
||||||
|
SUM(stock) as total_stock
|
||||||
|
FROM products
|
||||||
|
GROUP BY category
|
||||||
|
ORDER BY category
|
||||||
|
''');
|
||||||
|
|
||||||
|
Map<String, Map<String, int>> stats = {};
|
||||||
|
for (var row in result) {
|
||||||
|
stats[row['category'] as String] = {
|
||||||
|
'total': row['total_products'] as int,
|
||||||
|
'in_stock': row['in_stock'] as int,
|
||||||
|
'out_of_stock': row['out_of_stock'] as int,
|
||||||
|
'total_stock': row['total_stock'] as int? ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recherche rapide par code-barres/QR/IMEI
|
||||||
|
Future<Product?> findProductByCode(String code) async {
|
||||||
|
final db = await database;
|
||||||
|
|
||||||
|
// Essayer de trouver par référence d'abord
|
||||||
|
var maps = await db.query(
|
||||||
|
'products',
|
||||||
|
where: 'reference = ?',
|
||||||
|
whereArgs: [code],
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (maps.isNotEmpty) {
|
||||||
|
return Product.fromMap(maps.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensuite par IMEI
|
||||||
|
maps = await db.query(
|
||||||
|
'products',
|
||||||
|
where: 'imei = ?',
|
||||||
|
whereArgs: [code],
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (maps.isNotEmpty) {
|
||||||
|
return Product.fromMap(maps.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enfin par QR code si disponible
|
||||||
|
maps = await db.query(
|
||||||
|
'products',
|
||||||
|
where: 'qrCode = ?',
|
||||||
|
whereArgs: [code],
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (maps.isNotEmpty) {
|
||||||
|
return Product.fromMap(maps.first);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtenir les produits avec stock faible (seuil personnalisable)
|
||||||
|
Future<List<Product>> getLowStockProducts({int threshold = 5}) async {
|
||||||
|
final db = await database;
|
||||||
|
final maps = await db.query(
|
||||||
|
'products',
|
||||||
|
where: 'stock <= ? AND stock > 0',
|
||||||
|
whereArgs: [threshold],
|
||||||
|
orderBy: 'stock ASC',
|
||||||
|
);
|
||||||
|
return List.generate(maps.length, (i) => Product.fromMap(maps[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtenir les produits les plus vendus (basé sur les commandes)
|
||||||
|
Future<List<Map<String, dynamic>>> getMostSoldProducts({int limit = 10}) async {
|
||||||
|
final db = await database;
|
||||||
|
final result = await db.rawQuery('''
|
||||||
|
SELECT
|
||||||
|
p.id,
|
||||||
|
p.name,
|
||||||
|
p.price,
|
||||||
|
p.stock,
|
||||||
|
p.category,
|
||||||
|
SUM(dc.quantite) as total_sold,
|
||||||
|
COUNT(DISTINCT dc.commandeId) as order_count
|
||||||
|
FROM products p
|
||||||
|
INNER JOIN details_commandes dc ON p.id = dc.produitId
|
||||||
|
INNER JOIN commandes c ON dc.commandeId = c.id
|
||||||
|
WHERE c.statut != 5 -- Exclure les commandes annulées
|
||||||
|
GROUP BY p.id, p.name, p.price, p.stock, p.category
|
||||||
|
ORDER BY total_sold DESC
|
||||||
|
LIMIT ?
|
||||||
|
''', [limit]);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recherche de produits similaires (par nom ou catégorie)
|
||||||
|
Future<List<Product>> getSimilarProducts(Product product, {int limit = 5}) async {
|
||||||
|
final db = await database;
|
||||||
|
|
||||||
|
// Rechercher par catégorie et nom similaire, exclure le produit actuel
|
||||||
|
final maps = await db.rawQuery('''
|
||||||
|
SELECT *
|
||||||
|
FROM products
|
||||||
|
WHERE id != ?
|
||||||
|
AND (
|
||||||
|
category = ?
|
||||||
|
OR name LIKE ?
|
||||||
|
)
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN category = ? THEN 1 ELSE 2 END,
|
||||||
|
name ASC
|
||||||
|
LIMIT ?
|
||||||
|
''', [
|
||||||
|
product.id,
|
||||||
|
product.category,
|
||||||
|
'%${product.name.split(' ').first}%',
|
||||||
|
product.category,
|
||||||
|
limit
|
||||||
|
]);
|
||||||
|
|
||||||
|
return List.generate(maps.length, (i) => Product.fromMap(maps[i]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -30,24 +30,29 @@ final GlobalKey _salesChartKey = GlobalKey();
|
|||||||
late AnimationController _animationController;
|
late AnimationController _animationController;
|
||||||
late Animation<double> _fadeAnimation;
|
late Animation<double> _fadeAnimation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadData();
|
_loadData();
|
||||||
|
|
||||||
_animationController = AnimationController(
|
_animationController = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: Duration(milliseconds: 800),
|
duration: Duration(milliseconds: 800),
|
||||||
);
|
);
|
||||||
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
|
_fadeAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||||||
CurvedAnimation(
|
CurvedAnimation(
|
||||||
parent: _animationController,
|
parent: _animationController,
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
_animationController.forward();
|
// Démarrer l'animation après un léger délai
|
||||||
}
|
Future.delayed(Duration(milliseconds: 50), () {
|
||||||
|
if (mounted) {
|
||||||
|
_animationController.forward();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -354,47 +359,51 @@ Future<void> _showCategoryProductsDialog(String category) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSalesChart() {
|
Widget _buildSalesChart() {
|
||||||
key: _salesChartKey;
|
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 4,
|
key: _salesChartKey,
|
||||||
shape: RoundedRectangleBorder(
|
elevation: 4,
|
||||||
borderRadius: BorderRadius.circular(12),
|
shape: RoundedRectangleBorder(
|
||||||
),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Padding(
|
),
|
||||||
padding: EdgeInsets.all(16),
|
child: Padding(
|
||||||
child: Column(
|
padding: EdgeInsets.all(16),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
child: Column(
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Row(
|
children: [
|
||||||
children: [
|
// ... titre
|
||||||
Icon(Icons.trending_up, color: Colors.blue),
|
Container(
|
||||||
SizedBox(width: 8),
|
height: 200,
|
||||||
Text(
|
child: FutureBuilder<List<Commande>>(
|
||||||
'Ventes par mois',
|
future: _allOrdersFuture,
|
||||||
style: TextStyle(
|
builder: (context, snapshot) {
|
||||||
fontSize: 16,
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
fontWeight: FontWeight.bold,
|
return Center(child: CircularProgressIndicator());
|
||||||
),
|
}
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
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) {
|
if (snapshot.hasError || !snapshot.hasData || snapshot.data!.isEmpty) {
|
||||||
return Center(child: Text('Aucune donnée disponible'));
|
return Center(
|
||||||
}
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.trending_up_outlined, size: 64, color: Colors.grey),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text('Aucune donnée de vente disponible', style: TextStyle(color: Colors.grey)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final salesData = _groupOrdersByMonth(snapshot.data!);
|
final salesData = _groupOrdersByMonth(snapshot.data!);
|
||||||
|
|
||||||
return BarChart(
|
// 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(
|
BarChartData(
|
||||||
alignment: BarChartAlignment.spaceAround,
|
alignment: BarChartAlignment.spaceAround,
|
||||||
maxY: salesData.map((e) => e['total']).reduce((a, b) => a > b ? a : b) * 1.2,
|
maxY: salesData.map((e) => e['total']).reduce((a, b) => a > b ? a : b) * 1.2,
|
||||||
@ -498,99 +507,147 @@ Future<void> _showCategoryProductsDialog(String category) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStockChart() {
|
Widget _buildStockChart() {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(16),
|
padding: EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.inventory, color: Colors.blue),
|
Icon(Icons.inventory, color: Colors.blue),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'État du stock',
|
'État du stock',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
SizedBox(height: 16),
|
),
|
||||||
Container(
|
SizedBox(height: 16),
|
||||||
height: 200,
|
Container(
|
||||||
child: FutureBuilder<List<Product>>(
|
height: 200,
|
||||||
future: _database.getProducts(),
|
child: FutureBuilder<List<Product>>(
|
||||||
builder: (context, snapshot) {
|
future: _database.getProducts(),
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
builder: (context, snapshot) {
|
||||||
return Center(child: CircularProgressIndicator());
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
}
|
return Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
if (snapshot.hasError || !snapshot.hasData) {
|
if (snapshot.hasError || !snapshot.hasData) {
|
||||||
return Center(child: Text('Aucune donnée disponible'));
|
return Center(child: Text('Aucune donnée disponible'));
|
||||||
}
|
}
|
||||||
|
|
||||||
final products = snapshot.data!;
|
final products = snapshot.data!;
|
||||||
final lowStock = products.where((p) => (p.stock ?? 0) < 10).length;
|
|
||||||
final inStock = products.length - lowStock;
|
|
||||||
|
|
||||||
return PieChart(
|
// Vérification si la liste est vide
|
||||||
PieChartData(
|
if (products.isEmpty) {
|
||||||
sectionsSpace: 0,
|
return Center(
|
||||||
centerSpaceRadius: 40,
|
child: Column(
|
||||||
sections: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
PieChartSectionData(
|
children: [
|
||||||
color: Colors.orange,
|
Icon(Icons.inventory_2_outlined, size: 64, color: Colors.grey),
|
||||||
value: lowStock.toDouble(),
|
SizedBox(height: 16),
|
||||||
title: '$lowStock',
|
Text('Aucun produit en stock', style: TextStyle(color: Colors.grey)),
|
||||||
radius: 20,
|
|
||||||
titleStyle: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PieChartSectionData(
|
|
||||||
color: Colors.green,
|
|
||||||
value: inStock.toDouble(),
|
|
||||||
title: '$inStock',
|
|
||||||
radius: 20,
|
|
||||||
titleStyle: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
pieTouchData: PieTouchData(
|
|
||||||
touchCallback: (FlTouchEvent event, pieTouchResponse) {},
|
|
||||||
),
|
|
||||||
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(
|
SizedBox(height: 8),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
_buildLegendItem(Colors.orange, 'Stock faible'),
|
children: [
|
||||||
SizedBox(width: 16),
|
_buildLegendItem(Colors.orange, 'Stock faible'),
|
||||||
_buildLegendItem(Colors.green, 'En stock'),
|
SizedBox(width: 16),
|
||||||
],
|
_buildLegendItem(Colors.green, 'En stock'),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildLegendItem(Color color, String text) {
|
Widget _buildLegendItem(Color color, String text) {
|
||||||
return Row(
|
return Row(
|
||||||
@ -805,8 +862,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRecentOrdersCard() {
|
Widget _buildRecentOrdersCard() {
|
||||||
key: _recentOrdersKey;
|
|
||||||
return Card(
|
return Card(
|
||||||
|
key: _recentOrdersKey,
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@ -944,8 +1002,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
|
|||||||
|
|
||||||
|
|
||||||
Widget _buildRecentClientsCard() {
|
Widget _buildRecentClientsCard() {
|
||||||
key: _recentClientsKey;
|
|
||||||
return Card(
|
return Card(
|
||||||
|
key: _recentClientsKey,
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@ -1029,8 +1088,9 @@ Future<void> _showCategoryProductsDialog(String category) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLowStockCard() {
|
Widget _buildLowStockCard() {
|
||||||
key: _lowStockKey;
|
|
||||||
return Card(
|
return Card(
|
||||||
|
key: _lowStockKey,
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@ -1136,12 +1196,6 @@ Future<void> _showCategoryProductsDialog(String category) async {
|
|||||||
return Colors.orange;
|
return Colors.orange;
|
||||||
case StatutCommande.confirmee:
|
case StatutCommande.confirmee:
|
||||||
return Colors.blue;
|
return Colors.blue;
|
||||||
case StatutCommande.enPreparation:
|
|
||||||
return Colors.purple;
|
|
||||||
case StatutCommande.expediee:
|
|
||||||
return Colors.teal;
|
|
||||||
case StatutCommande.livree:
|
|
||||||
return Colors.green;
|
|
||||||
case StatutCommande.annulee:
|
case StatutCommande.annulee:
|
||||||
return Colors.red;
|
return Colors.red;
|
||||||
default:
|
default:
|
||||||
|
|||||||
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:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:numbers_to_letters/numbers_to_letters.dart';
|
||||||
import 'package:pdf/pdf.dart';
|
import 'package:pdf/pdf.dart';
|
||||||
import 'package:pdf/widgets.dart' as pw;
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@ -118,10 +119,6 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
message = 'Commande annulée avec succès';
|
message = 'Commande annulée avec succès';
|
||||||
backgroundColor = Colors.orange;
|
backgroundColor = Colors.orange;
|
||||||
break;
|
break;
|
||||||
case StatutCommande.livree:
|
|
||||||
message = 'Commande marquée comme livrée';
|
|
||||||
backgroundColor = Colors.green;
|
|
||||||
break;
|
|
||||||
case StatutCommande.confirmee:
|
case StatutCommande.confirmee:
|
||||||
message = 'Commande confirmée';
|
message = 'Commande confirmée';
|
||||||
backgroundColor = Colors.blue;
|
backgroundColor = Colors.blue;
|
||||||
@ -230,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 {
|
Future<pw.Widget> buildIconGlobeText() async {
|
||||||
final details = await _database.getDetailsCommande(commande.id!);
|
final font = pw.Font.ttf(await rootBundle.load('assets/fa-solid-900.ttf'));
|
||||||
final client = await _database.getClientById(commande.clientId);
|
return pw.Text(String.fromCharCode(0xf0ac), style: pw.TextStyle(font: font));
|
||||||
final commandeur = commande.commandeurId != null
|
}
|
||||||
? await _database.getUserById(commande.commandeurId!)
|
|
||||||
: null;
|
|
||||||
final validateur = commande.validateurId != null
|
|
||||||
? await _database.getUserById(commande.validateurId!)
|
|
||||||
: null;
|
|
||||||
final pointDeVente = commandeur?.pointDeVenteId != null
|
|
||||||
? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
final pdf = pw.Document();
|
|
||||||
final imageBytes = await loadImage();
|
|
||||||
final image = pw.MemoryImage(imageBytes);
|
|
||||||
|
|
||||||
final headerStyle = pw.TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: pw.FontWeight.bold,
|
|
||||||
color: PdfColors.blue900,
|
|
||||||
);
|
|
||||||
|
|
||||||
final titleStyle = pw.TextStyle(
|
Future<void> _generateInvoice(Commande commande) async {
|
||||||
fontSize: 14,
|
final details = await _database.getDetailsCommande(commande.id!);
|
||||||
fontWeight: pw.FontWeight.bold,
|
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();
|
||||||
|
|
||||||
final subtitleStyle = pw.TextStyle(
|
// IMPORTANT: Récupérer tous les détails des produits AVANT de créer le PDF
|
||||||
fontSize: 12,
|
final List<Map<String, dynamic>> detailsAvecProduits = [];
|
||||||
color: PdfColors.grey600,
|
for (final detail in details) {
|
||||||
);
|
final produit = await _database.getProductById(detail.produitId);
|
||||||
|
detailsAvecProduits.add({
|
||||||
|
'detail': detail,
|
||||||
|
'produit': produit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pdf.addPage(
|
final pdf = pw.Document();
|
||||||
pw.Page(
|
final imageBytes = await loadImage();
|
||||||
margin: const pw.EdgeInsets.all(20),
|
final image = pw.MemoryImage(imageBytes);
|
||||||
build: (pw.Context context) {
|
final italicFont = pw.Font.ttf(await rootBundle.load('assets/fonts/Roboto-Italic.ttf'));
|
||||||
return pw.Column(
|
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
// Styles de texte
|
||||||
children: [
|
final smallTextStyle = pw.TextStyle(fontSize: 9);
|
||||||
pw.Row(
|
final smallBoldTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold);
|
||||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
final normalTextStyle = pw.TextStyle(fontSize: 10);
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
final boldTextStyle = pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold);
|
||||||
children: [
|
final boldTexClienttStyle = pw.TextStyle(fontSize: 12, fontWeight: pw.FontWeight.bold);
|
||||||
pw.Column(
|
final frameTextStyle = pw.TextStyle(fontSize: 10);
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
final italicTextStyle = pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold, font: italicFont);
|
||||||
children: [
|
final italicTextStyleLogo = pw.TextStyle(fontSize: 7, fontWeight: pw.FontWeight.bold, font: italicFont);
|
||||||
pw.Container(
|
|
||||||
width: 100,
|
pdf.addPage(
|
||||||
height: 80,
|
pw.Page(
|
||||||
decoration: pw.BoxDecoration(
|
margin: const pw.EdgeInsets.all(20),
|
||||||
border:
|
build: (pw.Context context) {
|
||||||
pw.Border.all(color: PdfColors.blue900, width: 2),
|
return pw.Column(
|
||||||
borderRadius: pw.BorderRadius.circular(8),
|
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),
|
padding: const pw.EdgeInsets.all(10),
|
||||||
pw.Text('guycom', style: headerStyle),
|
child: pw.Column(
|
||||||
if (pointDeVente != null)
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||||
pw.Text('Point de vente: ${pointDeVente['designation']}', style: subtitleStyle),
|
children: [
|
||||||
pw.Text('Tél: +213 123 456 789', style: subtitleStyle),
|
pw.Text('ID Client: ', style: frameTextStyle),
|
||||||
],
|
pw.SizedBox(height: 5),
|
||||||
),
|
pw.Text('${pointDeVente?['nom'] ?? 'S405A'} - ${client?.id ?? 'Non spécifié'}', style: boldTexClienttStyle),
|
||||||
pw.Column(
|
pw.SizedBox(height: 5),
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
pw.Container(width: 200, height: 1, color: PdfColors.black),
|
||||||
|
pw.Text(client?.nom ?? 'Non spécifié', style: boldTexClienttStyle),
|
||||||
|
pw.SizedBox(height: 10),
|
||||||
|
pw.Text(client?.telephone ?? 'Non spécifié', style: frameTextStyle),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
pw.SizedBox(height: 20),
|
||||||
|
|
||||||
|
// Tableau des produits avec plus de colonnes
|
||||||
|
pw.Table(
|
||||||
|
border: pw.TableBorder.all(width: 0.5),
|
||||||
|
columnWidths: {
|
||||||
|
0: const pw.FlexColumnWidth(3), // Désignation
|
||||||
|
1: const pw.FlexColumnWidth(1), // Qté
|
||||||
|
2: const pw.FlexColumnWidth(2), // Prix unitaire
|
||||||
|
3: const pw.FlexColumnWidth(2), // Montant
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
// En-tête du tableau
|
||||||
|
pw.TableRow(
|
||||||
|
decoration: const pw.BoxDecoration(color: PdfColors.grey200),
|
||||||
|
children: [
|
||||||
|
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Désignations', style: boldTextStyle)),
|
||||||
|
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Qté', style: boldTextStyle, textAlign: pw.TextAlign.center)),
|
||||||
|
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Prix unitaire', style: boldTextStyle, textAlign: pw.TextAlign.right)),
|
||||||
|
pw.Padding(padding: const pw.EdgeInsets.all(4), child: pw.Text('Montant', style: boldTextStyle, textAlign: pw.TextAlign.right)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Lignes des produits avec détails complets
|
||||||
|
...detailsAvecProduits.map((item) {
|
||||||
|
final detail = item['detail'] as DetailCommande;
|
||||||
|
final produit = item['produit'];
|
||||||
|
|
||||||
|
return pw.TableRow(
|
||||||
children: [
|
children: [
|
||||||
pw.Container(
|
pw.Padding(
|
||||||
padding: const pw.EdgeInsets.all(12),
|
padding: const pw.EdgeInsets.all(4),
|
||||||
decoration: pw.BoxDecoration(
|
|
||||||
color: PdfColors.blue50,
|
|
||||||
borderRadius: pw.BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: pw.Column(
|
child: pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
pw.Text(
|
// Nom du produit
|
||||||
'FACTURE',
|
pw.Text(detail.produitNom ?? 'Produit inconnu',
|
||||||
style: pw.TextStyle(
|
style: pw.TextStyle(fontSize: 10, fontWeight: pw.FontWeight.bold)),
|
||||||
fontSize: 20,
|
pw.SizedBox(height: 2),
|
||||||
fontWeight: pw.FontWeight.bold,
|
|
||||||
color: PdfColors.blue900,
|
|
||||||
),
|
if (produit?.category != null && produit!.category.isNotEmpty && produit?.marque != null && produit!.marque.isNotEmpty)
|
||||||
),
|
pw.Text('${produit.category} ${produit.marque}', style: smallTextStyle),
|
||||||
pw.SizedBox(height: 8),
|
|
||||||
pw.Text('N°: ${commande.id}', style: titleStyle),
|
// IMEI
|
||||||
pw.Text(
|
if (produit?.imei != null && produit!.imei!.isNotEmpty)
|
||||||
'Date: ${DateFormat('dd/MM/yyyy').format(commande.dateCommande)}'),
|
pw.Text('${produit.imei}', style: smallTextStyle),
|
||||||
|
|
||||||
|
|
||||||
|
// Référence
|
||||||
|
if (produit?.reference != null && produit!.reference!.isNotEmpty && produit?.ram != null && produit!.ram!.isNotEmpty && produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty)
|
||||||
|
pw.Text('${produit.ram} | ${produit.memoireInterne} | ${produit.reference}', style: smallTextStyle),
|
||||||
|
|
||||||
|
// // IMEI
|
||||||
|
// if (produit?.imei != null && produit!.imei!.isNotEmpty)
|
||||||
|
// pw.Text('IMEI: ${produit.imei}', style: smallTextStyle),
|
||||||
|
|
||||||
|
// // RAM
|
||||||
|
// if (produit?.ram != null && produit!.ram!.isNotEmpty)
|
||||||
|
// pw.Text('RAM: ${produit.ram}', style: smallTextStyle),
|
||||||
|
|
||||||
|
// // Stockage
|
||||||
|
// if (produit?.memoireInterne != null && produit!.memoireInterne!.isNotEmpty)
|
||||||
|
// pw.Text('Stockage: ${produit.memoireInterne}', style: smallTextStyle),
|
||||||
|
|
||||||
|
// // Catégorie
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(4),
|
||||||
|
child: pw.Text('${detail.quantite}', style: normalTextStyle, textAlign: pw.TextAlign.center),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(4),
|
||||||
|
child: pw.Text('${detail.prixUnitaire.toStringAsFixed(0)}', style: normalTextStyle, textAlign: pw.TextAlign.right),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(4),
|
||||||
|
child: pw.Text('${detail.sousTotal.toStringAsFixed(0)}', style: normalTextStyle, textAlign: pw.TextAlign.right),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
],
|
}).toList(),
|
||||||
),
|
],
|
||||||
|
),
|
||||||
|
|
||||||
pw.SizedBox(height: 30),
|
pw.SizedBox(height: 10),
|
||||||
|
|
||||||
// Informations client
|
// Total
|
||||||
pw.Container(
|
pw.Row(
|
||||||
width: double.infinity,
|
mainAxisAlignment: pw.MainAxisAlignment.end,
|
||||||
padding: const pw.EdgeInsets.all(12),
|
children: [
|
||||||
decoration: pw.BoxDecoration(
|
pw.Text('TOTAL', style: boldTextStyle),
|
||||||
color: PdfColors.grey100,
|
pw.SizedBox(width: 20),
|
||||||
borderRadius: pw.BorderRadius.circular(8),
|
pw.Text('${commande.montantTotal.toStringAsFixed(0)}', style: boldTextStyle),
|
||||||
),
|
],
|
||||||
child: pw.Column(
|
),
|
||||||
|
|
||||||
|
pw.SizedBox(height: 10),
|
||||||
|
|
||||||
|
// Montant en lettres
|
||||||
|
pw.Text('Arrêté à la somme de: ${_numberToWords(commande.montantTotal.toInt())} Ariary', style: italicTextStyle),
|
||||||
|
|
||||||
|
pw.SizedBox(height: 30),
|
||||||
|
|
||||||
|
// Signatures
|
||||||
|
pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
pw.Text('FACTURÉ À:', style: titleStyle),
|
pw.Text('Signature du vendeur', style: smallTextStyle),
|
||||||
pw.SizedBox(height: 5),
|
pw.SizedBox(height: 20),
|
||||||
pw.Text(client?.nomComplet ?? 'Client inconnu',
|
pw.Container(width: 150, height: 1, color: PdfColors.black),
|
||||||
style: pw.TextStyle(fontSize: 12)),
|
|
||||||
if (client?.telephone != null)
|
|
||||||
pw.Text('Tél: ${client!.telephone}',
|
|
||||||
style: pw.TextStyle(
|
|
||||||
fontSize: 10, color: PdfColors.grey600)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
pw.SizedBox(height: 20),
|
|
||||||
|
|
||||||
// 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(
|
|
||||||
children: [
|
children: [
|
||||||
pw.Text(
|
pw.Text('Signature du client', style: smallTextStyle),
|
||||||
'Merci pour votre confiance!',
|
pw.SizedBox(height: 20),
|
||||||
style: pw.TextStyle(
|
pw.Container(width: 150, height: 1, color: PdfColors.black),
|
||||||
fontSize: 14,
|
|
||||||
fontStyle: pw.FontStyle.italic,
|
|
||||||
color: PdfColors.blue900,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
pw.SizedBox(height: 5),
|
|
||||||
pw.Text(
|
|
||||||
'Cette facture est générée automatiquement par le système Youmaz Gestion',
|
|
||||||
style:
|
|
||||||
pw.TextStyle(fontSize: 8, color: PdfColors.grey600),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final output = await getTemporaryDirectory();
|
final output = await getTemporaryDirectory();
|
||||||
final file = File('${output.path}/facture_${commande.id}.pdf');
|
final file = File('${output.path}/facture_${commande.id}.pdf');
|
||||||
await file.writeAsBytes(await pdf.save());
|
await file.writeAsBytes(await pdf.save());
|
||||||
await OpenFile.open(file.path);
|
await OpenFile.open(file.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pw.Widget _buildCheckboxPointDeVente(String text, bool checked) {
|
||||||
|
return pw.Row(
|
||||||
|
children: [
|
||||||
|
pw.Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: pw.BoxDecoration(
|
||||||
|
border: pw.Border.all(width: 1),
|
||||||
|
color: checked ? PdfColors.black : PdfColors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.SizedBox(width: 5),
|
||||||
|
pw.Text(text, style: pw.TextStyle(fontSize: 9)),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _numberToWords(int number) {
|
||||||
|
// Implémentez la conversion du nombre en lettres ici
|
||||||
|
// Exemple simplifié:
|
||||||
|
NumbersToLetters.toLetters('fr', number);
|
||||||
|
return NumbersToLetters.toLetters('fr', number);
|
||||||
|
}
|
||||||
Future<void> _generateReceipt(Commande commande, PaymentMethod payment) async {
|
Future<void> _generateReceipt(Commande commande, PaymentMethod payment) async {
|
||||||
final details = await _database.getDetailsCommande(commande.id!);
|
final details = await _database.getDetailsCommande(commande.id!);
|
||||||
final client = await _database.getClientById(commande.clientId);
|
final client = await _database.getClientById(commande.clientId);
|
||||||
final commandeur = commande.commandeurId != null
|
final commandeur = commande.commandeurId != null
|
||||||
? await _database.getUserById(commande.commandeurId!)
|
? await _database.getUserById(commande.commandeurId!)
|
||||||
: null;
|
: null;
|
||||||
final validateur = commande.validateurId != null
|
final validateur = commande.validateurId != null
|
||||||
? await _database.getUserById(commande.validateurId!)
|
? await _database.getUserById(commande.validateurId!)
|
||||||
: null;
|
: null;
|
||||||
final pointDeVente = commandeur?.pointDeVenteId != null
|
final pointDeVente = commandeur?.pointDeVenteId != null
|
||||||
? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!)
|
? await _database.getPointDeVenteById(commandeur!.pointDeVenteId!)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
final pdf = pw.Document();
|
// Récupérer les détails complets des produits
|
||||||
final imageBytes = await loadImage();
|
final List<Map<String, dynamic>> detailsAvecProduits = [];
|
||||||
final image = pw.MemoryImage(imageBytes);
|
for (final detail in details) {
|
||||||
|
final produit = await _database.getProductById(detail.produitId);
|
||||||
pdf.addPage(
|
detailsAvecProduits.add({
|
||||||
pw.Page(
|
'detail': detail,
|
||||||
pageFormat: PdfPageFormat(70 * PdfPageFormat.mm, double.infinity),
|
'produit': produit,
|
||||||
margin: const pw.EdgeInsets.all(4),
|
});
|
||||||
build: (pw.Context context) {
|
|
||||||
return pw.Column(
|
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]) {
|
pw.Widget _buildTableCell(String text, [pw.TextStyle? style]) {
|
||||||
return pw.Padding(
|
return pw.Padding(
|
||||||
padding: const pw.EdgeInsets.all(4.0),
|
padding: const pw.EdgeInsets.all(4.0),
|
||||||
@ -632,12 +762,12 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
return Colors.orange.shade100;
|
return Colors.orange.shade100;
|
||||||
case StatutCommande.confirmee:
|
case StatutCommande.confirmee:
|
||||||
return Colors.blue.shade100;
|
return Colors.blue.shade100;
|
||||||
case StatutCommande.enPreparation:
|
// case StatutCommande.enPreparation:
|
||||||
return Colors.amber.shade100;
|
// return Colors.amber.shade100;
|
||||||
case StatutCommande.expediee:
|
// case StatutCommande.expediee:
|
||||||
return Colors.purple.shade100;
|
// return Colors.purple.shade100;
|
||||||
case StatutCommande.livree:
|
// case StatutCommande.livree:
|
||||||
return Colors.green.shade100;
|
// return Colors.green.shade100;
|
||||||
case StatutCommande.annulee:
|
case StatutCommande.annulee:
|
||||||
return Colors.red.shade100;
|
return Colors.red.shade100;
|
||||||
}
|
}
|
||||||
@ -649,12 +779,12 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
return Icons.schedule;
|
return Icons.schedule;
|
||||||
case StatutCommande.confirmee:
|
case StatutCommande.confirmee:
|
||||||
return Icons.check_circle_outline;
|
return Icons.check_circle_outline;
|
||||||
case StatutCommande.enPreparation:
|
// case StatutCommande.enPreparation:
|
||||||
return Icons.settings;
|
// return Icons.settings;
|
||||||
case StatutCommande.expediee:
|
// case StatutCommande.expediee:
|
||||||
return Icons.local_shipping;
|
// return Icons.local_shipping;
|
||||||
case StatutCommande.livree:
|
// case StatutCommande.livree:
|
||||||
return Icons.check_circle;
|
// return Icons.check_circle;
|
||||||
case StatutCommande.annulee:
|
case StatutCommande.annulee:
|
||||||
return Icons.cancel;
|
return Icons.cancel;
|
||||||
}
|
}
|
||||||
@ -1209,12 +1339,12 @@ class _GestionCommandesPageState extends State<GestionCommandesPage> {
|
|||||||
return 'En attente';
|
return 'En attente';
|
||||||
case StatutCommande.confirmee:
|
case StatutCommande.confirmee:
|
||||||
return 'Confirmée';
|
return 'Confirmée';
|
||||||
case StatutCommande.enPreparation:
|
// case StatutCommande.enPreparation:
|
||||||
return 'En préparation';
|
// return 'En préparation';
|
||||||
case StatutCommande.expediee:
|
// case StatutCommande.expediee:
|
||||||
return 'Expédiée';
|
// return 'Expédiée';
|
||||||
case StatutCommande.livree:
|
// case StatutCommande.livree:
|
||||||
return 'Livrée';
|
// return 'Livrée';
|
||||||
case StatutCommande.annulee:
|
case StatutCommande.annulee:
|
||||||
return 'Annulée';
|
return 'Annulée';
|
||||||
}
|
}
|
||||||
@ -1424,9 +1554,9 @@ class _CommandeActions extends StatelessWidget {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case StatutCommande.confirmee:
|
case StatutCommande.confirmee:
|
||||||
case StatutCommande.enPreparation:
|
// case StatutCommande.enPreparation:
|
||||||
case StatutCommande.expediee:
|
// case StatutCommande.expediee:
|
||||||
case StatutCommande.livree:
|
// case StatutCommande.livree:
|
||||||
buttons.add(
|
buttons.add(
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,6 @@ import 'package:youmazgestion/accueil.dart';
|
|||||||
import '../Models/users.dart';
|
import '../Models/users.dart';
|
||||||
import '../controller/userController.dart';
|
import '../controller/userController.dart';
|
||||||
|
|
||||||
|
|
||||||
class LoginPage extends StatefulWidget {
|
class LoginPage extends StatefulWidget {
|
||||||
const LoginPage({super.key});
|
const LoginPage({super.key});
|
||||||
|
|
||||||
@ -126,8 +125,8 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => const MainLayout()),
|
MaterialPageRoute(builder: (context) => const MainLayout()),
|
||||||
);
|
);
|
||||||
}else{
|
} else {
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => DashboardPage()),
|
MaterialPageRoute(builder: (context) => DashboardPage()),
|
||||||
);
|
);
|
||||||
@ -216,88 +215,124 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
],
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.lock_outline,
|
|
||||||
size: 100.0,
|
|
||||||
color: Color.fromARGB(255, 4, 54, 95),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
controller: _usernameController,
|
|
||||||
enabled: !_isLoading,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Username',
|
|
||||||
prefixIcon: const Icon(Icons.person, color: Colors.blueAccent),
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(30.0),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
const SizedBox(height: 16.0),
|
TextField(
|
||||||
TextField(
|
controller: _usernameController,
|
||||||
controller: _passwordController,
|
enabled: !_isLoading,
|
||||||
enabled: !_isLoading,
|
decoration: InputDecoration(
|
||||||
decoration: InputDecoration(
|
labelText: 'Nom d\'utilisateur',
|
||||||
labelText: 'Password',
|
labelStyle: TextStyle(
|
||||||
prefixIcon: const Icon(Icons.lock, color: Colors.redAccent),
|
color: primaryColor.withOpacity(0.7),
|
||||||
border: OutlineInputBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(30.0),
|
prefixIcon: Icon(Icons.person, color: accentColor),
|
||||||
|
filled: true,
|
||||||
|
fillColor: accentColor.withOpacity(0.045),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(30.0),
|
||||||
|
borderSide: BorderSide(color: accentColor, width: 2),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(30.0),
|
||||||
|
borderSide: BorderSide(color: accentColor, width: 2),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
obscureText: true,
|
const SizedBox(height: 18.0),
|
||||||
onSubmitted: (_) => _login(),
|
TextField(
|
||||||
),
|
controller: _passwordController,
|
||||||
const SizedBox(height: 16.0),
|
enabled: !_isLoading,
|
||||||
Visibility(
|
obscureText: true,
|
||||||
visible: _isErrorVisible,
|
decoration: InputDecoration(
|
||||||
child: Text(
|
labelText: 'Mot de passe',
|
||||||
_errorMessage,
|
labelStyle: TextStyle(
|
||||||
style: const TextStyle(
|
color: primaryColor.withOpacity(0.7),
|
||||||
color: Colors.red,
|
),
|
||||||
fontSize: 14,
|
prefixIcon: Icon(Icons.lock, color: accentColor),
|
||||||
|
filled: true,
|
||||||
|
fillColor: accentColor.withOpacity(0.045),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(30.0),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(30.0),
|
||||||
|
borderSide: BorderSide(color: accentColor, width: 2),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
onSubmitted: (_) => _login(),
|
||||||
),
|
),
|
||||||
),
|
if (_isErrorVisible) ...[
|
||||||
const SizedBox(height: 16.0),
|
const SizedBox(height: 12.0),
|
||||||
ElevatedButton(
|
Text(
|
||||||
onPressed: _isLoading ? null : _login,
|
_errorMessage,
|
||||||
style: ElevatedButton.styleFrom(
|
style: const TextStyle(
|
||||||
backgroundColor: const Color(0xFF0015B7),
|
color: Colors.redAccent,
|
||||||
elevation: 5.0,
|
fontSize: 15,
|
||||||
shape: RoundedRectangleBorder(
|
fontWeight: FontWeight.w600,
|
||||||
borderRadius: BorderRadius.circular(30.0),
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
minimumSize: const Size(double.infinity, 48),
|
],
|
||||||
),
|
const SizedBox(height: 26.0),
|
||||||
child: _isLoading
|
ElevatedButton(
|
||||||
? const SizedBox(
|
onPressed: _isLoading ? null : _login,
|
||||||
height: 20,
|
style: ElevatedButton.styleFrom(
|
||||||
width: 20,
|
backgroundColor: accentColor,
|
||||||
child: CircularProgressIndicator(
|
disabledBackgroundColor: accentColor.withOpacity(0.3),
|
||||||
color: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
strokeWidth: 2,
|
elevation: 7.0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(30.0),
|
||||||
|
),
|
||||||
|
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(
|
// Option debug, à enlever en prod
|
||||||
'Se connecter',
|
if (_isErrorVisible) ...[
|
||||||
style: TextStyle(
|
TextButton(
|
||||||
color: Colors.white,
|
onPressed: () async {
|
||||||
fontSize: 16,
|
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,10 +26,19 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
final TextEditingController _telephoneController = TextEditingController();
|
final TextEditingController _telephoneController = TextEditingController();
|
||||||
final TextEditingController _adresseController = TextEditingController();
|
final TextEditingController _adresseController = TextEditingController();
|
||||||
|
|
||||||
|
// Contrôleurs pour les filtres - NOUVEAU
|
||||||
|
final TextEditingController _searchNameController = TextEditingController();
|
||||||
|
final TextEditingController _searchImeiController = TextEditingController();
|
||||||
|
final TextEditingController _searchReferenceController = TextEditingController();
|
||||||
|
|
||||||
// Panier
|
// Panier
|
||||||
final List<Product> _products = [];
|
final List<Product> _products = [];
|
||||||
|
final List<Product> _filteredProducts = []; // NOUVEAU - Liste filtrée
|
||||||
final Map<int, int> _quantites = {};
|
final Map<int, int> _quantites = {};
|
||||||
|
|
||||||
|
// Variables de filtre - NOUVEAU
|
||||||
|
bool _showOnlyInStock = false;
|
||||||
|
|
||||||
// Utilisateurs commerciaux
|
// Utilisateurs commerciaux
|
||||||
List<Users> _commercialUsers = [];
|
List<Users> _commercialUsers = [];
|
||||||
Users? _selectedCommercialUser;
|
Users? _selectedCommercialUser;
|
||||||
@ -39,12 +48,20 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
_loadProducts();
|
_loadProducts();
|
||||||
_loadCommercialUsers();
|
_loadCommercialUsers();
|
||||||
|
|
||||||
|
// Listeners pour les filtres - NOUVEAU
|
||||||
|
_searchNameController.addListener(_filterProducts);
|
||||||
|
_searchImeiController.addListener(_filterProducts);
|
||||||
|
_searchReferenceController.addListener(_filterProducts);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadProducts() async {
|
Future<void> _loadProducts() async {
|
||||||
final products = await _appDatabase.getProducts();
|
final products = await _appDatabase.getProducts();
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_products.clear();
|
||||||
_products.addAll(products);
|
_products.addAll(products);
|
||||||
|
_filteredProducts.clear();
|
||||||
|
_filteredProducts.addAll(products); // Initialiser la liste filtrée
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,78 +75,204 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
floatingActionButton: _buildFloatingCartButton(),
|
floatingActionButton: _buildFloatingCartButton(),
|
||||||
appBar: CustomAppBar(title: 'Nouvelle Commande'),
|
appBar: CustomAppBar(title: 'Faire un commande'),
|
||||||
drawer: CustomDrawer(),
|
drawer: CustomDrawer(),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Header
|
// 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(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
@ -146,6 +289,11 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
child: const Text('Ajouter les informations client'),
|
child: const Text('Ajouter les informations client'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// NOUVEAU - Section des filtres
|
||||||
|
_buildFilterSection(),
|
||||||
|
|
||||||
|
// Liste des produits
|
||||||
_buildProductList(),
|
_buildProductList(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -171,54 +319,72 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
void _showClientFormDialog() {
|
void _showClientFormDialog() {
|
||||||
Get.dialog(
|
Get.dialog(
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
title: const Text('Informations Client'),
|
title: Row(
|
||||||
content: SingleChildScrollView(
|
children: [
|
||||||
child: Form(
|
Container(
|
||||||
key: _formKey,
|
padding: const EdgeInsets.all(8),
|
||||||
child: Column(
|
decoration: BoxDecoration(
|
||||||
mainAxisSize: MainAxisSize.min,
|
color: Colors.blue.shade100,
|
||||||
children: [
|
borderRadius: BorderRadius.circular(8),
|
||||||
_buildTextFormField(
|
),
|
||||||
controller: _nomController,
|
child: Icon(Icons.person_add, color: Colors.blue.shade700),
|
||||||
label: 'Nom',
|
),
|
||||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
|
const SizedBox(width: 12),
|
||||||
),
|
const Text('Informations Client'),
|
||||||
const SizedBox(height: 12),
|
],
|
||||||
_buildTextFormField(
|
),
|
||||||
controller: _prenomController,
|
content: Container(
|
||||||
label: 'Prénom',
|
width: 600,
|
||||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null,
|
constraints: const BoxConstraints(maxHeight: 600),
|
||||||
),
|
child: SingleChildScrollView(
|
||||||
const SizedBox(height: 12),
|
child: Form(
|
||||||
_buildTextFormField(
|
key: _formKey,
|
||||||
controller: _emailController,
|
child: Column(
|
||||||
label: 'Email',
|
mainAxisSize: MainAxisSize.min,
|
||||||
keyboardType: TextInputType.emailAddress,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
validator: (value) {
|
children: [
|
||||||
if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
|
_buildTextFormField(
|
||||||
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
|
controller: _nomController,
|
||||||
return 'Email invalide';
|
label: 'Nom',
|
||||||
}
|
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
|
||||||
return null;
|
),
|
||||||
},
|
const SizedBox(height: 12),
|
||||||
),
|
_buildTextFormField(
|
||||||
const SizedBox(height: 12),
|
controller: _prenomController,
|
||||||
_buildTextFormField(
|
label: 'Prénom',
|
||||||
controller: _telephoneController,
|
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null,
|
||||||
label: 'Téléphone',
|
),
|
||||||
keyboardType: TextInputType.phone,
|
const SizedBox(height: 12),
|
||||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null,
|
_buildTextFormField(
|
||||||
),
|
controller: _emailController,
|
||||||
const SizedBox(height: 12),
|
label: 'Email',
|
||||||
_buildTextFormField(
|
keyboardType: TextInputType.emailAddress,
|
||||||
controller: _adresseController,
|
validator: (value) {
|
||||||
label: 'Adresse',
|
if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
|
||||||
maxLines: 2,
|
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
|
||||||
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null,
|
return 'Email invalide';
|
||||||
),
|
}
|
||||||
const SizedBox(height: 12),
|
return null;
|
||||||
_buildCommercialDropdown(),
|
},
|
||||||
],
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _telephoneController,
|
||||||
|
label: 'Téléphone',
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
|
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildTextFormField(
|
||||||
|
controller: _adresseController,
|
||||||
|
label: 'Adresse',
|
||||||
|
maxLines: 2,
|
||||||
|
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildCommercialDropdown(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -231,20 +397,15 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: Colors.blue.shade800,
|
backgroundColor: Colors.blue.shade800,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_formKey.currentState!.validate()) {
|
if (_formKey.currentState!.validate()) {
|
||||||
Get.back();
|
Get.back();
|
||||||
Get.snackbar(
|
_submitOrder();
|
||||||
'Succès',
|
|
||||||
'Informations client enregistrées',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: const Text('Enregistrer'),
|
child: const Text('Valider la commande'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -306,6 +467,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WIDGET MODIFIÉ - Liste des produits (utilise maintenant _filteredProducts)
|
||||||
Widget _buildProductList() {
|
Widget _buildProductList() {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
@ -326,14 +488,14 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_products.isEmpty
|
_filteredProducts.isEmpty
|
||||||
? const Center(child: CircularProgressIndicator())
|
? _buildEmptyState()
|
||||||
: ListView.builder(
|
: ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemCount: _products.length,
|
itemCount: _filteredProducts.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final product = _products[index];
|
final product = _filteredProducts[index];
|
||||||
final quantity = _quantites[product.id] ?? 0;
|
final quantity = _quantites[product.id] ?? 0;
|
||||||
|
|
||||||
return _buildProductListItem(product, quantity);
|
return _buildProductListItem(product, quantity);
|
||||||
@ -345,94 +507,171 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOUVEAU WIDGET - État vide
|
||||||
|
Widget _buildEmptyState() {
|
||||||
|
return Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(32.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.search_off,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Aucun produit trouvé',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Essayez de modifier vos critères de recherche',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WIDGET MODIFIÉ - Item de produit (ajout d'informations IMEI/Référence)
|
||||||
Widget _buildProductListItem(Product product, int quantity) {
|
Widget _buildProductListItem(Product product, int quantity) {
|
||||||
|
final bool isOutOfStock = product.stock != null && product.stock! <= 0;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: Container(
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
decoration: BoxDecoration(
|
||||||
horizontal: 16,
|
borderRadius: BorderRadius.circular(8),
|
||||||
vertical: 8,
|
border: isOutOfStock
|
||||||
|
? Border.all(color: Colors.red.shade200, width: 1.5)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
leading: Container(
|
child: ListTile(
|
||||||
width: 50,
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
height: 50,
|
horizontal: 16,
|
||||||
decoration: BoxDecoration(
|
vertical: 8,
|
||||||
color: Colors.blue.shade50,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.shopping_bag, color: Colors.blue),
|
leading: Container(
|
||||||
),
|
width: 50,
|
||||||
title: Text(
|
height: 50,
|
||||||
product.name,
|
decoration: BoxDecoration(
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
color: isOutOfStock
|
||||||
),
|
? Colors.red.shade50
|
||||||
subtitle: Column(
|
: Colors.blue.shade50,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
borderRadius: BorderRadius.circular(8),
|
||||||
children: [
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'${product.price.toStringAsFixed(2)} DA',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.green.shade700,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
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(
|
Text(
|
||||||
'Stock: ${product.stock}',
|
'${product.price.toStringAsFixed(2)} MGA',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
color: Colors.green.shade700,
|
||||||
color: Colors.grey.shade600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
if (product.stock != null)
|
||||||
),
|
Text(
|
||||||
trailing: Container(
|
'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}',
|
||||||
decoration: BoxDecoration(
|
style: TextStyle(
|
||||||
color: Colors.blue.shade50,
|
fontSize: 12,
|
||||||
borderRadius: BorderRadius.circular(20),
|
color: isOutOfStock
|
||||||
),
|
? Colors.red.shade600
|
||||||
child: Row(
|
: Colors.grey.shade600,
|
||||||
mainAxisSize: MainAxisSize.min,
|
fontWeight: isOutOfStock ? FontWeight.w600 : FontWeight.normal,
|
||||||
children: [
|
),
|
||||||
IconButton(
|
),
|
||||||
icon: const Icon(Icons.remove, size: 18),
|
// Affichage IMEI et Référence
|
||||||
onPressed: () {
|
if (product.imei != null && product.imei!.isNotEmpty)
|
||||||
if (quantity > 0) {
|
Text(
|
||||||
setState(() {
|
'IMEI: ${product.imei}',
|
||||||
_quantites[product.id!] = quantity - 1;
|
style: TextStyle(
|
||||||
});
|
fontSize: 11,
|
||||||
}
|
color: Colors.grey.shade600,
|
||||||
},
|
fontFamily: 'monospace',
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
quantity.toString(),
|
if (product.reference != null && product.reference!.isNotEmpty)
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
Text(
|
||||||
),
|
'Réf: ${product.reference}',
|
||||||
IconButton(
|
style: TextStyle(
|
||||||
icon: const Icon(Icons.add, size: 18),
|
fontSize: 11,
|
||||||
onPressed: () {
|
color: Colors.grey.shade600,
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
trailing: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isOutOfStock
|
||||||
|
? Colors.grey.shade100
|
||||||
|
: Colors.blue.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.remove, size: 18),
|
||||||
|
onPressed: isOutOfStock ? null : () {
|
||||||
|
if (quantity > 0) {
|
||||||
|
setState(() {
|
||||||
|
_quantites[product.id!] = quantity - 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
quantity.toString(),
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.add, size: 18),
|
||||||
|
onPressed: isOutOfStock ? null : () {
|
||||||
|
if (product.stock == null || quantity < product.stock!) {
|
||||||
|
setState(() {
|
||||||
|
_quantites[product.id!] = quantity + 1;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
'Stock insuffisant',
|
||||||
|
'Quantité demandée non disponible',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -537,9 +776,9 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
child: const Icon(Icons.shopping_bag, size: 20),
|
child: const Icon(Icons.shopping_bag, size: 20),
|
||||||
),
|
),
|
||||||
title: Text(product.name),
|
title: Text(product.name),
|
||||||
subtitle: Text('${entry.value} x ${product.price.toStringAsFixed(2)} DA'),
|
subtitle: Text('${entry.value} x ${product.price.toStringAsFixed(2)} MGA'),
|
||||||
trailing: Text(
|
trailing: Text(
|
||||||
'${(entry.value * product.price).toStringAsFixed(2)} DA',
|
'${(entry.value * product.price).toStringAsFixed(2)} MGA',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.blue.shade800,
|
color: Colors.blue.shade800,
|
||||||
@ -569,7 +808,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${total.toStringAsFixed(2)} DA',
|
'${total.toStringAsFixed(2)} MGA',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -619,23 +858,7 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _submitOrder() async {
|
Future<void> _submitOrder() async {
|
||||||
if (_nomController.text.isEmpty ||
|
// Vérifier d'abord si le panier est vide
|
||||||
_prenomController.text.isEmpty ||
|
|
||||||
_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;
|
|
||||||
}
|
|
||||||
|
|
||||||
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList();
|
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList();
|
||||||
if (itemsInCart.isEmpty) {
|
if (itemsInCart.isEmpty) {
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
@ -645,6 +868,24 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
colorText: Colors.white,
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -692,14 +933,12 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
try {
|
try {
|
||||||
await _appDatabase.createCommandeComplete(client, commande, details);
|
await _appDatabase.createCommandeComplete(client, commande, details);
|
||||||
|
|
||||||
Get.back(); // Ferme le bottom sheet
|
|
||||||
|
|
||||||
// Afficher le dialogue de confirmation
|
// Afficher le dialogue de confirmation
|
||||||
await showDialog(
|
await showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('Commande Validée'),
|
title: const Text('Commande Validée'),
|
||||||
content: const Text('Votre commande a été enregistrée avec succès.'),
|
content: const Text('Votre commande a été enregistrée et expédiée avec succès.'),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@ -714,6 +953,8 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
_quantites.clear();
|
_quantites.clear();
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
// Recharger les produits pour mettre à jour le stock
|
||||||
|
_loadProducts();
|
||||||
},
|
},
|
||||||
child: const Text('OK'),
|
child: const Text('OK'),
|
||||||
),
|
),
|
||||||
@ -743,6 +984,12 @@ class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|||||||
_emailController.dispose();
|
_emailController.dispose();
|
||||||
_telephoneController.dispose();
|
_telephoneController.dispose();
|
||||||
_adresseController.dispose();
|
_adresseController.dispose();
|
||||||
|
|
||||||
|
// Disposal des contrôleurs de filtre
|
||||||
|
_searchNameController.dispose();
|
||||||
|
_searchImeiController.dispose();
|
||||||
|
_searchReferenceController.dispose();
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:youmazgestion/Models/users.dart';
|
import 'package:youmazgestion/Models/users.dart';
|
||||||
import 'package:youmazgestion/Models/role.dart';
|
import 'package:youmazgestion/Models/role.dart';
|
||||||
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
||||||
|
import 'package:youmazgestion/Views/Dashboard.dart';
|
||||||
import 'package:youmazgestion/accueil.dart';
|
import 'package:youmazgestion/accueil.dart';
|
||||||
|
|
||||||
//import '../Services/app_database.dart'; // Changé de authDatabase.dart
|
//import '../Services/app_database.dart'; // Changé de authDatabase.dart
|
||||||
@ -215,7 +216,7 @@ Future<void> _loadPointsDeVente() async {
|
|||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => const AccueilPage()),
|
MaterialPageRoute(builder: (context) => DashboardPage()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text('OK'),
|
child: const Text('OK'),
|
||||||
@ -416,7 +417,7 @@ _isLoadingPointsDeVente
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Icons.store, size: 20),
|
const Icon(Icons.store, size: 20),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(point['designation'] as String),
|
Text(point['nom']),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -18,6 +18,7 @@ void main() async {
|
|||||||
// await ProductDatabase.instance.initDatabase();
|
// await ProductDatabase.instance.initDatabase();
|
||||||
await AppDatabase.instance.initDatabase();
|
await AppDatabase.instance.initDatabase();
|
||||||
|
|
||||||
|
|
||||||
// Afficher les informations de la base (pour debug)
|
// Afficher les informations de la base (pour debug)
|
||||||
// await AppDatabase.instance.printDatabaseInfo();
|
// await AppDatabase.instance.printDatabaseInfo();
|
||||||
Get.put(
|
Get.put(
|
||||||
|
|||||||
@ -640,6 +640,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
numbers_to_letters:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: numbers_to_letters
|
||||||
|
sha256: "70c7ed2f04c1982a299e753101fbc2d52ed5b39a2b3dd2a9c07ba131e9c0948e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
open_file:
|
open_file:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -64,6 +64,7 @@ dependencies:
|
|||||||
excel: ^2.0.1
|
excel: ^2.0.1
|
||||||
mobile_scanner: ^5.0.0 # ou la version la plus récente
|
mobile_scanner: ^5.0.0 # ou la version la plus récente
|
||||||
fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons
|
fl_chart: ^0.65.0 # Version la plus récente au moment de cette répons
|
||||||
|
numbers_to_letters: ^1.0.0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -105,6 +106,8 @@ flutter:
|
|||||||
- assets/airtel_money.png
|
- assets/airtel_money.png
|
||||||
- assets/mvola.jpg
|
- assets/mvola.jpg
|
||||||
- assets/Orange_money.png
|
- assets/Orange_money.png
|
||||||
|
- assets/fa-solid-900.ttf
|
||||||
|
- assets/fonts/Roboto-Italic.ttf
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user