Browse Source

last commit excel gestion mety

31052025_01
b.razafimandimbihery 6 months ago
parent
commit
1aceb5669a
  1. 27
      lib/Components/appDrawer.dart
  2. 55
      lib/Components/app_bar.dart
  3. 32
      lib/Models/produit.dart
  4. 22
      lib/Services/productDatabase.dart
  5. 63
      lib/Services/workDatabase.dart
  6. 1532
      lib/Views/HandleProduct.dart
  7. 711
      lib/Views/addProduct.dart
  8. 116
      lib/Views/gestionProduct.dart
  9. 2
      lib/Views/produitsCard.dart
  10. 294
      lib/accueil.dart
  11. 8
      pubspec.lock
  12. 3
      pubspec.yaml

27
lib/Components/appDrawer.dart

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:youmazgestion/Views/HandleProduct.dart';
import 'package:youmazgestion/Views/RoleListPage.dart';
import 'package:youmazgestion/Views/historique.dart';
import 'package:youmazgestion/Views/addProduct.dart';
@ -113,11 +114,11 @@ class CustomDrawer extends StatelessWidget {
ListTile(
leading: const Icon(Icons.add),
iconColor: Colors.indigoAccent,
title: const Text("Ajouter un produit"),
title: const Text("Gestion des produit"),
onTap: () async {
bool hasPermission = await userController.hasPermission('create', '/ajouter-produit');
if (hasPermission) {
Get.to(const AddProductPage());
Get.to(() => const ProductManagementPage());
} else {
Get.snackbar(
"Accès refusé",
@ -131,27 +132,7 @@ class CustomDrawer extends StatelessWidget {
}
},
),
ListTile(
leading: const Icon(Icons.edit),
iconColor: Colors.redAccent,
title: const Text("Modifier/Supprimer un produit"),
onTap: () async {
bool hasPermission = await userController.hasPermission('update', '/modifier-produit');
if (hasPermission) {
Get.to(GestionProduit());
} else {
Get.snackbar(
"Accès refusé",
"Vous n'avez pas les droits pour modifier/supprimer un produit",
backgroundColor: Colors.red,
colorText: Colors.white,
icon: const Icon(Icons.error),
duration: const Duration(seconds: 3),
snackPosition: SnackPosition.TOP,
);
}
},
),
ListTile(
leading: const Icon(Icons.bar_chart),
title: const Text("Bilan"),

55
lib/Components/app_bar.dart

@ -2,53 +2,30 @@ import 'package:flutter/material.dart';
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final Widget? subtitle;
const CustomAppBar({Key? key, required this.title}) : super(key: key);
const CustomAppBar({
Key? key,
required this.title,
this.subtitle,
}) : super(key: key);
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 72.0);
@override
Widget build(BuildContext context) {
return AppBar(
backgroundColor: Colors.transparent,
elevation: 5,
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.white, const Color.fromARGB(255, 4, 54, 95)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Row(
children: [
const Spacer(),
Align(
alignment: Alignment.center,
child: Text(
title,
style: const TextStyle(
fontSize: 25,
color: Colors.white,
),
),
title: subtitle == null
? Text(title)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: TextStyle(fontSize: 20)),
subtitle!,
],
),
const Spacer(),
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Align(
alignment: Alignment.centerRight,
child: Image.asset(
'assets/youmaz2.png',
width: 100,
height: 50,
),
),
),
],
),
),
// autres propriétés si besoin
);
}
}

32
lib/Models/produit.dart

@ -1,26 +1,25 @@
class Product {
final int? id;
int? id;
final String name;
final double price;
String image;
final String? image;
final String category;
int? stock; // Paramètre optionnel pour le stock
String? description; // Nouveau champ
String? qrCode; // Nouveau champ
String? reference;
final int? stock;
final String? description;
String? qrCode;
final String? reference;
Product({
this.id,
required this.name,
required this.price,
required this.image,
this.image,
required this.category,
this.stock = 0,
this.description,
this.description = '',
this.qrCode,
this.reference,
});
// Vérifie si le stock est défini
bool isStockDefined() {
if (stock != null) {
@ -30,18 +29,17 @@ class Product {
return false;
}
}
Map<String, dynamic> toMap() {
return {
'id': id,
'name': name,
'price': price,
'image': image,
'image': image ?? '',
'category': category,
'stock': stock,
'description': description,
'qrCode': qrCode,
'reference':reference
'stock': stock ?? 0,
'description': description ?? '',
'qrCode': qrCode ?? '',
'reference': reference ?? '',
};
}
@ -55,7 +53,7 @@ class Product {
stock: map['stock'],
description: map['description'],
qrCode: map['qrCode'],
reference:map['reference']
reference: map['reference'],
);
}
}
}

22
lib/Services/productDatabase.dart

@ -42,7 +42,7 @@ class ProductDatabase {
return await databaseFactoryFfi.openDatabase(path);
}
Future<void> _createDB(Database db, int version) async {
Future<void> _createDB(Database db, int version) async {
// Récupère la liste des colonnes de la table "products"
final tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'");
final tableNames = tables.map((row) => row['name'] as String).toList();
@ -58,13 +58,13 @@ class ProductDatabase {
category TEXT,
stock INTEGER,
description TEXT,
qrCode TEXT
qrCode TEXT,
reference TEXT
)
''');
print("Table 'products' créée avec toutes les colonnes.");
} else {
// Vérifie si les colonnes "description" et "qrCode" existent déjà
// Vérifie si les colonnes "description", "qrCode" et "reference" existent déjà
final columns = await db.rawQuery('PRAGMA table_info(products)');
final columnNames = columns.map((e) => e['name'] as String).toList();
@ -87,6 +87,7 @@ class ProductDatabase {
print("Erreur ajout colonne qrCode : $e");
}
}
// Ajoute la colonne "reference" si elle n'existe pas
if (!columnNames.contains('reference')) {
try {
@ -153,4 +154,19 @@ class ProductDatabase {
return await db
.rawUpdate('UPDATE products SET stock = ? WHERE id = ?', [stock, id]);
}
// Ajouter cette méthode dans la classe ProductDatabase
Future<Product?> getProductByReference(String reference) async {
final db = await database;
final maps = await db.query(
'products',
where: 'reference = ?',
whereArgs: [reference],
);
if (maps.isNotEmpty) {
return Product.fromMap(maps.first);
}
return null;
}
}

63
lib/Services/workDatabase.dart

@ -8,40 +8,45 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart';
class WorkDatabase {
static final WorkDatabase instance = WorkDatabase._init();
late Database _database;
static Database? _database;
bool _isInitialized = false;
WorkDatabase._init() {
sqflite_ffi.sqfliteFfiInit();
}
Future<void> initDatabase() async {
_database = await _initDB('work.db');
await _createDB(_database, 1);
if (!_isInitialized) {
_database = await _initDB('work.db');
await _createDB(_database!, 1);
_isInitialized = true;
}
}
Future<Database> get database async {
if (_database.isOpen) return _database;
_database = await _initDB('work.db');
return _database;
if (!_isInitialized) {
await initDatabase();
}
return _database!;
}
Future<Database> _initDB(String filePath) async {
// Obtenez le répertoire de stockage local de l'application
final documentsDirectory = await getApplicationDocumentsDirectory();
final path = join(documentsDirectory.path, filePath);
// Vérifiez si le fichier de base de données existe déjà dans le répertoire de stockage local
bool dbExists = await File(path).exists();
if (!dbExists) {
// Si le fichier n'existe pas, copiez-le depuis le dossier assets/database
ByteData data = await rootBundle.load('assets/database/$filePath');
List<int> bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await File(path).writeAsBytes(bytes);
try {
ByteData data = await rootBundle.load('assets/database/$filePath');
List<int> bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
await File(path).writeAsBytes(bytes);
} catch (e) {
print("No pre-existing database found in assets, creating new one");
}
}
// Ouvrez la base de données
return await databaseFactoryFfi.openDatabase(path);
}
@ -49,42 +54,30 @@ class WorkDatabase {
await db.execute('''
CREATE TABLE IF NOT EXISTS work (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT
date TEXT UNIQUE
)
''');
}
Future<int> insertDate(String date) async {
final db = await database;
final existingDates =
await db.query('work', where: 'date = ?', whereArgs: [date]);
if (existingDates.isNotEmpty) {
// Date already exists, return 0 to indicate no new insertion
try {
return await db.insert('work', {'date': date});
} catch (e) {
// En cas de doublon (date déjà existante)
return 0;
}
return await db.insert('work', {'date': date});
}
/*Future<List<Work>> getDates() async {
final db = await database;
final result = await db.query('work');
return result.map((json) => Work.fromJson(json)).toList();
}*/
Future<List<String>> getDates() async {
final db = await database;
final result = await db.query('work');
return List.generate(
result.length, (index) => result[index]['date'] as String);
return result.map((row) => row['date'] as String).toList();
}
// recuperer les dates par ordre du plus recent au plus ancien
Future<List<String>> getDatesDesc() async {
final db = await database;
final result = await db.query('work', orderBy: 'date DESC');
return List.generate(
result.length, (index) => result[index]['date'] as String);
return result.map((row) => row['date'] as String).toList();
}
}
}

1532
lib/Views/HandleProduct.dart

File diff suppressed because it is too large

711
lib/Views/addProduct.dart

@ -1,10 +1,13 @@
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:file_picker/file_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:excel/excel.dart' hide Border;
import 'package:flutter/services.dart';
import '../Components/appDrawer.dart';
import '../Components/app_bar.dart';
@ -23,13 +26,20 @@ class _AddProductPageState extends State<AddProductPage> {
final TextEditingController _priceController = TextEditingController();
final TextEditingController _imageController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
final TextEditingController _stockController = TextEditingController();
final List<String> _categories = ['Sucré', 'Salé', 'Jus', 'Gateaux'];
final List<String> _categories = ['Sucré', 'Salé', 'Jus', 'Gateaux', 'Non catégorisé'];
String? _selectedCategory;
File? _pickedImage;
String? _qrData;
String? _currentReference; // Ajout pour stocker la référence actuelle
late ProductDatabase _productDatabase;
// Variables pour la barre de progression
bool _isImporting = false;
double _importProgress = 0.0;
String _importStatusText = '';
@override
void initState() {
super.initState();
@ -45,18 +55,35 @@ class _AddProductPageState extends State<AddProductPage> {
_priceController.dispose();
_imageController.dispose();
_descriptionController.dispose();
_stockController.dispose();
super.dispose();
}
// Méthode pour générer une référence unique
String _generateUniqueReference() {
final timestamp = DateTime.now().millisecondsSinceEpoch;
final randomSuffix = DateTime.now().microsecond.toString().padLeft(6, '0');
return 'PROD_${timestamp}${randomSuffix}';
}
void _updateQrData() {
if (_nameController.text.isNotEmpty) {
final reference = 'PROD_PREVIEW_${_nameController.text}_${DateTime.now().millisecondsSinceEpoch}';
setState(() {
_qrData = 'https://tonsite.com/$reference';
});
if (_nameController.text.isNotEmpty) {
// Générer une nouvelle référence si elle n'existe pas encore
if (_currentReference == null) {
_currentReference = _generateUniqueReference();
}
setState(() {
// Utiliser la référence courante dans l'URL du QR code
_qrData = 'https://stock.guycom.mg/$_currentReference';
});
} else {
setState(() {
_currentReference = null;
_qrData = null;
});
}
}
Future<void> _selectImage() async {
final result = await FilePicker.platform.pickFiles(type: FileType.image);
if (result != null && result.files.single.path != null) {
@ -67,69 +94,488 @@ class _AddProductPageState extends State<AddProductPage> {
}
}
Future<String> _generateAndSaveQRCode(String reference) async {
final validation = QrValidator.validate(
data: 'https://tonsite.com/$reference',
version: QrVersions.auto,
errorCorrectionLevel: QrErrorCorrectLevel.L,
);
// Assurez-vous aussi que _generateAndSaveQRCode utilise bien la référence passée :
Future<String> _generateAndSaveQRCode(String reference) async {
final qrUrl = 'https://stock.guycom.mg/$reference'; // Utilise le paramètre reference
final validation = QrValidator.validate(
data: qrUrl,
version: QrVersions.auto,
errorCorrectionLevel: QrErrorCorrectLevel.L,
);
final qrCode = validation.qrCode!;
final painter = QrPainter.withQr(
qr: qrCode,
color: Colors.black,
emptyColor: Colors.white,
gapless: true,
);
if (validation.status != QrValidationStatus.valid) {
throw Exception('Données QR invalides: ${validation.error}');
}
final directory = await getApplicationDocumentsDirectory();
final path = '${directory.path}/$reference.png';
final picData = await painter.toImageData(2048, format: ImageByteFormat.png);
await File(path).writeAsBytes(picData!.buffer.asUint8List());
final qrCode = validation.qrCode!;
final painter = QrPainter.withQr(
qr: qrCode,
color: Colors.black,
emptyColor: Colors.white,
gapless: true,
);
return path;
final directory = await getApplicationDocumentsDirectory();
final path = '${directory.path}/$reference.png'; // Utilise le paramètre reference
try {
final picData = await painter.toImageData(2048, format: ImageByteFormat.png);
if (picData != null) {
await File(path).writeAsBytes(picData.buffer.asUint8List());
} else {
throw Exception('Impossible de générer l\'image QR');
}
} catch (e) {
throw Exception('Erreur lors de la génération du QR code: $e');
}
return path;
}
void _addProduct() async {
final name = _nameController.text.trim();
final price = double.tryParse(_priceController.text.trim()) ?? 0.0;
final image = _imageController.text.trim();
final category = _selectedCategory;
final description = _descriptionController.text.trim();
if (name.isEmpty || price <= 0 || image.isEmpty || category == null) {
Get.snackbar('Erreur', 'Veuillez remplir tous les champs requis');
return;
}
final name = _nameController.text.trim();
final price = double.tryParse(_priceController.text.trim()) ?? 0.0;
final image = _imageController.text.trim();
final category = _selectedCategory ?? 'Non catégorisé';
final description = _descriptionController.text.trim();
final stock = int.tryParse(_stockController.text.trim()) ?? 0;
final reference = 'PROD_${DateTime.now().millisecondsSinceEpoch}';
final qrPath = await _generateAndSaveQRCode(reference);
final product = Product(
name: name,
price: price,
image: image,
category: category,
description: description,
qrCode: qrPath,
reference: reference,
);
if (name.isEmpty || price <= 0) {
Get.snackbar('Erreur', 'Nom et prix sont obligatoires');
return;
}
// Utiliser la référence générée ou en créer une nouvelle
String finalReference = _currentReference ?? _generateUniqueReference();
// Vérifier l'unicité de la référence en base
var existingProduct = await _productDatabase.getProductByReference(finalReference);
// Si la référence existe déjà, en générer une nouvelle
while (existingProduct != null) {
finalReference = _generateUniqueReference();
existingProduct = await _productDatabase.getProductByReference(finalReference);
}
// Mettre à jour la référence courante avec la référence finale
_currentReference = finalReference;
// Générer le QR code avec la référence finale
final qrPath = await _generateAndSaveQRCode(finalReference);
final product = Product(
name: name,
price: price,
image: image,
category: category,
description: description,
qrCode: qrPath,
reference: finalReference, // Utiliser la référence finale
stock: stock,
);
try {
await _productDatabase.createProduct(product);
Get.snackbar('Succès', 'Produit ajouté avec succès\nRéférence: $finalReference');
setState(() {
_nameController.clear();
_priceController.clear();
_imageController.clear();
_descriptionController.clear();
_stockController.clear();
_selectedCategory = null;
_pickedImage = null;
_qrData = null;
_currentReference = null; // Reset de la référence
});
} catch (e) {
Get.snackbar('Erreur', 'Ajout du produit échoué : $e');
print(e);
}
}
// Méthode pour réinitialiser l'état d'importation
void _resetImportState() {
setState(() {
_isImporting = false;
_importProgress = 0.0;
_importStatusText = '';
});
}
Future<void> _importFromExcel() async {
try {
await _productDatabase.createProduct(product);
Get.snackbar('Succès', 'Produit ajouté avec succès');
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['xlsx', 'xls','csv'],
allowMultiple: false,
);
if (result == null || result.files.isEmpty) {
Get.snackbar('Annulé', 'Aucun fichier sélectionné');
return;
}
// Démarrer la progression
setState(() {
_isImporting = true;
_importProgress = 0.0;
_importStatusText = 'Lecture du fichier...';
});
final file = File(result.files.single.path!);
// Vérifier que le fichier existe
if (!await file.exists()) {
_resetImportState();
Get.snackbar('Erreur', 'Le fichier sélectionné n\'existe pas');
return;
}
setState(() {
_importProgress = 0.1;
_importStatusText = 'Vérification du fichier...';
});
final bytes = await file.readAsBytes();
// Vérifier que le fichier n'est pas vide
if (bytes.isEmpty) {
_resetImportState();
Get.snackbar('Erreur', 'Le fichier Excel est vide');
return;
}
setState(() {
_importProgress = 0.2;
_importStatusText = 'Décodage du fichier Excel...';
});
Excel excel;
try {
// Initialisation
setState(() {
_isImporting = true;
_importProgress = 0.0;
_importStatusText = 'Initialisation...';
});
// Petit délai pour permettre au build de s'exécuter
await Future.delayed(Duration(milliseconds: 50));
excel = Excel.decodeBytes(bytes);
} catch (e) {
_resetImportState();
debugPrint('Erreur décodage Excel: $e');
if (e.toString().contains('styles') || e.toString().contains('Damaged')) {
_showExcelCompatibilityError();
return;
} else {
Get.snackbar('Erreur', 'Impossible de lire le fichier Excel. Format non supporté.');
return;
}
}
if (excel.tables.isEmpty) {
_resetImportState();
Get.snackbar('Erreur', 'Le fichier Excel ne contient aucune feuille');
return;
}
setState(() {
_nameController.clear();
_priceController.clear();
_imageController.clear();
_descriptionController.clear();
_selectedCategory = null;
_pickedImage = null;
_qrData = null;
_importProgress = 0.3;
_importStatusText = 'Analyse des données...';
});
int successCount = 0;
int errorCount = 0;
List<String> errorMessages = [];
// Prendre la première feuille disponible
final sheetName = excel.tables.keys.first;
final sheet = excel.tables[sheetName]!;
if (sheet.rows.isEmpty) {
_resetImportState();
Get.snackbar('Erreur', 'La feuille Excel est vide');
return;
}
final totalRows = sheet.rows.length - 1; // -1 pour exclure l'en-tête
setState(() {
_importStatusText = 'Importation en cours... (0/$totalRows)';
});
// Ignorer la première ligne (en-têtes) et traiter les données
for (var i = 1; i < sheet.rows.length; i++) {
try {
// Mettre à jour la progression
final currentProgress = 0.3 + (0.6 * (i - 1) / totalRows);
setState(() {
_importProgress = currentProgress;
_importStatusText = 'Importation en cours... (${i - 1}/$totalRows)';
});
// Petite pause pour permettre à l'UI de se mettre à jour
await Future.delayed(const Duration(milliseconds: 10));
final row = sheet.rows[i];
// Vérifier que la ligne a au moins les colonnes obligatoires (nom et prix)
if (row.isEmpty || row.length < 2) {
errorCount++;
errorMessages.add('Ligne ${i + 1}: Données insuffisantes');
continue;
}
// Extraire les valeurs avec vérifications sécurisées
final nameCell = row[0];
final priceCell = row[1];
// Extraction sécurisée des valeurs
String? nameValue;
String? priceValue;
if (nameCell?.value != null) {
nameValue = nameCell!.value.toString().trim();
}
if (priceCell?.value != null) {
priceValue = priceCell!.value.toString().trim();
}
if (nameValue == null || nameValue.isEmpty) {
errorCount++;
errorMessages.add('Ligne ${i + 1}: Nom du produit manquant');
continue;
}
if (priceValue == null || priceValue.isEmpty) {
errorCount++;
errorMessages.add('Ligne ${i + 1}: Prix manquant');
continue;
}
final name = nameValue;
// Remplacer les virgules par des points pour les décimaux
final price = double.tryParse(priceValue.replaceAll(',', '.'));
if (price == null || price <= 0) {
errorCount++;
errorMessages.add('Ligne ${i + 1}: Prix invalide ($priceValue)');
continue;
}
// Extraire les autres colonnes optionnelles de manière sécurisée
String category = 'Non catégorisé';
if (row.length > 2 && row[2]?.value != null) {
final categoryValue = row[2]!.value.toString().trim();
if (categoryValue.isNotEmpty) {
category = categoryValue;
}
}
String description = '';
if (row.length > 3 && row[3]?.value != null) {
description = row[3]!.value.toString().trim();
}
int stock = 0;
if (row.length > 4 && row[4]?.value != null) {
final stockStr = row[4]!.value.toString().trim();
stock = int.tryParse(stockStr) ?? 0;
}
// Générer une référence unique et vérifier son unicité
String reference = _generateUniqueReference();
// Vérifier l'unicité en base de données
var existingProduct = await _productDatabase.getProductByReference(reference);
while (existingProduct != null) {
reference = _generateUniqueReference();
existingProduct = await _productDatabase.getProductByReference(reference);
}
// Créer le produit
final product = Product(
name: name,
price: price,
image: '', // Pas d'image lors de l'import
category: category,
description: description,
stock: stock,
qrCode: '', // Sera généré après
reference: reference,
);
// Générer et sauvegarder le QR code avec la nouvelle URL
setState(() {
_importStatusText = 'Génération QR Code... (${i - 1}/$totalRows)';
});
final qrPath = await _generateAndSaveQRCode(reference);
product.qrCode = qrPath;
// Sauvegarder en base de données
await _productDatabase.createProduct(product);
successCount++;
} catch (e) {
errorCount++;
errorMessages.add('Ligne ${i + 1}: Erreur de traitement - $e');
debugPrint('Erreur ligne ${i + 1}: $e');
}
}
// Finalisation
setState(() {
_importProgress = 1.0;
_importStatusText = 'Finalisation...';
});
await Future.delayed(const Duration(milliseconds: 500));
// Réinitialiser l'état d'importation
_resetImportState();
// Afficher le résultat
String message = '$successCount produits importés avec succès';
if (errorCount > 0) {
message += ', $errorCount erreurs';
// Afficher les détails des erreurs si pas trop nombreuses
if (errorMessages.length <= 5) {
message += ':\n${errorMessages.join('\n')}';
}
}
Get.snackbar(
'Importation terminée',
message,
duration: const Duration(seconds: 6),
colorText: Colors.white,
backgroundColor: successCount > 0 ? Colors.green : Colors.orange,
);
} catch (e) {
Get.snackbar('Erreur', 'Ajout du produit échoué : $e');
_resetImportState();
Get.snackbar('Erreur', 'Erreur lors de l\'importation Excel: $e');
debugPrint('Erreur générale import Excel: $e');
}
}
void _showExcelCompatibilityError() {
Get.dialog(
AlertDialog(
title: const Text('Fichier Excel incompatible'),
content: const Text(
'Ce fichier Excel contient des éléments qui ne sont pas compatibles avec notre système d\'importation.\n\n'
'Solutions recommandées :\n'
'• Téléchargez notre modèle Excel et copiez-y vos données\n'
'• Ou exportez votre fichier en format simple: Classeur Excel .xlsx depuis Excel\n'
'• Ou créez un nouveau fichier Excel simple sans formatage complexe'
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Annuler'),
),
TextButton(
onPressed: () {
Get.back();
_downloadExcelTemplate();
},
child: const Text('Télécharger modèle'),
style: TextButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
],
),
);
}
Future<void> _downloadExcelTemplate() async {
try {
// Créer un fichier Excel temporaire comme modèle
final excel = Excel.createExcel();
// Supprimer la feuille par défaut et créer une nouvelle
excel.delete('Sheet1');
excel.copy('Sheet1', 'Produits');
excel.delete('Sheet1');
final sheet = excel['Produits'];
// Ajouter les en-têtes avec du style
final headers = ['Nom', 'Prix', 'Catégorie', 'Description', 'Stock'];
for (int i = 0; i < headers.length; i++) {
final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: i, rowIndex: 0));
cell.value = headers[i];
cell.cellStyle = CellStyle(
bold: true,
backgroundColorHex: '#E8F4FD',
);
}
// Ajouter des exemples
final examples = [
['Croissant', '1.50', 'Sucré', 'Délicieux croissant beurré', '20'],
['Sandwich jambon', '4.00', 'Salé', 'Sandwich fait maison', '15'],
['Jus d\'orange', '2.50', 'Jus', 'Jus d\'orange frais', '30'],
['Gâteau chocolat', '18.00', 'Gateaux', 'Gâteau au chocolat portion 8 personnes', '5'],
];
for (int row = 0; row < examples.length; row++) {
for (int col = 0; col < examples[row].length; col++) {
final cell = sheet.cell(CellIndex.indexByColumnRow(columnIndex: col, rowIndex: row + 1));
cell.value = examples[row][col];
}
}
// Ajuster la largeur des colonnes
sheet.setColWidth(0, 20); // Nom
sheet.setColWidth(1, 10); // Prix
sheet.setColWidth(2, 15); // Catégorie
sheet.setColWidth(3, 30); // Description
sheet.setColWidth(4, 10); // Stock
// Sauvegarder en mémoire
final bytes = excel.save();
if (bytes == null) {
Get.snackbar('Erreur', 'Impossible de créer le fichier modèle');
return;
}
// Demander sauvegarder
final String? outputFile = await FilePicker.platform.saveFile(
fileName: 'modele_import_produits.xlsx',
allowedExtensions: ['xlsx'],
type: FileType.custom,
);
if (outputFile != null) {
try {
await File(outputFile).writeAsBytes(bytes);
Get.snackbar(
'Succès',
'Modèle téléchargé avec succès\n$outputFile',
duration: const Duration(seconds: 4),
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
Get.snackbar('Erreur', 'Impossible d\'écrire le fichier: $e');
}
}
} catch (e) {
Get.snackbar('Erreur', 'Erreur lors de la création du modèle: $e');
debugPrint('Erreur création modèle Excel: $e');
}
}
@ -148,12 +594,18 @@ class _AddProductPageState extends State<AddProductPage> {
return Container(
width: 100,
height: 100,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.image, size: 32, color: Colors.grey),
Text('Aucune image', style: TextStyle(color: Colors.grey)),
],
),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8.0),
),
child: const Icon(Icons.image, size: 48, color: Colors.grey),
);
));
}
}
@ -169,49 +621,161 @@ class _AddProductPageState extends State<AddProductPage> {
children: [
const Text('Ajouter un produit', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
// Boutons d'importation
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _isImporting ? null : _importFromExcel,
icon: const Icon(Icons.upload),
label: const Text('Importer depuis Excel'),
),
),
const SizedBox(width: 10),
TextButton(
onPressed: _isImporting ? null : _downloadExcelTemplate,
child: const Text('Modèle'),
),
],
),
const SizedBox(height: 16),
// Barre de progression
if (_isImporting) ...[
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Importation en cours...',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: _importProgress,
backgroundColor: Colors.blue.shade100,
valueColor: AlwaysStoppedAnimation<Color>(Colors.blue.shade600),
),
const SizedBox(height: 8),
Text(
_importStatusText,
style: TextStyle(
fontSize: 14,
color: Colors.blue.shade700,
),
),
const SizedBox(height: 8),
Text(
'${(_importProgress * 100).round()}%',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.blue.shade600,
),
),
],
),
),
const SizedBox(height: 16),
],
const Divider(),
const SizedBox(height: 16),
// Formulaire d'ajout manuel
TextField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Nom du produit', border: OutlineInputBorder()),
enabled: !_isImporting,
decoration: const InputDecoration(
labelText: 'Nom du produit*',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _priceController,
enabled: !_isImporting,
keyboardType: TextInputType.numberWithOptions(decimal: true),
decoration: const InputDecoration(labelText: 'Prix', border: OutlineInputBorder()),
decoration: const InputDecoration(
labelText: 'Prix*',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _stockController,
enabled: !_isImporting,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Stock',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// Section image (optionnelle)
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: TextField(
controller: _imageController,
decoration: const InputDecoration(labelText: 'Chemin de l\'image', border: OutlineInputBorder()),
enabled: !_isImporting,
decoration: const InputDecoration(
labelText: 'Chemin de l\'image (optionnel)',
border: OutlineInputBorder(),
),
readOnly: true,
),
),
const SizedBox(width: 8),
ElevatedButton(onPressed: _selectImage, child: const Text('Sélectionner')),
ElevatedButton(
onPressed: _isImporting ? null : _selectImage,
child: const Text('Sélectionner'),
),
],
),
const SizedBox(height: 16),
_displayImage(),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: _selectedCategory,
items: _categories
.map((c) => DropdownMenuItem(value: c, child: Text(c)))
.toList(),
onChanged: (value) => setState(() => _selectedCategory = value),
decoration: const InputDecoration(labelText: 'Catégorie', border: OutlineInputBorder()),
onChanged: _isImporting ? null : (value) => setState(() => _selectedCategory = value),
decoration: const InputDecoration(
labelText: 'Catégorie',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _descriptionController,
enabled: !_isImporting,
maxLines: 3,
decoration: const InputDecoration(labelText: 'Description', border: OutlineInputBorder()),
decoration: const InputDecoration(
labelText: 'Description (optionnel)',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
if (_qrData != null) ...[
const Text('Aperçu du QR Code :'),
const SizedBox(height: 8),
@ -222,12 +786,21 @@ class _AddProductPageState extends State<AddProductPage> {
size: 120,
),
),
const SizedBox(height: 8),
Center(
child: Text(
_qrData!,
style: const TextStyle(fontSize: 12, color: Colors.grey),
textAlign: TextAlign.center,
),
),
],
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _addProduct,
onPressed: _isImporting ? null : _addProduct,
child: const Text('Ajouter le produit'),
),
),
@ -236,4 +809,4 @@ class _AddProductPageState extends State<AddProductPage> {
),
);
}
}
}

116
lib/Views/gestionProduct.dart

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import '../Components/appDrawer.dart';
import '../Models/produit.dart';
import '../Services/productDatabase.dart';
@ -11,7 +10,7 @@ import 'dart:io';
class GestionProduit extends StatelessWidget {
final ProductDatabase _productDatabase = ProductDatabase.instance;
GestionProduit({super.key});
GestionProduit({super.key});
@override
Widget build(BuildContext context) {
@ -19,28 +18,22 @@ class GestionProduit extends StatelessWidget {
return Scaffold(
appBar: const CustomAppBar(title: 'Gestion des produits'),
drawer: CustomDrawer(),
drawer: CustomDrawer(),
body: FutureBuilder<List<Product>>(
future: _productDatabase.getProducts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return const Center(
child: Text('Une erreur s\'est produite'),
);
return const Center(child: Text('Une erreur s\'est produite'));
}
final products = snapshot.data;
if (products == null || products.isEmpty) {
return const Center(
child: Text('Aucun produit disponible'),
);
return const Center(child: Text('Aucun produit disponible'));
}
return ListView.builder(
@ -52,68 +45,30 @@ class GestionProduit extends StatelessWidget {
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: Colors.grey,
width: 1.0,
),
border: Border.all(color: Colors.grey, width: 1.0),
),
child: ListTile(
leading: CircleAvatar(
backgroundImage: product.image != null
? FileImage(File(product.image)) as ImageProvider<
Object> // Charger l'image à partir du chemin d'accès
: const AssetImage(
'assets/placeholder_image.png'), // Image de substitution si le chemin d'accès est vide
),
leading: _buildProductImage(product.image),
title: Text(product.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Price: \$${product.price.toStringAsFixed(2)}'),
Text('Category: ${product.category}'),
Text('Prix: \$${product.price.toStringAsFixed(2)}'),
Text('Catégorie: ${product.category}'),
if (product.stock != null)
Text('Stock: ${product.stock}'),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {
_productDatabase
.deleteProduct(product.id)
.then((value) {
Get.snackbar(
'Produit supprimé',
'Le produit a été supprimé avec succès',
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 3),
backgroundColor: Colors.green,
colorText: Colors.white,
);
});
},
onPressed: () => _deleteProduct(product.id),
icon: const Icon(Icons.delete),
color: Colors.red,
),
IconButton(
onPressed: () {
Get.to(EditProductPage(product: product))
?.then((result) {
if (result != null && result is Product) {
_productDatabase
.updateProduct(result)
.then((value) {
Get.snackbar(
'Produit mis à jour',
'Le produit a été mis à jour avec succès',
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 3),
backgroundColor: Colors.green,
colorText: Colors.white,
);
});
}
});
},
onPressed: () => _editProduct(product),
icon: const Icon(Icons.edit),
color: Colors.blue,
),
@ -127,4 +82,47 @@ class GestionProduit extends StatelessWidget {
),
);
}
}
Widget _buildProductImage(String? imagePath) {
if (imagePath != null && imagePath.isNotEmpty && File(imagePath).existsSync()) {
return CircleAvatar(
backgroundImage: FileImage(File(imagePath)),
);
} else {
return const CircleAvatar(
backgroundColor: Colors.grey,
child: Icon(Icons.shopping_bag, color: Colors.white),
);
}
}
void _deleteProduct(int? id) {
_productDatabase.deleteProduct(id).then((value) {
Get.snackbar(
'Produit supprimé',
'Le produit a été supprimé avec succès',
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 3),
backgroundColor: Colors.green,
colorText: Colors.white,
);
});
}
void _editProduct(Product product) {
Get.to(() => EditProductPage(product: product))?.then((result) {
if (result != null && result is Product) {
_productDatabase.updateProduct(result).then((value) {
Get.snackbar(
'Produit mis à jour',
'Le produit a été mis à jour avec succès',
snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 3),
backgroundColor: Colors.green,
colorText: Colors.white,
);
});
}
});
}
}

2
lib/Views/produitsCard.dart

@ -114,7 +114,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
),
child: widget.product.image != null
? Image.file(
File(widget.product.image),
File(widget.product.image!),
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return _buildPlaceholderImage();

294
lib/accueil.dart

@ -47,6 +47,30 @@ class _AccueilPageState extends State<AccueilPage> {
initwork();
loadUserData();
productsFuture = _initDatabaseAndFetchProducts();
_initializeRegister();
super.initState();
_initializeDatabases();
loadUserData();
productsFuture = _initDatabaseAndFetchProducts();
}
Future<void> _initializeDatabases() async {
await orderDatabase.initDatabase();
await workDatabase.initDatabase(); // Attendre l'initialisation complète
await _initializeRegister();
}
Future<void> _initializeRegister() async {
if (!MyApp.isRegisterOpen) {
setState(() {
MyApp.isRegisterOpen = true;
String formattedDate = DateFormat('yyyy-MM-dd').format(DateTime.now());
startDate = DateFormat('yyyy-MM-dd').parse(formattedDate);
MyApp.startDate = startDate;
workDatabase.insertDate(formattedDate);
});
}
}
Future<void> loadUserData() async {
@ -169,11 +193,16 @@ class _AccueilPageState extends State<AccueilPage> {
);
}
}
@override
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const CustomAppBar(title: "Accueil"),
appBar: CustomAppBar(
title: "Accueil",
subtitle: Text('Bienvenue $username ! (Rôle: $role)'),
),
drawer: CustomDrawer(),
body: ParticleBackground(
child: Container(
@ -181,8 +210,7 @@ class _AccueilPageState extends State<AccueilPage> {
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.white, Color.fromARGB(255, 4, 54, 95)],
),
colors: [Colors.white, Color.fromARGB(255, 4, 54, 95)]),
),
child: FutureBuilder<Map<String, List<Product>>>(
future: productsFuture,
@ -195,146 +223,176 @@ class _AccueilPageState extends State<AccueilPage> {
final productsByCategory = snapshot.data!;
final categories = productsByCategory.keys.toList();
if (!MyApp.isRegisterOpen) {
return Column(
children: [
Text('Bienvenue $username ! (Rôle: $role)'),
Center(
child: ElevatedButton(
onPressed: () {
setState(() {
MyApp.isRegisterOpen = true;
String formattedDate =
DateFormat('yyyy-MM-dd').format(DateTime.now());
startDate =
DateFormat('yyyy-MM-dd').parse(formattedDate);
MyApp.startDate = startDate;
workDatabase.insertDate(formattedDate);
});
},
child: const Text('Démarrer la caisse'),
),
),
],
);
} else {
return Row(
children: [
Expanded(
flex: 3,
child: ListView.builder(
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
final products = productsByCategory[category]!;
return Row(
children: [
Expanded(
flex: 3,
child: ListView.builder(
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
final products = productsByCategory[category]!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
category,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Text(
category,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
),
GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
childAspectRatio: 1,
),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ProductCard(
product: product,
onAddToCart: (product, quantity) {
addToCartWithDetails(product, quantity);
},
);
},
),
GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
childAspectRatio: 1,
),
],
);
},
),
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ProductCard(
product: product,
onAddToCart: (product, quantity) {
addToCartWithDetails(product, quantity);
},
);
},
),
],
);
},
),
Expanded(
flex: 1,
child: Container(
),
Expanded(
flex: 1,
child: Container(
decoration: BoxDecoration(
color: Colors.grey[200],
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Panier',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
bottomLeft: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
spreadRadius: 2,
),
],
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Panier',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
const SizedBox(height: 16),
Expanded(
child: selectedProducts.isEmpty
? const Center(
child: Text("Votre panier est vide"),
)
: ListView.builder(
itemCount: selectedProducts.length,
itemBuilder: (context, index) {
final cartItem = selectedProducts[index];
return ListTile(
title: Text(cartItem.product.name),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Expanded(
child: selectedProducts.isEmpty
? const Center(
child: Text(
"Votre panier est vide",
style: TextStyle(fontSize: 16),
),
)
: ListView.builder(
itemCount: selectedProducts.length,
itemBuilder: (context, index) {
final cartItem = selectedProducts[index];
return Card(
margin: const EdgeInsets.symmetric(
vertical: 4),
child: ListTile(
title: Text(
cartItem.product.name,
style: const TextStyle(
fontWeight: FontWeight.bold),
),
subtitle: Text(
'${cartItem.product.price} FCFA x ${cartItem.quantity}'),
'${NumberFormat('#,##0').format(cartItem.product.price)} FCFA x ${cartItem.quantity}',
style: const TextStyle(
fontSize: 14),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
icon: const Icon(Icons.delete,
color: Colors.red),
onPressed: () {
setState(() {
selectedProducts.removeAt(index);
selectedProducts
.removeAt(index);
});
},
),
);
},
),
),
Text(
'Total: ${calculateTotalPrice().toStringAsFixed(2)} FCFA',
),
);
},
),
),
const Divider(thickness: 1),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
'Total: ${NumberFormat('#,##0.00').format(calculateTotalPrice())} FCFA',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
TextField(
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: 'Montant payé',
),
TextField(
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'Montant payé',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
onChanged: (value) {
amountPaid = double.tryParse(value) ?? 0;
},
filled: true,
fillColor: Colors.white,
),
ElevatedButton(
onPressed: saveOrderToDatabase,
child: const Text('Valider la commande'),
onChanged: (value) {
amountPaid = double.tryParse(value) ?? 0;
},
),
const SizedBox(height: 16),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
],
),
onPressed: saveOrderToDatabase,
child: const Text(
'Valider la commande',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white),
),
),
],
),
),
],
);
}
),
],
);
} else {
return const Center(child: Text("Aucun produit disponible"));
}

8
pubspec.lock

@ -169,6 +169,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
excel:
dependency: "direct main"
description:
name: excel
sha256: f6a76fff6ac14f48fd44a6528e72705965e02cbc593e00427ab1d9a9f5d3bffa
url: "https://pub.dev"
source: hosted
version: "2.1.0"
fake_async:
dependency: transitive
description:

3
pubspec.yaml

@ -60,7 +60,8 @@ dependencies:
particles_fly: ^0.0.8
qr_flutter: ^4.0.0
path_provider: ^2.0.15
shared_preferences: ^2.2.2
shared_preferences: ^2.2.2
excel: ^2.0.1

Loading…
Cancel
Save