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.
 
 
 
 
 
 

837 lines
26 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',
),
],
),
);
}
}
// Votre code existant pour NouvelleCommandePage reste inchangé
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();
// Panier
final List<Product> _products = [];
final Map<int, int> _quantites = {};
// Utilisateurs commerciaux
List<Users> _commercialUsers = [];
Users? _selectedCommercialUser;
@override
void initState() {
super.initState();
_loadProducts();
_loadCommercialUsers();
}
Future<void> _loadProducts() async {
final products = await _appDatabase.getProducts();
setState(() {
_products.addAll(products);
});
}
Future<void> _loadCommercialUsers() async {
final commercialUsers = await _appDatabase.getCommercialUsers();
setState(() {
_commercialUsers = commercialUsers;
if (_commercialUsers.isNotEmpty) {
_selectedCommercialUser = _commercialUsers.first;
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: _buildFloatingCartButton(),
drawer: MediaQuery.of(context).size.width > 600 ? null : CustomDrawer(),
body: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade800, Colors.blue.shade600],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.shopping_cart,
color: Colors.blue,
size: 30,
),
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Nouvelle Commande',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Text(
'Créez une nouvelle commande pour un client',
style: TextStyle(
fontSize: 14,
color: Colors.white70,
),
),
],
),
),
],
),
],
),
),
// Contenu principal
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
),
onPressed: _showClientFormDialog,
child: const Text('Ajouter les informations client'),
),
const SizedBox(height: 20),
_buildProductList(),
],
),
),
),
],
),
);
}
Widget _buildFloatingCartButton() {
return FloatingActionButton.extended(
onPressed: () {
_showCartBottomSheet();
},
icon: const Icon(Icons.shopping_cart),
label: Text('Panier (${_quantites.values.where((q) => q > 0).length})'),
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
);
}
void _showClientFormDialog() {
Get.dialog(
AlertDialog(
title: const Text('Informations Client'),
content: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildTextFormField(
controller: _nomController,
label: 'Nom',
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un nom' : null,
),
const SizedBox(height: 12),
_buildTextFormField(
controller: _prenomController,
label: 'Prénom',
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un prénom' : null,
),
const SizedBox(height: 12),
_buildTextFormField(
controller: _emailController,
label: 'Email',
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value?.isEmpty ?? true) return 'Veuillez entrer un email';
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value!)) {
return 'Email invalide';
}
return null;
},
),
const SizedBox(height: 12),
_buildTextFormField(
controller: _telephoneController,
label: 'Téléphone',
keyboardType: TextInputType.phone,
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer un téléphone' : null,
),
const SizedBox(height: 12),
_buildTextFormField(
controller: _adresseController,
label: 'Adresse',
maxLines: 2,
validator: (value) => value?.isEmpty ?? true ? 'Veuillez entrer une adresse' : null,
),
const SizedBox(height: 12),
_buildCommercialDropdown(),
],
),
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Annuler'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
),
onPressed: () {
if (_formKey.currentState!.validate()) {
Get.back();
Get.snackbar(
'Succès',
'Informations client enregistrées',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
}
},
child: const Text('Enregistrer'),
),
],
),
);
}
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() {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Produits Disponibles',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 9, 56, 95),
),
),
const SizedBox(height: 16),
_products.isEmpty
? const Center(child: CircularProgressIndicator())
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _products.length,
itemBuilder: (context, index) {
final product = _products[index];
final quantity = _quantites[product.id] ?? 0;
return _buildProductListItem(product, quantity);
},
),
],
),
),
);
}
Widget _buildProductListItem(Product product, int quantity) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 8),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(Icons.shopping_bag, color: Colors.blue),
),
title: Text(
product.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
'${product.price.toStringAsFixed(2)} DA',
style: TextStyle(
color: Colors.green.shade700,
fontWeight: FontWeight.w600,
),
),
if (product.stock != null)
Text(
'Stock: ${product.stock}',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
trailing: Container(
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove, size: 18),
onPressed: () {
if (quantity > 0) {
setState(() {
_quantites[product.id!] = quantity - 1;
});
}
},
),
Text(
quantity.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.add, size: 18),
onPressed: () {
if (product.stock == null || quantity < product.stock!) {
setState(() {
_quantites[product.id!] = quantity + 1;
});
} else {
Get.snackbar(
'Stock insuffisant',
'Quantité demandée non disponible',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
},
),
],
),
),
),
);
}
void _showCartBottomSheet() {
Get.bottomSheet(
Container(
height: MediaQuery.of(context).size.height * 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: [
const Text(
'Votre Panier',
style: TextStyle(fontSize: 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)} DA'),
trailing: Text(
'${(entry.value * product.price).toStringAsFixed(2)} DA',
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)} DA',
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() {
return SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 4,
),
onPressed: _submitOrder,
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'Valider la Commande',
style: TextStyle(fontSize: 16),
),
),
);
}
Future<void> _submitOrder() async {
if (_nomController.text.isEmpty ||
_prenomController.text.isEmpty ||
_emailController.text.isEmpty ||
_telephoneController.text.isEmpty ||
_adresseController.text.isEmpty) {
Get.back(); // Ferme le bottom sheet
Get.snackbar(
'Informations manquantes',
'Veuillez remplir les informations client',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
_showClientFormDialog();
return;
}
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,
);
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);
Get.back(); // Ferme le bottom sheet
// Afficher le dialogue de confirmation
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Commande Validée'),
content: const Text('Votre commande a été enregistrée avec succès.'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
// Réinitialiser le formulaire
_nomController.clear();
_prenomController.clear();
_emailController.clear();
_telephoneController.clear();
_adresseController.clear();
setState(() {
_quantites.clear();
_isLoading = false;
});
},
child: const Text('OK'),
),
],
),
);
} 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();
super.dispose();
}
}