last commit excel gestion mety
This commit is contained in:
parent
d41936441c
commit
1aceb5669a
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:youmazgestion/Views/HandleProduct.dart';
|
||||||
import 'package:youmazgestion/Views/RoleListPage.dart';
|
import 'package:youmazgestion/Views/RoleListPage.dart';
|
||||||
import 'package:youmazgestion/Views/historique.dart';
|
import 'package:youmazgestion/Views/historique.dart';
|
||||||
import 'package:youmazgestion/Views/addProduct.dart';
|
import 'package:youmazgestion/Views/addProduct.dart';
|
||||||
@ -113,11 +114,11 @@ class CustomDrawer extends StatelessWidget {
|
|||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.add),
|
leading: const Icon(Icons.add),
|
||||||
iconColor: Colors.indigoAccent,
|
iconColor: Colors.indigoAccent,
|
||||||
title: const Text("Ajouter un produit"),
|
title: const Text("Gestion des produit"),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
bool hasPermission = await userController.hasPermission('create', '/ajouter-produit');
|
bool hasPermission = await userController.hasPermission('create', '/ajouter-produit');
|
||||||
if (hasPermission) {
|
if (hasPermission) {
|
||||||
Get.to(const AddProductPage());
|
Get.to(() => const ProductManagementPage());
|
||||||
} else {
|
} else {
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
"Accès refusé",
|
"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(
|
ListTile(
|
||||||
leading: const Icon(Icons.bar_chart),
|
leading: const Icon(Icons.bar_chart),
|
||||||
title: const Text("Bilan"),
|
title: const Text("Bilan"),
|
||||||
|
|||||||
@ -2,53 +2,30 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
final String title;
|
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
|
@override
|
||||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
Size get preferredSize => Size.fromHeight(subtitle == null ? 56.0 : 72.0);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
backgroundColor: Colors.transparent,
|
title: subtitle == null
|
||||||
elevation: 5,
|
? Text(title)
|
||||||
flexibleSpace: Container(
|
: Column(
|
||||||
decoration: const BoxDecoration(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
gradient: LinearGradient(
|
children: [
|
||||||
colors: [Colors.white, const Color.fromARGB(255, 4, 54, 95)],
|
Text(title, style: TextStyle(fontSize: 20)),
|
||||||
begin: Alignment.topLeft,
|
subtitle!,
|
||||||
end: Alignment.bottomRight,
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Spacer(),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 25,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Spacer(),
|
// autres propriétés si besoin
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 10.0),
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: Image.asset(
|
|
||||||
'assets/youmaz2.png',
|
|
||||||
width: 100,
|
|
||||||
height: 50,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,26 +1,25 @@
|
|||||||
class Product {
|
class Product {
|
||||||
final int? id;
|
int? id;
|
||||||
final String name;
|
final String name;
|
||||||
final double price;
|
final double price;
|
||||||
String image;
|
final String? image;
|
||||||
final String category;
|
final String category;
|
||||||
int? stock; // Paramètre optionnel pour le stock
|
final int? stock;
|
||||||
String? description; // Nouveau champ
|
final String? description;
|
||||||
String? qrCode; // Nouveau champ
|
String? qrCode;
|
||||||
String? reference;
|
final String? reference;
|
||||||
|
|
||||||
Product({
|
Product({
|
||||||
this.id,
|
this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.price,
|
required this.price,
|
||||||
required 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,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Vérifie si le stock est défini
|
// Vérifie si le stock est défini
|
||||||
bool isStockDefined() {
|
bool isStockDefined() {
|
||||||
if (stock != null) {
|
if (stock != null) {
|
||||||
@ -30,18 +29,17 @@ class Product {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
'id': id,
|
'id': id,
|
||||||
'name': name,
|
'name': name,
|
||||||
'price': price,
|
'price': price,
|
||||||
'image': image,
|
'image': image ?? '',
|
||||||
'category': category,
|
'category': category,
|
||||||
'stock': stock,
|
'stock': stock ?? 0,
|
||||||
'description': description,
|
'description': description ?? '',
|
||||||
'qrCode': qrCode,
|
'qrCode': qrCode ?? '',
|
||||||
'reference':reference
|
'reference': reference ?? '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +53,7 @@ class Product {
|
|||||||
stock: map['stock'],
|
stock: map['stock'],
|
||||||
description: map['description'],
|
description: map['description'],
|
||||||
qrCode: map['qrCode'],
|
qrCode: map['qrCode'],
|
||||||
reference:map['reference']
|
reference: map['reference'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,7 +42,7 @@ class ProductDatabase {
|
|||||||
return await databaseFactoryFfi.openDatabase(path);
|
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"
|
// 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 tables = await db.rawQuery("SELECT name FROM sqlite_master WHERE type='table'");
|
||||||
final tableNames = tables.map((row) => row['name'] as String).toList();
|
final tableNames = tables.map((row) => row['name'] as String).toList();
|
||||||
@ -58,13 +58,13 @@ class ProductDatabase {
|
|||||||
category TEXT,
|
category TEXT,
|
||||||
stock INTEGER,
|
stock INTEGER,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
qrCode TEXT
|
qrCode TEXT,
|
||||||
reference TEXT
|
reference TEXT
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
print("Table 'products' créée avec toutes les colonnes.");
|
print("Table 'products' créée avec toutes les colonnes.");
|
||||||
} else {
|
} 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 columns = await db.rawQuery('PRAGMA table_info(products)');
|
||||||
final columnNames = columns.map((e) => e['name'] as String).toList();
|
final columnNames = columns.map((e) => e['name'] as String).toList();
|
||||||
|
|
||||||
@ -87,6 +87,7 @@ class ProductDatabase {
|
|||||||
print("Erreur ajout colonne qrCode : $e");
|
print("Erreur ajout colonne qrCode : $e");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajoute la colonne "reference" si elle n'existe pas
|
// Ajoute la colonne "reference" si elle n'existe pas
|
||||||
if (!columnNames.contains('reference')) {
|
if (!columnNames.contains('reference')) {
|
||||||
try {
|
try {
|
||||||
@ -153,4 +154,19 @@ class ProductDatabase {
|
|||||||
return await db
|
return await db
|
||||||
.rawUpdate('UPDATE products SET stock = ? WHERE id = ?', [stock, id]);
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,40 +8,45 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
|||||||
|
|
||||||
class WorkDatabase {
|
class WorkDatabase {
|
||||||
static final WorkDatabase instance = WorkDatabase._init();
|
static final WorkDatabase instance = WorkDatabase._init();
|
||||||
late Database _database;
|
|
||||||
|
static Database? _database;
|
||||||
|
bool _isInitialized = false;
|
||||||
|
|
||||||
WorkDatabase._init() {
|
WorkDatabase._init() {
|
||||||
sqflite_ffi.sqfliteFfiInit();
|
sqflite_ffi.sqfliteFfiInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> initDatabase() async {
|
Future<void> initDatabase() async {
|
||||||
_database = await _initDB('work.db');
|
if (!_isInitialized) {
|
||||||
await _createDB(_database, 1);
|
_database = await _initDB('work.db');
|
||||||
|
await _createDB(_database!, 1);
|
||||||
|
_isInitialized = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Database> get database async {
|
Future<Database> get database async {
|
||||||
if (_database.isOpen) return _database;
|
if (!_isInitialized) {
|
||||||
|
await initDatabase();
|
||||||
_database = await _initDB('work.db');
|
}
|
||||||
return _database;
|
return _database!;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Database> _initDB(String filePath) async {
|
Future<Database> _initDB(String filePath) async {
|
||||||
// Obtenez le répertoire de stockage local de l'application
|
|
||||||
final documentsDirectory = await getApplicationDocumentsDirectory();
|
final documentsDirectory = await getApplicationDocumentsDirectory();
|
||||||
final path = join(documentsDirectory.path, filePath);
|
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();
|
bool dbExists = await File(path).exists();
|
||||||
if (!dbExists) {
|
if (!dbExists) {
|
||||||
// Si le fichier n'existe pas, copiez-le depuis le dossier assets/database
|
try {
|
||||||
ByteData data = await rootBundle.load('assets/database/$filePath');
|
ByteData data = await rootBundle.load('assets/database/$filePath');
|
||||||
List<int> bytes =
|
List<int> bytes =
|
||||||
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
|
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
|
||||||
await File(path).writeAsBytes(bytes);
|
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);
|
return await databaseFactoryFfi.openDatabase(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,42 +54,30 @@ class WorkDatabase {
|
|||||||
await db.execute('''
|
await db.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS work (
|
CREATE TABLE IF NOT EXISTS work (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
date TEXT
|
date TEXT UNIQUE
|
||||||
)
|
)
|
||||||
''');
|
''');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> insertDate(String date) async {
|
Future<int> insertDate(String date) async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
final existingDates =
|
try {
|
||||||
await db.query('work', where: 'date = ?', whereArgs: [date]);
|
return await db.insert('work', {'date': date});
|
||||||
|
} catch (e) {
|
||||||
if (existingDates.isNotEmpty) {
|
// En cas de doublon (date déjà existante)
|
||||||
// Date already exists, return 0 to indicate no new insertion
|
|
||||||
return 0;
|
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 {
|
Future<List<String>> getDates() async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
final result = await db.query('work');
|
final result = await db.query('work');
|
||||||
return List.generate(
|
return result.map((row) => row['date'] as String).toList();
|
||||||
result.length, (index) => result[index]['date'] as String);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// recuperer les dates par ordre du plus recent au plus ancien
|
|
||||||
Future<List<String>> getDatesDesc() async {
|
Future<List<String>> getDatesDesc() async {
|
||||||
final db = await database;
|
final db = await database;
|
||||||
final result = await db.query('work', orderBy: 'date DESC');
|
final result = await db.query('work', orderBy: 'date DESC');
|
||||||
return List.generate(
|
return result.map((row) => row['date'] as String).toList();
|
||||||
result.length, (index) => result[index]['date'] as String);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1532
lib/Views/HandleProduct.dart
Normal file
1532
lib/Views/HandleProduct.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,13 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.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/appDrawer.dart';
|
||||||
import '../Components/app_bar.dart';
|
import '../Components/app_bar.dart';
|
||||||
@ -23,13 +26,20 @@ class _AddProductPageState extends State<AddProductPage> {
|
|||||||
final TextEditingController _priceController = TextEditingController();
|
final TextEditingController _priceController = TextEditingController();
|
||||||
final TextEditingController _imageController = TextEditingController();
|
final TextEditingController _imageController = TextEditingController();
|
||||||
final TextEditingController _descriptionController = 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;
|
String? _selectedCategory;
|
||||||
File? _pickedImage;
|
File? _pickedImage;
|
||||||
String? _qrData;
|
String? _qrData;
|
||||||
|
String? _currentReference; // Ajout pour stocker la référence actuelle
|
||||||
late ProductDatabase _productDatabase;
|
late ProductDatabase _productDatabase;
|
||||||
|
|
||||||
|
// Variables pour la barre de progression
|
||||||
|
bool _isImporting = false;
|
||||||
|
double _importProgress = 0.0;
|
||||||
|
String _importStatusText = '';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@ -45,18 +55,35 @@ class _AddProductPageState extends State<AddProductPage> {
|
|||||||
_priceController.dispose();
|
_priceController.dispose();
|
||||||
_imageController.dispose();
|
_imageController.dispose();
|
||||||
_descriptionController.dispose();
|
_descriptionController.dispose();
|
||||||
|
_stockController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateQrData() {
|
// Méthode pour générer une référence unique
|
||||||
if (_nameController.text.isNotEmpty) {
|
String _generateUniqueReference() {
|
||||||
final reference = 'PROD_PREVIEW_${_nameController.text}_${DateTime.now().millisecondsSinceEpoch}';
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
setState(() {
|
final randomSuffix = DateTime.now().microsecond.toString().padLeft(6, '0');
|
||||||
_qrData = 'https://tonsite.com/$reference';
|
return 'PROD_${timestamp}${randomSuffix}';
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _updateQrData() {
|
||||||
|
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 {
|
Future<void> _selectImage() async {
|
||||||
final result = await FilePicker.platform.pickFiles(type: FileType.image);
|
final result = await FilePicker.platform.pickFiles(type: FileType.image);
|
||||||
if (result != null && result.files.single.path != null) {
|
if (result != null && result.files.single.path != null) {
|
||||||
@ -67,69 +94,488 @@ class _AddProductPageState extends State<AddProductPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _generateAndSaveQRCode(String reference) async {
|
// Assurez-vous aussi que _generateAndSaveQRCode utilise bien la référence passée :
|
||||||
final validation = QrValidator.validate(
|
Future<String> _generateAndSaveQRCode(String reference) async {
|
||||||
data: 'https://tonsite.com/$reference',
|
final qrUrl = 'https://stock.guycom.mg/$reference'; // Utilise le paramètre reference
|
||||||
version: QrVersions.auto,
|
|
||||||
errorCorrectionLevel: QrErrorCorrectLevel.L,
|
final validation = QrValidator.validate(
|
||||||
);
|
data: qrUrl,
|
||||||
|
version: QrVersions.auto,
|
||||||
|
errorCorrectionLevel: QrErrorCorrectLevel.L,
|
||||||
|
);
|
||||||
|
|
||||||
final qrCode = validation.qrCode!;
|
if (validation.status != QrValidationStatus.valid) {
|
||||||
final painter = QrPainter.withQr(
|
throw Exception('Données QR invalides: ${validation.error}');
|
||||||
qr: qrCode,
|
|
||||||
color: Colors.black,
|
|
||||||
emptyColor: Colors.white,
|
|
||||||
gapless: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
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());
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _addProduct() async {
|
final qrCode = validation.qrCode!;
|
||||||
final name = _nameController.text.trim();
|
final painter = QrPainter.withQr(
|
||||||
final price = double.tryParse(_priceController.text.trim()) ?? 0.0;
|
qr: qrCode,
|
||||||
final image = _imageController.text.trim();
|
color: Colors.black,
|
||||||
final category = _selectedCategory;
|
emptyColor: Colors.white,
|
||||||
final description = _descriptionController.text.trim();
|
gapless: true,
|
||||||
|
);
|
||||||
|
|
||||||
if (name.isEmpty || price <= 0 || image.isEmpty || category == null) {
|
final directory = await getApplicationDocumentsDirectory();
|
||||||
Get.snackbar('Erreur', 'Veuillez remplir tous les champs requis');
|
final path = '${directory.path}/$reference.png'; // Utilise le paramètre reference
|
||||||
return;
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
final reference = 'PROD_${DateTime.now().millisecondsSinceEpoch}';
|
return path;
|
||||||
final qrPath = await _generateAndSaveQRCode(reference);
|
}
|
||||||
|
|
||||||
final product = Product(
|
void _addProduct() async {
|
||||||
name: name,
|
final name = _nameController.text.trim();
|
||||||
price: price,
|
final price = double.tryParse(_priceController.text.trim()) ?? 0.0;
|
||||||
image: image,
|
final image = _imageController.text.trim();
|
||||||
category: category,
|
final category = _selectedCategory ?? 'Non catégorisé';
|
||||||
description: description,
|
final description = _descriptionController.text.trim();
|
||||||
qrCode: qrPath,
|
final stock = int.tryParse(_stockController.text.trim()) ?? 0;
|
||||||
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 {
|
try {
|
||||||
await _productDatabase.createProduct(product);
|
final result = await FilePicker.platform.pickFiles(
|
||||||
Get.snackbar('Succès', 'Produit ajouté avec succès');
|
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(() {
|
setState(() {
|
||||||
_nameController.clear();
|
_importProgress = 0.1;
|
||||||
_priceController.clear();
|
_importStatusText = 'Vérification du fichier...';
|
||||||
_imageController.clear();
|
|
||||||
_descriptionController.clear();
|
|
||||||
_selectedCategory = null;
|
|
||||||
_pickedImage = null;
|
|
||||||
_qrData = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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(() {
|
||||||
|
_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) {
|
} 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 où 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(
|
return Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 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(
|
decoration: BoxDecoration(
|
||||||
color: Colors.grey[200],
|
color: Colors.grey[200],
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
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: [
|
children: [
|
||||||
const Text('Ajouter un produit', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
const Text('Ajouter un produit', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 16),
|
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(
|
TextField(
|
||||||
controller: _nameController,
|
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),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
controller: _priceController,
|
controller: _priceController,
|
||||||
|
enabled: !_isImporting,
|
||||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||||
decoration: const InputDecoration(labelText: 'Prix', border: OutlineInputBorder()),
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Prix*',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
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(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _imageController,
|
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,
|
readOnly: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
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),
|
const SizedBox(height: 16),
|
||||||
_displayImage(),
|
_displayImage(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: _selectedCategory,
|
value: _selectedCategory,
|
||||||
items: _categories
|
items: _categories
|
||||||
.map((c) => DropdownMenuItem(value: c, child: Text(c)))
|
.map((c) => DropdownMenuItem(value: c, child: Text(c)))
|
||||||
.toList(),
|
.toList(),
|
||||||
onChanged: (value) => setState(() => _selectedCategory = value),
|
onChanged: _isImporting ? null : (value) => setState(() => _selectedCategory = value),
|
||||||
decoration: const InputDecoration(labelText: 'Catégorie', border: OutlineInputBorder()),
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Catégorie',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
controller: _descriptionController,
|
controller: _descriptionController,
|
||||||
|
enabled: !_isImporting,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
decoration: const InputDecoration(labelText: 'Description', border: OutlineInputBorder()),
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Description (optionnel)',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
if (_qrData != null) ...[
|
if (_qrData != null) ...[
|
||||||
const Text('Aperçu du QR Code :'),
|
const Text('Aperçu du QR Code :'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@ -222,12 +786,21 @@ class _AddProductPageState extends State<AddProductPage> {
|
|||||||
size: 120,
|
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),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _addProduct,
|
onPressed: _isImporting ? null : _addProduct,
|
||||||
child: const Text('Ajouter le produit'),
|
child: const Text('Ajouter le produit'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -236,4 +809,4 @@ class _AddProductPageState extends State<AddProductPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:youmazgestion/Components/app_bar.dart';
|
import 'package:youmazgestion/Components/app_bar.dart';
|
||||||
|
|
||||||
import '../Components/appDrawer.dart';
|
import '../Components/appDrawer.dart';
|
||||||
import '../Models/produit.dart';
|
import '../Models/produit.dart';
|
||||||
import '../Services/productDatabase.dart';
|
import '../Services/productDatabase.dart';
|
||||||
@ -11,7 +10,7 @@ import 'dart:io';
|
|||||||
class GestionProduit extends StatelessWidget {
|
class GestionProduit extends StatelessWidget {
|
||||||
final ProductDatabase _productDatabase = ProductDatabase.instance;
|
final ProductDatabase _productDatabase = ProductDatabase.instance;
|
||||||
|
|
||||||
GestionProduit({super.key});
|
GestionProduit({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -19,28 +18,22 @@ class GestionProduit extends StatelessWidget {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const CustomAppBar(title: 'Gestion des produits'),
|
appBar: const CustomAppBar(title: 'Gestion des produits'),
|
||||||
drawer: CustomDrawer(),
|
drawer: CustomDrawer(),
|
||||||
body: FutureBuilder<List<Product>>(
|
body: FutureBuilder<List<Product>>(
|
||||||
future: _productDatabase.getProducts(),
|
future: _productDatabase.getProducts(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const Center(
|
return const Center(child: CircularProgressIndicator());
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
return const Center(
|
return const Center(child: Text('Une erreur s\'est produite'));
|
||||||
child: Text('Une erreur s\'est produite'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final products = snapshot.data;
|
final products = snapshot.data;
|
||||||
|
|
||||||
if (products == null || products.isEmpty) {
|
if (products == null || products.isEmpty) {
|
||||||
return const Center(
|
return const Center(child: Text('Aucun produit disponible'));
|
||||||
child: Text('Aucun produit disponible'),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
@ -52,68 +45,30 @@ class GestionProduit extends StatelessWidget {
|
|||||||
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 20),
|
margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: BorderRadius.circular(30),
|
||||||
border: Border.all(
|
border: Border.all(color: Colors.grey, width: 1.0),
|
||||||
color: Colors.grey,
|
|
||||||
width: 1.0,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: CircleAvatar(
|
leading: _buildProductImage(product.image),
|
||||||
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
|
|
||||||
),
|
|
||||||
title: Text(product.name),
|
title: Text(product.name),
|
||||||
subtitle: Column(
|
subtitle: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('Price: \$${product.price.toStringAsFixed(2)}'),
|
Text('Prix: \$${product.price.toStringAsFixed(2)}'),
|
||||||
Text('Category: ${product.category}'),
|
Text('Catégorie: ${product.category}'),
|
||||||
|
if (product.stock != null)
|
||||||
|
Text('Stock: ${product.stock}'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: Row(
|
trailing: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () => _deleteProduct(product.id),
|
||||||
_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,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () => _editProduct(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,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
color: Colors.blue,
|
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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -114,7 +114,7 @@ class _ProductCardState extends State<ProductCard> with TickerProviderStateMixin
|
|||||||
),
|
),
|
||||||
child: widget.product.image != null
|
child: widget.product.image != null
|
||||||
? Image.file(
|
? Image.file(
|
||||||
File(widget.product.image),
|
File(widget.product.image!),
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
return _buildPlaceholderImage();
|
return _buildPlaceholderImage();
|
||||||
|
|||||||
300
lib/accueil.dart
300
lib/accueil.dart
@ -47,6 +47,30 @@ class _AccueilPageState extends State<AccueilPage> {
|
|||||||
initwork();
|
initwork();
|
||||||
loadUserData();
|
loadUserData();
|
||||||
productsFuture = _initDatabaseAndFetchProducts();
|
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 {
|
Future<void> loadUserData() async {
|
||||||
@ -169,11 +193,16 @@ class _AccueilPageState extends State<AccueilPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@override
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: const CustomAppBar(title: "Accueil"),
|
appBar: CustomAppBar(
|
||||||
|
title: "Accueil",
|
||||||
|
subtitle: Text('Bienvenue $username ! (Rôle: $role)'),
|
||||||
|
),
|
||||||
drawer: CustomDrawer(),
|
drawer: CustomDrawer(),
|
||||||
body: ParticleBackground(
|
body: ParticleBackground(
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -181,8 +210,7 @@ class _AccueilPageState extends State<AccueilPage> {
|
|||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
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>>>(
|
child: FutureBuilder<Map<String, List<Product>>>(
|
||||||
future: productsFuture,
|
future: productsFuture,
|
||||||
@ -195,146 +223,176 @@ class _AccueilPageState extends State<AccueilPage> {
|
|||||||
final productsByCategory = snapshot.data!;
|
final productsByCategory = snapshot.data!;
|
||||||
final categories = productsByCategory.keys.toList();
|
final categories = productsByCategory.keys.toList();
|
||||||
|
|
||||||
if (!MyApp.isRegisterOpen) {
|
return Row(
|
||||||
return Column(
|
children: [
|
||||||
children: [
|
Expanded(
|
||||||
Text('Bienvenue $username ! (Rôle: $role)'),
|
flex: 3,
|
||||||
Center(
|
child: ListView.builder(
|
||||||
child: ElevatedButton(
|
itemCount: categories.length,
|
||||||
onPressed: () {
|
itemBuilder: (context, index) {
|
||||||
setState(() {
|
final category = categories[index];
|
||||||
MyApp.isRegisterOpen = true;
|
final products = productsByCategory[category]!;
|
||||||
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 Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
category,
|
category,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 22,
|
fontSize: 22,
|
||||||
fontWeight: FontWeight.bold,
|
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);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
flex: 1,
|
|
||||||
child: Container(
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
GridView.builder(
|
||||||
Expanded(
|
gridDelegate:
|
||||||
child: selectedProducts.isEmpty
|
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
? const Center(
|
crossAxisCount: 4,
|
||||||
child: Text("Votre panier est vide"),
|
childAspectRatio: 1,
|
||||||
)
|
),
|
||||||
: ListView.builder(
|
shrinkWrap: true,
|
||||||
itemCount: selectedProducts.length,
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemBuilder: (context, index) {
|
itemCount: products.length,
|
||||||
final cartItem = selectedProducts[index];
|
itemBuilder: (context, index) {
|
||||||
return ListTile(
|
final product = products[index];
|
||||||
title: Text(cartItem.product.name),
|
return ProductCard(
|
||||||
|
product: product,
|
||||||
|
onAddToCart: (product, quantity) {
|
||||||
|
addToCartWithDetails(product, quantity);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
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(
|
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(
|
trailing: IconButton(
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete,
|
||||||
|
color: Colors.red),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
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(
|
style: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
TextField(
|
),
|
||||||
keyboardType: TextInputType.number,
|
TextField(
|
||||||
decoration: const InputDecoration(
|
keyboardType: TextInputType.number,
|
||||||
labelText: 'Montant payé',
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Montant payé',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
filled: true,
|
||||||
amountPaid = double.tryParse(value) ?? 0;
|
fillColor: Colors.white,
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
onChanged: (value) {
|
||||||
onPressed: saveOrderToDatabase,
|
amountPaid = double.tryParse(value) ?? 0;
|
||||||
child: const Text('Valider la commande'),
|
},
|
||||||
|
),
|
||||||
|
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 {
|
} else {
|
||||||
return const Center(child: Text("Aucun produit disponible"));
|
return const Center(child: Text("Aucun produit disponible"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -169,6 +169,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
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:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -60,7 +60,8 @@ dependencies:
|
|||||||
particles_fly: ^0.0.8
|
particles_fly: ^0.0.8
|
||||||
qr_flutter: ^4.0.0
|
qr_flutter: ^4.0.0
|
||||||
path_provider: ^2.0.15
|
path_provider: ^2.0.15
|
||||||
shared_preferences: ^2.2.2
|
shared_preferences: ^2.2.2
|
||||||
|
excel: ^2.0.1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user