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.
452 lines
15 KiB
452 lines
15 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/Services/stock_managementDatabase.dart';
|
|
import 'package:youmazgestion/controller/userController.dart';
|
|
|
|
class ApprobationSortiesPage extends StatefulWidget {
|
|
const ApprobationSortiesPage({super.key});
|
|
|
|
@override
|
|
_ApprobationSortiesPageState createState() => _ApprobationSortiesPageState();
|
|
}
|
|
|
|
class _ApprobationSortiesPageState extends State<ApprobationSortiesPage> {
|
|
final AppDatabase _database = AppDatabase.instance;
|
|
final UserController _userController = Get.find<UserController>();
|
|
|
|
List<Map<String, dynamic>> _sortiesEnAttente = [];
|
|
bool _isLoading = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadSortiesEnAttente();
|
|
}
|
|
|
|
Future<void> _loadSortiesEnAttente() async {
|
|
setState(() => _isLoading = true);
|
|
try {
|
|
final sorties = await _database.getSortiesPersonnellesEnAttente();
|
|
setState(() {
|
|
_sortiesEnAttente = sorties;
|
|
_isLoading = false;
|
|
});
|
|
} catch (e) {
|
|
setState(() => _isLoading = false);
|
|
Get.snackbar('Erreur', 'Impossible de charger les demandes: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _approuverSortie(Map<String, dynamic> sortie) async {
|
|
final confirm = await Get.dialog<bool>(
|
|
AlertDialog(
|
|
title: const Text('Approuver la demande'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text('Produit: ${sortie['produit_nom']}'),
|
|
Text('Quantité: ${sortie['quantite']}'),
|
|
Text('Demandeur: ${sortie['admin_nom']}'),
|
|
Text('Motif: ${sortie['motif']}'),
|
|
Text('Note: ${sortie['notes']}'),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Confirmer l\'approbation de cette demande de sortie personnelle ?',
|
|
style: TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Get.back(result: false),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Get.back(result: true),
|
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.green),
|
|
child: const Text('Approuver', style: TextStyle(color: Colors.white)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirm == true) {
|
|
try {
|
|
await _database.approuverSortiePersonnelle(
|
|
sortie['id'] as int,
|
|
_userController.userId,
|
|
);
|
|
|
|
Get.snackbar(
|
|
'Demande approuvée',
|
|
'La sortie personnelle a été approuvée et le stock mis à jour',
|
|
backgroundColor: Colors.green,
|
|
colorText: Colors.white,
|
|
);
|
|
|
|
_loadSortiesEnAttente();
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Erreur',
|
|
'Impossible d\'approuver la demande: $e',
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _refuserSortie(Map<String, dynamic> sortie) async {
|
|
final motifController = TextEditingController();
|
|
|
|
final confirm = await Get.dialog<bool>(
|
|
AlertDialog(
|
|
title: const Text('Refuser la demande'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text('Demande de: ${sortie['admin_nom']}'),
|
|
Text('Produit: ${sortie['produit_nom']}'),
|
|
const SizedBox(height: 16),
|
|
TextField(
|
|
controller: motifController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Motif du refus *',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
maxLines: 3,
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Get.back(result: false),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
if (motifController.text.trim().isNotEmpty) {
|
|
Get.back(result: true);
|
|
} else {
|
|
Get.snackbar('Erreur', 'Veuillez indiquer un motif de refus');
|
|
}
|
|
},
|
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
|
child: const Text('Refuser', style: TextStyle(color: Colors.white)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (confirm == true && motifController.text.trim().isNotEmpty) {
|
|
try {
|
|
await _database.refuserSortiePersonnelle(
|
|
sortie['id'] as int,
|
|
_userController.userId,
|
|
motifController.text.trim(),
|
|
);
|
|
|
|
Get.snackbar(
|
|
'Demande refusée',
|
|
'La sortie personnelle a été refusée',
|
|
backgroundColor: Colors.orange,
|
|
colorText: Colors.white,
|
|
);
|
|
|
|
_loadSortiesEnAttente();
|
|
} catch (e) {
|
|
Get.snackbar(
|
|
'Erreur',
|
|
'Impossible de refuser la demande: $e',
|
|
backgroundColor: Colors.red,
|
|
colorText: Colors.white,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: CustomAppBar(title: 'Approbation sorties personnelles'),
|
|
drawer: CustomDrawer(),
|
|
body: _isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: RefreshIndicator(
|
|
onRefresh: _loadSortiesEnAttente,
|
|
child: _sortiesEnAttente.isEmpty
|
|
? const Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.inbox, size: 64, color: Colors.grey),
|
|
SizedBox(height: 16),
|
|
Text(
|
|
'Aucune demande en attente',
|
|
style: TextStyle(fontSize: 18, color: Colors.grey),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
: ListView.builder(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: _sortiesEnAttente.length,
|
|
itemBuilder: (context, index) {
|
|
final sortie = _sortiesEnAttente[index];
|
|
return _buildSortieCard(sortie);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildSortieCard(Map<String, dynamic> sortie) {
|
|
final dateSortie = DateTime.parse(sortie['date_sortie'].toString());
|
|
|
|
return Card(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
elevation: 2,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// En-tête avec statut
|
|
Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.orange.shade100,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
'EN ATTENTE',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.orange.shade700,
|
|
),
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Text(
|
|
DateFormat('dd/MM/yyyy HH:mm').format(dateSortie),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Informations du produit
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.blue.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.inventory, color: Colors.blue.shade700, size: 16),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Produit demandé',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.blue.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
sortie['produit_nom'].toString(),
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
children: [
|
|
Text('Référence: ${sortie['produit_reference'] ?? 'N/A'}'),
|
|
const SizedBox(width: 16),
|
|
Text('Stock actuel: ${sortie['stock_actuel']}'),
|
|
const SizedBox(width: 16),
|
|
Text(
|
|
'Quantité demandée: ${sortie['quantite']}',
|
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Informations du demandeur
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.green.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.person, color: Colors.green.shade700, size: 16),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Demandeur',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.green.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'${sortie['admin_nom']} ${sortie['admin_nom_famille'] ?? ''}',
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
if (sortie['point_vente_nom'] != null)
|
|
Text('Point de vente: ${sortie['point_vente_nom']}'),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Motif
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.purple.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(Icons.note, color: Colors.purple.shade700, size: 16),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
'Motif',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.purple.shade700,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
sortie['motif'].toString(),
|
|
style: const TextStyle(fontSize: 14),
|
|
),
|
|
if (sortie['notes'] != null && sortie['notes'].toString().isNotEmpty) ...[
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Notes: ${sortie['notes']}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: Colors.grey.shade600,
|
|
fontStyle: FontStyle.italic,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Vérification de stock
|
|
if ((sortie['stock_actuel'] as int) < (sortie['quantite'] as int))
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.red.shade50,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.red.shade300),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.warning, color: Colors.red.shade700),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'ATTENTION: Stock insuffisant pour cette demande',
|
|
style: TextStyle(
|
|
color: Colors.red.shade700,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Boutons d'action
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: () => _refuserSortie(sortie),
|
|
icon: const Icon(Icons.close, size: 18),
|
|
label: const Text('Refuser'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red.shade600,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: ElevatedButton.icon(
|
|
onPressed: ((sortie['stock_actuel'] as int) >= (sortie['quantite'] as int))
|
|
? () => _approuverSortie(sortie)
|
|
: null,
|
|
icon: const Icon(Icons.check, size: 18),
|
|
label: const Text('Approuver'),
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.green.shade600,
|
|
foregroundColor: Colors.white,
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|