You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1238 lines
40 KiB
1238 lines
40 KiB
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:youmazgestion/Components/QrScan.dart';
|
|
import 'package:youmazgestion/Components/app_bar.dart';
|
|
import 'package:youmazgestion/Components/appDrawer.dart';
|
|
import 'package:youmazgestion/Models/client.dart';
|
|
import 'package:youmazgestion/Models/users.dart';
|
|
import 'package:youmazgestion/Models/produit.dart';
|
|
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
|
|
import 'package:youmazgestion/Views/historique.dart';
|
|
|
|
void main() {
|
|
runApp(const MyApp());
|
|
}
|
|
|
|
class MyApp extends StatelessWidget {
|
|
const MyApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GetMaterialApp(
|
|
title: 'Youmaz Gestion',
|
|
theme: ThemeData(
|
|
primarySwatch: Colors.blue,
|
|
visualDensity: VisualDensity.adaptivePlatformDensity,
|
|
),
|
|
home: const MainLayout(),
|
|
debugShowCheckedModeBanner: false,
|
|
);
|
|
}
|
|
}
|
|
|
|
class MainLayout extends StatefulWidget {
|
|
const MainLayout({super.key});
|
|
|
|
@override
|
|
State<MainLayout> createState() => _MainLayoutState();
|
|
}
|
|
|
|
class _MainLayoutState extends State<MainLayout> {
|
|
int _currentIndex = 1; // Index par défaut pour la page de commande
|
|
|
|
final List<Widget> _pages = [
|
|
const HistoriquePage(),
|
|
const NouvelleCommandePage(), // Page 1 - Nouvelle commande
|
|
const ScanQRPage(), // Page 2 - Scan QR
|
|
];
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: _currentIndex == 1 ? CustomAppBar(title: 'Nouvelle Commande') : null,
|
|
drawer: CustomDrawer(),
|
|
body: _pages[_currentIndex],
|
|
bottomNavigationBar: _buildAdaptiveBottomNavBar(),
|
|
);
|
|
}
|
|
|
|
Widget _buildAdaptiveBottomNavBar() {
|
|
final isDesktop = MediaQuery.of(context).size.width > 600;
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
border: isDesktop
|
|
? const Border(top: BorderSide(color: Colors.grey, width: 0.5))
|
|
: null,
|
|
),
|
|
child: BottomNavigationBar(
|
|
currentIndex: _currentIndex,
|
|
onTap: (index) {
|
|
setState(() {
|
|
_currentIndex = index;
|
|
});
|
|
},
|
|
// Style adapté pour desktop
|
|
type: isDesktop ? BottomNavigationBarType.fixed : BottomNavigationBarType.fixed,
|
|
selectedFontSize: isDesktop ? 14 : 12,
|
|
unselectedFontSize: isDesktop ? 14 : 12,
|
|
iconSize: isDesktop ? 28 : 24,
|
|
items: const [
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.history),
|
|
label: 'Historique',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.add_shopping_cart),
|
|
label: 'Commande',
|
|
),
|
|
BottomNavigationBarItem(
|
|
icon: Icon(Icons.qr_code_scanner),
|
|
label: 'Scan QR',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class NouvelleCommandePage extends StatefulWidget {
|
|
const NouvelleCommandePage({super.key});
|
|
|
|
@override
|
|
_NouvelleCommandePageState createState() => _NouvelleCommandePageState();
|
|
}
|
|
|
|
class _NouvelleCommandePageState extends State<NouvelleCommandePage> {
|
|
final AppDatabase _appDatabase = AppDatabase.instance;
|
|
final _formKey = GlobalKey<FormState>();
|
|
bool _isLoading = false;
|
|
|
|
// Contrôleurs client
|
|
final TextEditingController _nomController = TextEditingController();
|
|
final TextEditingController _prenomController = TextEditingController();
|
|
final TextEditingController _emailController = TextEditingController();
|
|
final TextEditingController _telephoneController = TextEditingController();
|
|
final TextEditingController _adresseController = TextEditingController();
|
|
|
|
// Contrôleurs pour les filtres
|
|
final TextEditingController _searchNameController = TextEditingController();
|
|
final TextEditingController _searchImeiController = TextEditingController();
|
|
final TextEditingController _searchReferenceController = TextEditingController();
|
|
|
|
// Panier
|
|
final List<Product> _products = [];
|
|
final List<Product> _filteredProducts = [];
|
|
final Map<int, int> _quantites = {};
|
|
|
|
// Variables de filtre
|
|
bool _showOnlyInStock = false;
|
|
|
|
// Utilisateurs commerciaux
|
|
List<Users> _commercialUsers = [];
|
|
Users? _selectedCommercialUser;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadProducts();
|
|
_loadCommercialUsers();
|
|
|
|
// Listeners pour les filtres
|
|
_searchNameController.addListener(_filterProducts);
|
|
_searchImeiController.addListener(_filterProducts);
|
|
_searchReferenceController.addListener(_filterProducts);
|
|
}
|
|
|
|
Future<void> _loadProducts() async {
|
|
final products = await _appDatabase.getProducts();
|
|
setState(() {
|
|
_products.clear();
|
|
_products.addAll(products);
|
|
_filteredProducts.clear();
|
|
_filteredProducts.addAll(products);
|
|
});
|
|
}
|
|
|
|
Future<void> _loadCommercialUsers() async {
|
|
final commercialUsers = await _appDatabase.getCommercialUsers();
|
|
setState(() {
|
|
_commercialUsers = commercialUsers;
|
|
if (_commercialUsers.isNotEmpty) {
|
|
_selectedCommercialUser = _commercialUsers.first;
|
|
}
|
|
});
|
|
}
|
|
|
|
void _filterProducts() {
|
|
final nameQuery = _searchNameController.text.toLowerCase();
|
|
final imeiQuery = _searchImeiController.text.toLowerCase();
|
|
final referenceQuery = _searchReferenceController.text.toLowerCase();
|
|
|
|
setState(() {
|
|
_filteredProducts.clear();
|
|
|
|
for (var product in _products) {
|
|
bool matchesName = nameQuery.isEmpty ||
|
|
product.name.toLowerCase().contains(nameQuery);
|
|
|
|
bool matchesImei = imeiQuery.isEmpty ||
|
|
(product.imei?.toLowerCase().contains(imeiQuery) ?? false);
|
|
|
|
bool matchesReference = referenceQuery.isEmpty ||
|
|
(product.reference?.toLowerCase().contains(referenceQuery) ?? false);
|
|
|
|
bool matchesStock = !_showOnlyInStock ||
|
|
(product.stock != null && product.stock! > 0);
|
|
|
|
if (matchesName && matchesImei && matchesReference && matchesStock) {
|
|
_filteredProducts.add(product);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void _toggleStockFilter() {
|
|
setState(() {
|
|
_showOnlyInStock = !_showOnlyInStock;
|
|
});
|
|
_filterProducts();
|
|
}
|
|
|
|
void _clearFilters() {
|
|
setState(() {
|
|
_searchNameController.clear();
|
|
_searchImeiController.clear();
|
|
_searchReferenceController.clear();
|
|
_showOnlyInStock = false;
|
|
});
|
|
_filterProducts();
|
|
}
|
|
|
|
// Section des filtres adaptée pour mobile
|
|
Widget _buildFilterSection() {
|
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
|
|
|
return Card(
|
|
elevation: 2,
|
|
margin: const EdgeInsets.only(bottom: 16),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.filter_list, color: Colors.blue.shade700),
|
|
const SizedBox(width: 8),
|
|
const Text(
|
|
'Filtres de recherche',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color.fromARGB(255, 9, 56, 95),
|
|
),
|
|
),
|
|
const Spacer(),
|
|
TextButton.icon(
|
|
onPressed: _clearFilters,
|
|
icon: const Icon(Icons.clear, size: 18),
|
|
label: isMobile ? const SizedBox() : const Text('Réinitialiser'),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Champ de recherche par nom
|
|
TextField(
|
|
controller: _searchNameController,
|
|
decoration: InputDecoration(
|
|
labelText: 'Rechercher par nom',
|
|
prefixIcon: const Icon(Icons.search),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
if (!isMobile) ...[
|
|
// Version desktop - champs sur la même ligne
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _searchImeiController,
|
|
decoration: InputDecoration(
|
|
labelText: 'IMEI',
|
|
prefixIcon: const Icon(Icons.phone_android),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: TextField(
|
|
controller: _searchReferenceController,
|
|
decoration: InputDecoration(
|
|
labelText: 'Référence',
|
|
prefixIcon: const Icon(Icons.qr_code),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
] else ...[
|
|
// Version mobile - champs empilés
|
|
TextField(
|
|
controller: _searchImeiController,
|
|
decoration: InputDecoration(
|
|
labelText: 'IMEI',
|
|
prefixIcon: const Icon(Icons.phone_android),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _searchReferenceController,
|
|
decoration: InputDecoration(
|
|
labelText: 'Référence',
|
|
prefixIcon: const Icon(Icons.qr_code),
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.grey.shade50,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 16),
|
|
|
|
// Boutons de filtre adaptés pour mobile
|
|
Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
children: [
|
|
ElevatedButton.icon(
|
|
onPressed: _toggleStockFilter,
|
|
icon: Icon(
|
|
_showOnlyInStock ? Icons.inventory : Icons.inventory_2,
|
|
size: 20,
|
|
),
|
|
label: Text(_showOnlyInStock
|
|
? isMobile ? 'Tous' : 'Afficher tous'
|
|
: isMobile ? 'En stock' : 'Stock disponible'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: _showOnlyInStock
|
|
? Colors.green.shade600
|
|
: Colors.blue.shade600,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: isMobile ? 12 : 16,
|
|
vertical: 8
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
// Compteur de résultats
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 12,
|
|
vertical: 8
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade50,
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
'${_filteredProducts.length} produit(s)',
|
|
style: TextStyle(
|
|
color: Colors.blue.shade700,
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: isMobile ? 12 : 14,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
|
|
|
return Scaffold(
|
|
floatingActionButton: _buildFloatingCartButton(),
|
|
drawer: isMobile ? CustomDrawer() : null,
|
|
body: Column(
|
|
children: [
|
|
// Bouton client - version compacte pour mobile
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
style: ElevatedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: isMobile ? 12 : 16
|
|
),
|
|
backgroundColor: Colors.blue.shade800,
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
onPressed: _showClientFormDialog,
|
|
icon: const Icon(Icons.person_add),
|
|
label: Text(
|
|
isMobile ? 'Client' : 'Ajouter les informations client',
|
|
style: TextStyle(fontSize: isMobile ? 14 : 16),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
|
|
// Section des filtres - adaptée comme dans HistoriquePage
|
|
if (!isMobile)
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
child: _buildFilterSection(),
|
|
),
|
|
|
|
// Sur mobile, bouton pour afficher les filtres dans un modal
|
|
if (isMobile) ...[
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
|
child: SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton.icon(
|
|
icon: const Icon(Icons.filter_alt),
|
|
label: const Text('Filtres produits'),
|
|
onPressed: () {
|
|
showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
builder: (context) => SingleChildScrollView(
|
|
padding: EdgeInsets.only(
|
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
),
|
|
child: _buildFilterSection(),
|
|
),
|
|
);
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue.shade700,
|
|
foregroundColor: Colors.white,
|
|
minimumSize: const Size(double.infinity, 48),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
// Compteur de résultats visible en haut sur mobile
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade50,
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
'${_filteredProducts.length} produit(s)',
|
|
style: TextStyle(
|
|
color: Colors.blue.shade700,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
|
|
// Liste des produits
|
|
Expanded(
|
|
child: _buildProductList(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFloatingCartButton() {
|
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
|
final cartItemCount = _quantites.values.where((q) => q > 0).length;
|
|
|
|
return FloatingActionButton.extended(
|
|
onPressed: () {
|
|
_showCartBottomSheet();
|
|
},
|
|
icon: const Icon(Icons.shopping_cart),
|
|
label: Text(
|
|
isMobile ? 'Panier ($cartItemCount)' : 'Panier ($cartItemCount)',
|
|
style: TextStyle(fontSize: isMobile ? 12 : 14),
|
|
),
|
|
backgroundColor: Colors.blue.shade800,
|
|
foregroundColor: Colors.white,
|
|
);
|
|
}
|
|
|
|
void _showClientFormDialog() {
|
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
|
|
|
Get.dialog(
|
|
AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade100,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(Icons.person_add, color: Colors.blue.shade700),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
isMobile ? 'Client' : 'Informations Client',
|
|
style: TextStyle(fontSize: isMobile ? 16 : 18),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
content: Container(
|
|
width: isMobile ? double.maxFinite : 600,
|
|
constraints: BoxConstraints(
|
|
maxHeight: MediaQuery.of(context).size.height * 0.7,
|
|
),
|
|
child: SingleChildScrollView(
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildTextFormField(
|
|
controller: _nomController,
|
|
label: 'Nom',
|
|
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildTextFormField(
|
|
controller: _prenomController,
|
|
label: 'Prénom',
|
|
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildTextFormField(
|
|
controller: _emailController,
|
|
label: 'Email',
|
|
keyboardType: TextInputType.emailAddress,
|
|
validator: (value) {
|
|
if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
|
|
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
|
|
return 'Email invalide';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildTextFormField(
|
|
controller: _telephoneController,
|
|
label: 'Téléphone',
|
|
keyboardType: TextInputType.phone,
|
|
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildTextFormField(
|
|
controller: _adresseController,
|
|
label: 'Adresse',
|
|
maxLines: 2,
|
|
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_buildCommercialDropdown(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Get.back(),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.blue.shade800,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: isMobile ? 16 : 20,
|
|
vertical: isMobile ? 10 : 12
|
|
),
|
|
),
|
|
onPressed: () {
|
|
if (_formKey.currentState!.validate()) {
|
|
Get.back();
|
|
_submitOrder();
|
|
}
|
|
},
|
|
child: Text(
|
|
isMobile ? 'Valider' : 'Valider la commande',
|
|
style: TextStyle(fontSize: isMobile ? 12 : 14),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTextFormField({
|
|
required TextEditingController controller,
|
|
required String label,
|
|
TextInputType? keyboardType,
|
|
String? Function(String?)? validator,
|
|
int? maxLines,
|
|
}) {
|
|
return TextFormField(
|
|
controller: controller,
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide(color: Colors.grey.shade400),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: BorderSide(color: Colors.grey.shade400),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
),
|
|
keyboardType: keyboardType,
|
|
validator: validator,
|
|
maxLines: maxLines,
|
|
);
|
|
}
|
|
|
|
Widget _buildCommercialDropdown() {
|
|
return DropdownButtonFormField<Users>(
|
|
value: _selectedCommercialUser,
|
|
decoration: InputDecoration(
|
|
labelText: 'Commercial',
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
),
|
|
items: _commercialUsers.map((Users user) {
|
|
return DropdownMenuItem<Users>(
|
|
value: user,
|
|
child: Text('${user.name} ${user.lastName}'),
|
|
);
|
|
}).toList(),
|
|
onChanged: (Users? newValue) {
|
|
setState(() {
|
|
_selectedCommercialUser = newValue;
|
|
});
|
|
},
|
|
validator: (value) => value == null ? 'Veuillez sélectionner un commercial' : null,
|
|
);
|
|
}
|
|
|
|
Widget _buildProductList() {
|
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
|
|
|
return _filteredProducts.isEmpty
|
|
? _buildEmptyState()
|
|
: ListView.builder(
|
|
padding: const EdgeInsets.all(16.0),
|
|
itemCount: _filteredProducts.length,
|
|
itemBuilder: (context, index) {
|
|
final product = _filteredProducts[index];
|
|
final quantity = _quantites[product.id] ?? 0;
|
|
|
|
return _buildProductListItem(product, quantity, isMobile);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyState() {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32.0),
|
|
child: Column(
|
|
children: [
|
|
Icon(
|
|
Icons.search_off,
|
|
size: 64,
|
|
color: Colors.grey.shade400,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Aucun produit trouvé',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w500,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Modifiez vos critères de recherche',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Colors.grey.shade500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildProductListItem(Product product, int quantity, bool isMobile) {
|
|
final bool isOutOfStock = product.stock != null && product.stock! <= 0;
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
elevation: 2,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: isOutOfStock
|
|
? Border.all(color: Colors.red.shade200, width: 1.5)
|
|
: null,
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: isMobile ? 40 : 50,
|
|
height: isMobile ? 40 : 50,
|
|
decoration: BoxDecoration(
|
|
color: isOutOfStock
|
|
? Colors.red.shade50
|
|
: Colors.blue.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
Icons.shopping_bag,
|
|
size: isMobile ? 20 : 24,
|
|
color: isOutOfStock ? Colors.red : Colors.blue,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
product.name,
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: isMobile ? 14 : 16,
|
|
color: isOutOfStock ? Colors.red.shade700 : null,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'${product.price.toStringAsFixed(2)} MGA',
|
|
style: TextStyle(
|
|
color: Colors.green.shade700,
|
|
fontWeight: FontWeight.w600,
|
|
fontSize: isMobile ? 12 : 14,
|
|
),
|
|
),
|
|
if (product.stock != null)
|
|
Text(
|
|
'Stock: ${product.stock}${isOutOfStock ? ' (Rupture)' : ''}',
|
|
style: TextStyle(
|
|
fontSize: isMobile ? 10 : 12,
|
|
color: isOutOfStock
|
|
? Colors.red.shade600
|
|
: Colors.grey.shade600,
|
|
fontWeight: isOutOfStock ? FontWeight.w600 : FontWeight.normal,
|
|
),
|
|
),
|
|
// Affichage IMEI et Référence - plus compact sur mobile
|
|
if (product.imei != null && product.imei!.isNotEmpty)
|
|
Text(
|
|
'IMEI: ${product.imei}',
|
|
style: TextStyle(
|
|
fontSize: isMobile ? 9 : 11,
|
|
color: Colors.grey.shade600,
|
|
fontFamily: 'monospace',
|
|
),
|
|
),
|
|
if (product.reference != null && product.reference!.isNotEmpty)
|
|
Text(
|
|
'Réf: ${product.reference}',
|
|
style: TextStyle(
|
|
fontSize: isMobile ? 9 : 11,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: isOutOfStock
|
|
? Colors.grey.shade100
|
|
: Colors.blue.shade50,
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.remove,
|
|
size: isMobile ? 16 : 18
|
|
),
|
|
onPressed: isOutOfStock ? null : () {
|
|
if (quantity > 0) {
|
|
setState(() {
|
|
_quantites[product.id!] = quantity - 1;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
Text(
|
|
quantity.toString(),
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: isMobile ? 12 : 14,
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.add,
|
|
size: isMobile ? 16 : 18
|
|
),
|
|
onPressed: isOutOfStock ? null : () {
|
|
if (product.stock == null || quantity < product.stock!) {
|
|
setState(() {
|
|
_quantites[product.id!] = quantity + 1;
|
|
});
|
|
} else {
|
|
Get.snackbar(
|
|
'Stock insuffisant',
|
|
'Quantité demandée non disponible',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showCartBottomSheet() {
|
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
|
|
|
Get.bottomSheet(
|
|
Container(
|
|
height: MediaQuery.of(context).size.height * (isMobile ? 0.85 : 0.7),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
'Votre Panier',
|
|
style: TextStyle(
|
|
fontSize: isMobile ? 18 : 20,
|
|
fontWeight: FontWeight.bold
|
|
),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.close),
|
|
onPressed: () => Get.back(),
|
|
),
|
|
],
|
|
),
|
|
const Divider(),
|
|
Expanded(child: _buildCartItemsList()),
|
|
const Divider(),
|
|
_buildCartTotalSection(),
|
|
const SizedBox(height: 16),
|
|
_buildSubmitButton(),
|
|
],
|
|
),
|
|
),
|
|
isScrollControlled: true,
|
|
);
|
|
}
|
|
|
|
Widget _buildCartItemsList() {
|
|
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList();
|
|
|
|
if (itemsInCart.isEmpty) {
|
|
return const Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(Icons.shopping_cart_outlined, size: 60, color: Colors.grey),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'Votre panier est vide',
|
|
style: TextStyle(fontSize: 16, color: Colors.grey),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
return ListView.builder(
|
|
itemCount: itemsInCart.length,
|
|
itemBuilder: (context, index) {
|
|
final entry = itemsInCart[index];
|
|
final product = _products.firstWhere((p) => p.id == entry.key);
|
|
|
|
return Dismissible(
|
|
key: Key(entry.key.toString()),
|
|
background: Container(
|
|
color: Colors.red.shade100,
|
|
alignment: Alignment.centerRight,
|
|
padding: const EdgeInsets.only(right: 20),
|
|
child: const Icon(Icons.delete, color: Colors.red),
|
|
),
|
|
direction: DismissDirection.endToStart,
|
|
onDismissed: (direction) {
|
|
setState(() {
|
|
_quantites.remove(entry.key);
|
|
});
|
|
Get.snackbar(
|
|
'Produit retiré',
|
|
'${product.name} a été retiré du panier',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
);
|
|
},
|
|
child: Card(
|
|
margin: const EdgeInsets.only(bottom: 8),
|
|
elevation: 1,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: ListTile(
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
leading: Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Icon(Icons.shopping_bag, size: 20),
|
|
),
|
|
title: Text(product.name),
|
|
subtitle: Text('${entry.value} x ${product.price.toStringAsFixed(2)} MGA'),
|
|
trailing: Text(
|
|
'${(entry.value * product.price).toStringAsFixed(2)} MGA',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.blue.shade800,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildCartTotalSection() {
|
|
double total = 0;
|
|
_quantites.forEach((productId, quantity) {
|
|
final product = _products.firstWhere((p) => p.id == productId);
|
|
total += quantity * product.price;
|
|
});
|
|
|
|
return Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'Total:',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
Text(
|
|
'${total.toStringAsFixed(2)} MGA',
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.green,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'${_quantites.values.where((q) => q > 0).length} article(s)',
|
|
style: TextStyle(color: Colors.grey.shade600),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildSubmitButton() {
|
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
|
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: isMobile ? 12 : 16
|
|
),
|
|
backgroundColor: Colors.blue.shade800,
|
|
foregroundColor: Colors.white,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
elevation: 4,
|
|
),
|
|
onPressed: _submitOrder,
|
|
child: _isLoading
|
|
? SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Colors.white,
|
|
),
|
|
)
|
|
: Text(
|
|
isMobile ? 'Valider' : 'Valider la Commande',
|
|
style: TextStyle(fontSize: isMobile ? 14 : 16),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _submitOrder() async {
|
|
// Vérifier d'abord si le panier est vide
|
|
final itemsInCart = _quantites.entries.where((e) => e.value > 0).toList();
|
|
if (itemsInCart.isEmpty) {
|
|
Get.snackbar(
|
|
'Panier vide',
|
|
'Veuillez ajouter des produits à votre commande',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
_showCartBottomSheet(); // Ouvrir le panier pour montrer qu'il est vide
|
|
return;
|
|
}
|
|
|
|
// Ensuite vérifier les informations client
|
|
if (_nomController.text.isEmpty ||
|
|
_prenomController.text.isEmpty ||
|
|
_emailController.text.isEmpty ||
|
|
_telephoneController.text.isEmpty ||
|
|
_adresseController.text.isEmpty) {
|
|
Get.snackbar(
|
|
'Informations manquantes',
|
|
'Veuillez remplir les informations client',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
_showClientFormDialog();
|
|
return;
|
|
}
|
|
|
|
setState(() {
|
|
_isLoading = true;
|
|
});
|
|
|
|
// Créer le client
|
|
final client = Client(
|
|
nom: _nomController.text,
|
|
prenom: _prenomController.text,
|
|
email: _emailController.text,
|
|
telephone: _telephoneController.text,
|
|
adresse: _adresseController.text,
|
|
dateCreation: DateTime.now(),
|
|
);
|
|
|
|
// Calculer le total et préparer les détails
|
|
double total = 0;
|
|
final details = <DetailCommande>[];
|
|
|
|
for (final entry in itemsInCart) {
|
|
final product = _products.firstWhere((p) => p.id == entry.key);
|
|
total += entry.value * product.price;
|
|
|
|
details.add(DetailCommande(
|
|
commandeId: 0,
|
|
produitId: product.id!,
|
|
quantite: entry.value,
|
|
prixUnitaire: product.price,
|
|
sousTotal: entry.value * product.price,
|
|
));
|
|
}
|
|
|
|
// Créer la commande
|
|
final commande = Commande(
|
|
clientId: 0,
|
|
dateCommande: DateTime.now(),
|
|
statut: StatutCommande.enAttente,
|
|
montantTotal: total,
|
|
notes: 'Commande passée via l\'application',
|
|
commandeurId: _selectedCommercialUser?.id,
|
|
);
|
|
|
|
try {
|
|
await _appDatabase.createCommandeComplete(client, commande, details);
|
|
|
|
// Afficher le dialogue de confirmation - adapté pour mobile
|
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
|
|
|
await showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(8),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.shade100,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(Icons.check_circle, color: Colors.green.shade700),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Text(
|
|
'Commande Validée',
|
|
style: TextStyle(fontSize: isMobile ? 16 : 18),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
content: Text(
|
|
'Votre commande a été enregistrée et expédiée avec succès.',
|
|
style: TextStyle(fontSize: isMobile ? 14 : 16),
|
|
),
|
|
actions: [
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green.shade700,
|
|
foregroundColor: Colors.white,
|
|
padding: EdgeInsets.symmetric(
|
|
vertical: isMobile ? 12 : 16
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
// Réinitialiser le formulaire
|
|
_nomController.clear();
|
|
_prenomController.clear();
|
|
_emailController.clear();
|
|
_telephoneController.clear();
|
|
_adresseController.clear();
|
|
setState(() {
|
|
_quantites.clear();
|
|
_isLoading = false;
|
|
});
|
|
// Recharger les produits pour mettre à jour le stock
|
|
_loadProducts();
|
|
},
|
|
child: Text(
|
|
'OK',
|
|
style: TextStyle(fontSize: isMobile ? 14 : 16),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
} catch (e) {
|
|
setState(() {
|
|
_isLoading = false;
|
|
});
|
|
|
|
Get.snackbar(
|
|
'Erreur',
|
|
'Une erreur est survenue: ${e.toString()}',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_nomController.dispose();
|
|
_prenomController.dispose();
|
|
_emailController.dispose();
|
|
_telephoneController.dispose();
|
|
_adresseController.dispose();
|
|
|
|
// Disposal des contrôleurs de filtre
|
|
_searchNameController.dispose();
|
|
_searchImeiController.dispose();
|
|
_searchReferenceController.dispose();
|
|
|
|
super.dispose();
|
|
}
|
|
}
|