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.
 
 
 
 
 
 

923 lines
32 KiB

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:youmazgestion/Components/app_bar.dart';
import 'package:youmazgestion/Components/appDrawer.dart';
import 'package:youmazgestion/Models/client.dart';
import 'package:youmazgestion/Models/produit.dart';
import 'package:youmazgestion/Services/stock_managementDatabase.dart';
class HistoriquePage extends StatefulWidget {
const HistoriquePage({super.key});
@override
_HistoriquePageState createState() => _HistoriquePageState();
}
class _HistoriquePageState extends State<HistoriquePage> {
final AppDatabase _appDatabase = AppDatabase.instance;
// Listes pour les commandes
final List<Commande> _commandes = [];
final List<Commande> _filteredCommandes = [];
bool _isLoading = true;
DateTimeRange? _dateRange;
// Contrôleurs pour les filtres
final TextEditingController _searchController = TextEditingController();
final TextEditingController _searchClientController = TextEditingController();
final TextEditingController _searchCommandeIdController = TextEditingController();
// Variables de filtre
StatutCommande? _selectedStatut;
bool _showOnlyToday = false;
double? _minAmount;
double? _maxAmount;
@override
void initState() {
super.initState();
_loadCommandes();
// Listeners pour les filtres
_searchController.addListener(_filterCommandes);
_searchClientController.addListener(_filterCommandes);
_searchCommandeIdController.addListener(_filterCommandes);
}
Future<void> _loadCommandes() async {
setState(() {
_isLoading = true;
});
try {
final commandes = await _appDatabase.getCommandes();
setState(() {
_commandes.clear();
_commandes.addAll(commandes);
_filteredCommandes.clear();
_filteredCommandes.addAll(commandes);
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
});
Get.snackbar(
'Erreur',
'Impossible de charger les commandes: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
Future<void> _selectDateRange(BuildContext context) async {
final DateTimeRange? picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2020),
lastDate: DateTime.now().add(const Duration(days: 365)),
initialDateRange: _dateRange ?? DateTimeRange(
start: DateTime.now().subtract(const Duration(days: 30)),
end: DateTime.now(),
),
);
if (picked != null) {
setState(() {
_dateRange = picked;
});
_filterCommandes();
}
}
// Méthode pour filtrer les commandes
void _filterCommandes() {
final searchText = _searchController.text.toLowerCase();
final clientQuery = _searchClientController.text.toLowerCase();
final commandeIdQuery = _searchCommandeIdController.text.toLowerCase();
setState(() {
_filteredCommandes.clear();
for (var commande in _commandes) {
bool matchesSearch = searchText.isEmpty ||
commande.clientNom!.toLowerCase().contains(searchText) ||
commande.clientPrenom!.toLowerCase().contains(searchText) ||
commande.id.toString().contains(searchText);
bool matchesClient = clientQuery.isEmpty ||
commande.clientNom!.toLowerCase().contains(clientQuery) ||
commande.clientPrenom!.toLowerCase().contains(clientQuery);
bool matchesCommandeId = commandeIdQuery.isEmpty ||
commande.id.toString().contains(commandeIdQuery);
bool matchesStatut = _selectedStatut == null ||
commande.statut == _selectedStatut;
bool matchesDate = true;
if (_dateRange != null) {
final date = commande.dateCommande;
matchesDate = date.isAfter(_dateRange!.start) &&
date.isBefore(_dateRange!.end.add(const Duration(days: 1)));
}
bool matchesToday = !_showOnlyToday ||
_isToday(commande.dateCommande);
bool matchesAmount = true;
if (_minAmount != null && commande.montantTotal < _minAmount!) {
matchesAmount = false;
}
if (_maxAmount != null && commande.montantTotal > _maxAmount!) {
matchesAmount = false;
}
if (matchesSearch && matchesClient && matchesCommandeId &&
matchesStatut && matchesDate && matchesToday && matchesAmount) {
_filteredCommandes.add(commande);
}
}
});
}
bool _isToday(DateTime date) {
final now = DateTime.now();
return date.year == now.year &&
date.month == now.month &&
date.day == now.day;
}
// Toggle filtre aujourd'hui
void _toggleTodayFilter() {
setState(() {
_showOnlyToday = !_showOnlyToday;
});
_filterCommandes();
}
// Réinitialiser les filtres
void _clearFilters() {
setState(() {
_searchController.clear();
_searchClientController.clear();
_searchCommandeIdController.clear();
_selectedStatut = null;
_dateRange = null;
_showOnlyToday = false;
_minAmount = null;
_maxAmount = null;
});
_filterCommandes();
}
// Widget pour la section des filtres (adapté 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',
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 générale
TextField(
controller: _searchController,
decoration: InputDecoration(
labelText: 'Recherche',
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: _searchClientController,
decoration: InputDecoration(
labelText: 'Client',
prefixIcon: const Icon(Icons.person),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
),),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _searchCommandeIdController,
decoration: InputDecoration(
labelText: 'ID Commande',
prefixIcon: const Icon(Icons.confirmation_number),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
keyboardType: TextInputType.number,
),
),
],
),
] else ...[
// Version mobile - champs empilés
TextField(
controller: _searchClientController,
decoration: InputDecoration(
labelText: 'Client',
prefixIcon: const Icon(Icons.person),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
),
const SizedBox(height: 12),
TextField(
controller: _searchCommandeIdController,
decoration: InputDecoration(
labelText: 'ID Commande',
prefixIcon: const Icon(Icons.confirmation_number),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
keyboardType: TextInputType.number,
),
],
const SizedBox(height: 12),
// Dropdown pour le statut
DropdownButtonFormField<StatutCommande>(
value: _selectedStatut,
decoration: InputDecoration(
labelText: 'Statut',
prefixIcon: const Icon(Icons.assignment),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.grey.shade50,
),
items: [
const DropdownMenuItem<StatutCommande>(
value: null,
child: Text('Tous les statuts'),
),
...StatutCommande.values.map((StatutCommande statut) {
return DropdownMenuItem<StatutCommande>(
value: statut,
child: Text(_getStatutText(statut)),
);
}).toList(),
],
onChanged: (StatutCommande? newValue) {
setState(() {
_selectedStatut = newValue;
});
_filterCommandes();
},
),
const SizedBox(height: 16),
// Boutons de filtre rapide - adaptés pour mobile
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton.icon(
onPressed: _toggleTodayFilter,
icon: Icon(
_showOnlyToday ? Icons.today : Icons.calendar_today,
size: 20,
),
label: Text(_showOnlyToday
? isMobile ? 'Toutes dates' : 'Toutes les dates'
: isMobile ? 'Aujourd\'hui' : 'Aujourd\'hui seulement'),
style: ElevatedButton.styleFrom(
backgroundColor: _showOnlyToday
? Colors.green.shade600
: Colors.blue.shade600,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 12 : 16,
vertical: 8
),
),
),
ElevatedButton.icon(
onPressed: () => _selectDateRange(context),
icon: const Icon(Icons.date_range, size: 20),
label: Text(_dateRange != null
? isMobile ? 'Période' : 'Période sélectionnée'
: isMobile ? 'Période' : 'Choisir période'),
style: ElevatedButton.styleFrom(
backgroundColor: _dateRange != null
? Colors.orange.shade600
: Colors.grey.shade600,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 12 : 16,
vertical: 8
),
),
),
],
),
if (_dateRange != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.date_range,
size: 16,
color: Colors.orange.shade700),
const SizedBox(width: 4),
Text(
'${DateFormat('dd/MM/yyyy').format(_dateRange!.start)} - ${DateFormat('dd/MM/yyyy').format(_dateRange!.end)}',
style: TextStyle(
fontSize: isMobile ? 10 : 12,
color: Colors.orange.shade700,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 4),
GestureDetector(
onTap: () {
setState(() {
_dateRange = null;
});
_filterCommandes();
},
child: Icon(Icons.close,
size: 16,
color: Colors.orange.shade700),
),
],
),
),
),
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(
'${_filteredCommandes.length} commande(s)',
style: TextStyle(
color: Colors.blue.shade700,
fontWeight: FontWeight.w600,
fontSize: isMobile ? 12 : 14,
),
),
),
],
),
),
);
}
void _showCommandeDetails(Commande commande) async {
final details = await _appDatabase.getDetailsCommande(commande.id!);
final client = await _appDatabase.getClientById(commande.clientId);
Get.bottomSheet(
Container(
padding: const EdgeInsets.all(16),
height: MediaQuery.of(context).size.height * 0.85, // Plus grand sur mobile
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Icon(Icons.receipt_long, color: Colors.blue.shade700),
),
const SizedBox(width: 12),
Text(
'Commande #${commande.id}',
style: const TextStyle(
fontSize: 18, // Taille réduite pour mobile
fontWeight: FontWeight.bold,
),
),
],
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Get.back(),
),
],
),
const Divider(),
// Informations de la commande - version compacte pour mobile
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow('Client', '${client?.nom} ${client?.prenom}', Icons.person),
_buildDetailRow('Date', DateFormat('dd/MM/yyyy à HH:mm').format(commande.dateCommande), Icons.calendar_today),
Row(
children: [
Icon(Icons.assignment, size: 16, color: Colors.grey.shade600),
const SizedBox(width: 8),
const Text('Statut: ', style: TextStyle(fontWeight: FontWeight.w500)),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getStatutColor(commande.statut).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_getStatutText(commande.statut),
style: TextStyle(
color: _getStatutColor(commande.statut),
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey.shade700,
),
),
Text(
'${commande.montantTotal.toStringAsFixed(2)} MGA',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
],
),
),
const SizedBox(height: 12),
const Text(
'Articles:',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Expanded(
child: details.isEmpty
? const Center(
child: Text('Aucun détail disponible'),
)
: ListView.builder(
itemCount: details.length,
itemBuilder: (context, index) {
final detail = details[index];
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
elevation: 1,
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
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(
detail.produitNom ?? 'Produit inconnu',
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(
'${detail.quantite} x ${detail.prixUnitaire.toStringAsFixed(2)} MGA',
),
trailing: Text(
'${detail.sousTotal.toStringAsFixed(2)} MGA',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
),
),
);
},
),
),
if (commande.statut == StatutCommande.enAttente)
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14), // Plus compact
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () => _updateStatutCommande(commande.id!),
child: const Text('Marquer comme livrée'),
),
),
],
),
),
isScrollControlled: true,
);
}
Widget _buildDetailRow(String label, String value, IconData icon) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Icon(icon, size: 16, color: Colors.grey.shade600),
const SizedBox(width: 8),
Text('$label: ', style: const TextStyle(fontWeight: FontWeight.w500)),
Expanded(
child: Text(
value,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
);
}
String _getStatutText(StatutCommande statut) {
switch (statut) {
case StatutCommande.enAttente:
return 'En attente';
case StatutCommande.confirmee:
return 'Confirmée';
case StatutCommande.annulee:
return 'Annulée';
default:
return 'Inconnu';
}
}
Color _getStatutColor(StatutCommande statut) {
switch (statut) {
case StatutCommande.enAttente:
return Colors.orange;
case StatutCommande.confirmee:
return Colors.green;
case StatutCommande.annulee:
return Colors.red;
default:
return Colors.grey;
}
}
Future<void> _updateStatutCommande(int commandeId) async {
try {
await _appDatabase.updateStatutCommande(
commandeId, StatutCommande.confirmee);
Get.back(); // Ferme le bottom sheet
_loadCommandes();
Get.snackbar(
'Succès',
'Statut de la commande mis à jour',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible de mettre à jour le statut: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
// Widget pour l'état vide
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
children: [
Icon(
Icons.receipt_long_outlined,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucune commande trouvée',
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 pour l'item de commande (adapté pour mobile)
Widget _buildCommandeListItem(Commande commande) {
final isMobile = MediaQuery.of(context).size.width < 600;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () => _showCommandeDetails(commande),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Row(
children: [
Container(
width: isMobile ? 40 : 50,
height: isMobile ? 40 : 50,
decoration: BoxDecoration(
color: _getStatutColor(commande.statut).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.shopping_cart,
size: isMobile ? 20 : 24,
color: _getStatutColor(commande.statut),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Commande #${commande.id}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
'${commande.clientNom} ${commande.clientPrenom}',
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
Text(
DateFormat('dd/MM/yyyy').format(commande.dateCommande),
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${commande.montantTotal.toStringAsFixed(2)} MGA',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.green.shade700,
fontSize: isMobile ? 14 : 16,
),
),
const SizedBox(height: 4),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getStatutColor(commande.statut).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_getStatutText(commande.statut),
style: TextStyle(
color: _getStatutColor(commande.statut),
fontWeight: FontWeight.bold,
fontSize: isMobile ? 10 : 12,
),
),
),
],
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
final isMobile = MediaQuery.of(context).size.width < 600;
return Scaffold(
appBar: CustomAppBar(
title: 'Historique',
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadCommandes,
),
],
),
drawer: CustomDrawer(),
body: Column(
children: [
// Section des filtres - toujours visible mais plus compacte sur mobile
if (!isMobile) _buildFilterSection(),
// Sur mobile, on ajoute un bouton pour afficher les filtres dans un modal
if (isMobile) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: ElevatedButton.icon(
icon: const Icon(Icons.filter_alt),
label: const Text('Filtres'),
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(
'${_filteredCommandes.length} commande(s)',
style: TextStyle(
color: Colors.blue.shade700,
fontWeight: FontWeight.w600,
),
),
),
),
],
// Liste des commandes
Expanded(
child: _isLoading
? const Center(
child: CircularProgressIndicator(),
)
: _filteredCommandes.isEmpty
? _buildEmptyState()
: ListView.builder(
padding: const EdgeInsets.all(16.0),
itemCount: _filteredCommandes.length,
itemBuilder: (context, index) {
final commande = _filteredCommandes[index];
return _buildCommandeListItem(commande);
},
),
),
],
),
);
}
@override
void dispose() {
_searchController.dispose();
_searchClientController.dispose();
_searchCommandeIdController.dispose();
super.dispose();
}
}