import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/providers/equipment_provider.dart'; import 'package:em2rp/providers/local_user_provider.dart'; import 'package:em2rp/services/equipment_service.dart'; import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/views/widgets/nav/custom_app_bar.dart'; import 'package:intl/intl.dart'; import 'package:em2rp/views/equipment_form/brand_model_selector.dart'; import 'package:em2rp/views/equipment_form/subcategory_selector.dart'; import 'package:em2rp/utils/id_generator.dart'; import 'package:em2rp/utils/debug_log.dart'; import 'package:em2rp/utils/debouncer.dart'; class EquipmentFormPage extends StatefulWidget { final EquipmentModel? equipment; const EquipmentFormPage({super.key, this.equipment}); @override State createState() => _EquipmentFormPageState(); } class _EquipmentFormPageState extends State { final _formKey = GlobalKey(); final EquipmentService _equipmentService = EquipmentService(); // Controllers final TextEditingController _identifierController = TextEditingController(); final TextEditingController _brandController = TextEditingController(); final TextEditingController _modelController = TextEditingController(); final TextEditingController _subCategoryController = TextEditingController(); final TextEditingController _purchasePriceController = TextEditingController(); final TextEditingController _rentalPriceController = TextEditingController(); final TextEditingController _totalQuantityController = TextEditingController(); final TextEditingController _criticalThresholdController = TextEditingController(); final TextEditingController _notesController = TextEditingController(); final TextEditingController _quantityToAddController = TextEditingController(text: '1'); // Physical characteristics controllers final TextEditingController _weightController = TextEditingController(); final TextEditingController _lengthController = TextEditingController(); final TextEditingController _widthController = TextEditingController(); final TextEditingController _heightController = TextEditingController(); // State variables EquipmentCategory? _selectedCategory; // Nullable by default to force selection EquipmentStatus _selectedStatus = EquipmentStatus.available; DateTime? _purchaseDate; DateTime? _lastMaintenanceDate; DateTime? _nextMaintenanceDate; bool _isLoading = false; String? _selectedBrand; List _filteredModels = []; List _filteredSubCategories = []; // ID auto-generation and check final ValueNotifier _autoGenerateIdNotifier = ValueNotifier(true); final _idCheckDebouncer = Debouncer(delay: const Duration(milliseconds: 500)); String? _idConflictMessage; List _candidateIds = []; bool _isCalculatingIds = false; @override void initState() { _candidateIds = []; _isCalculatingIds = false; super.initState(); // Set default dates to today for new equipment if (widget.equipment == null) { _purchaseDate = DateTime.now(); _lastMaintenanceDate = DateTime.now(); } WidgetsBinding.instance.addPostFrameCallback((_) { final provider = Provider.of(context, listen: false); provider.loadBrands(); provider.loadModels(); if (widget.equipment != null) { if (_selectedBrand != null && _selectedBrand!.isNotEmpty) { _loadFilteredModels(_selectedBrand!); } if (_selectedCategory != null) { _loadFilteredSubCategories(_selectedCategory!); } } }); if (widget.equipment != null) { _populateFields(); } else { // Set up listeners for auto-generation of ID _brandController.addListener(_triggerCandidateIdsUpdate); _modelController.addListener(_triggerCandidateIdsUpdate); _quantityToAddController.addListener(_triggerCandidateIdsUpdate); _identifierController.addListener(_onIdentifierManualChanged); // Run initial check once the page is fully mounted/built WidgetsBinding.instance.addPostFrameCallback((_) { _triggerCandidateIdsUpdate(); }); } } void _populateFields() { final equipment = widget.equipment!; setState(() { _identifierController.text = equipment.id; _brandController.text = equipment.brand ?? ''; _selectedBrand = equipment.brand; _modelController.text = equipment.model ?? ''; _subCategoryController.text = equipment.subCategory ?? ''; _selectedCategory = equipment.category; _selectedStatus = equipment.status; _purchasePriceController.text = equipment.purchasePrice?.toStringAsFixed(2) ?? ''; _rentalPriceController.text = equipment.rentalPrice?.toStringAsFixed(2) ?? ''; _totalQuantityController.text = equipment.totalQuantity?.toString() ?? ''; _criticalThresholdController.text = equipment.criticalThreshold?.toString() ?? ''; _purchaseDate = equipment.purchaseDate; _lastMaintenanceDate = equipment.lastMaintenanceDate; _nextMaintenanceDate = equipment.nextMaintenanceDate; _notesController.text = equipment.notes ?? ''; _weightController.text = equipment.weight?.toString() ?? ''; _lengthController.text = equipment.length?.toString() ?? ''; _widthController.text = equipment.width?.toString() ?? ''; _heightController.text = equipment.height?.toString() ?? ''; }); // Disable auto-generation for editing _autoGenerateIdNotifier.value = false; DebugLog.info('[EquipmentForm] Populating fields for equipment: ${equipment.id}'); } void _onIdentifierManualChanged() { if (!_autoGenerateIdNotifier.value && widget.equipment == null) { _triggerCandidateIdsUpdate(); } } void _triggerCandidateIdsUpdate() { _idCheckDebouncer(() async { if (!mounted || widget.equipment != null) return; setState(() { _isCalculatingIds = true; }); try { final ids = await _calculateCandidateIds(); if (!mounted) return; setState(() { _candidateIds = ids; // If auto-generating, update the text field with the first generated ID if (_autoGenerateIdNotifier.value && ids.isNotEmpty) { _identifierController.removeListener(_onIdentifierManualChanged); _identifierController.text = ids.first; _identifierController.addListener(_onIdentifierManualChanged); } // Determine if there was an ID replacement/conflict _idConflictMessage = null; if (ids.isNotEmpty) { final brand = _brandController.text.trim(); final model = _modelController.text.trim(); final quantityText = _quantityToAddController.text.trim(); final numbers = _parseQuantityOrRange(quantityText); if (_autoGenerateIdNotifier.value) { if (numbers != null && numbers.isNotEmpty) { final firstExpectedNum = numbers.first; final baseId = IdGenerator.generateEquipmentId(brand: brand, model: model, number: null); final expectedFirstId = '${baseId}_#$firstExpectedNum'; if (ids.first != expectedFirstId) { _idConflictMessage = "L'ID $expectedFirstId était déjà pris et a été remplacé par ${ids.first}"; } } } else { final manualId = _identifierController.text.trim().toUpperCase(); if (manualId.isNotEmpty && ids.first != manualId) { _idConflictMessage = "L'ID $manualId était déjà pris et a été remplacé par ${ids.first}"; } } } }); } catch (e) { DebugLog.error("Error calculating candidate IDs: $e"); } finally { if (mounted) { setState(() { _isCalculatingIds = false; }); } } }); } List? _parseQuantityOrRange(String text) { text = text.trim(); if (text.isEmpty) return null; // Try single number final singleNum = int.tryParse(text); if (singleNum != null) { if (singleNum < 1 || singleNum > 100) return null; return List.generate(singleNum, (i) => i + 1); } // Try range pattern like "3-6" or "3 - 6" final rangeRegex = RegExp(r'^(\d+)\s*-\s*(\d+)$'); final match = rangeRegex.firstMatch(text); if (match != null) { final start = int.tryParse(match.group(1)!); final end = int.tryParse(match.group(2)!); if (start != null && end != null && start > 0 && end >= start) { if (end - start + 1 > 100) return null; return List.generate(end - start + 1, (i) => start + i); } } return null; } IdParseResult _parseBaseAndNumber(String id) { final match = RegExp(r'^(.*)_#(\d+)$').firstMatch(id); if (match != null) { return IdParseResult(match.group(1)!, int.parse(match.group(2)!)); } return IdParseResult(id, null); } Future> _calculateCandidateIds() async { final brand = _brandController.text.trim(); final model = _modelController.text.trim(); final quantityText = _quantityToAddController.text.trim(); if (_autoGenerateIdNotifier.value && brand.isEmpty && model.isEmpty) { return []; } // Get base ID String baseId; int? initialNumber; if (_autoGenerateIdNotifier.value) { baseId = IdGenerator.generateEquipmentId(brand: brand, model: model, number: null); } else { final manualId = _identifierController.text.trim().toUpperCase(); if (manualId.isEmpty) return []; final parsed = _parseBaseAndNumber(manualId); baseId = parsed.baseId; initialNumber = parsed.number; } // Parse numbers final numbers = _parseQuantityOrRange(quantityText); if (numbers == null || numbers.isEmpty) return []; // If the quantityText is just a single number (e.g. "5") and we have a manual ID with an initial number (e.g. "CUSTOM_#3"), // we adjust the numbers to start from that initial number. List targetNumbers = numbers; final isSingleNumberInput = int.tryParse(quantityText) != null; if (isSingleNumberInput && initialNumber != null) { targetNumbers = List.generate(numbers.length, (i) => initialNumber! + i); } List resultIds = []; final Set allocatedInBatch = {}; for (final num in targetNumbers) { int currentNum = num; String candidateId = '${baseId}_#$currentNum'; while (allocatedInBatch.contains(candidateId) || !(await _equipmentService.isIdUnique(candidateId))) { currentNum++; candidateId = '${baseId}_#$currentNum'; } resultIds.add(candidateId); allocatedInBatch.add(candidateId); } return resultIds; } Future _loadFilteredModels(String brand) async { try { final equipmentProvider = Provider.of(context, listen: false); final models = await equipmentProvider.loadModelsByBrand(brand); setState(() { _filteredModels = models; }); } catch (e) { setState(() { _filteredModels = []; }); } } Future _loadFilteredSubCategories(EquipmentCategory category) async { try { final equipmentProvider = Provider.of(context, listen: false); final subCategories = await equipmentProvider.loadSubCategoriesByCategory(category); setState(() { _filteredSubCategories = subCategories; }); } catch (e) { setState(() { _filteredSubCategories = []; }); } } @override void dispose() { _brandController.removeListener(_triggerCandidateIdsUpdate); _modelController.removeListener(_triggerCandidateIdsUpdate); _quantityToAddController.removeListener(_triggerCandidateIdsUpdate); _identifierController.removeListener(_onIdentifierManualChanged); _identifierController.dispose(); _brandController.dispose(); _modelController.dispose(); _subCategoryController.dispose(); _purchasePriceController.dispose(); _rentalPriceController.dispose(); _totalQuantityController.dispose(); _criticalThresholdController.dispose(); _notesController.dispose(); _quantityToAddController.dispose(); _weightController.dispose(); _lengthController.dispose(); _widthController.dispose(); _heightController.dispose(); _autoGenerateIdNotifier.dispose(); _idCheckDebouncer.dispose(); super.dispose(); } bool get _isConsumable => _selectedCategory != null && (_selectedCategory == EquipmentCategory.consumable || _selectedCategory == EquipmentCategory.cable); Widget _buildCard({ required String title, required IconData icon, required List children, }) { return Card( elevation: 2, shadowColor: Colors.black12, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade200, width: 1), ), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Icon(icon, color: AppColors.rouge, size: 20), const SizedBox(width: 8), Text( title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.noir, ), ), ], ), const Divider(height: 24, thickness: 1), ...children, ], ), ), ); } @override Widget build(BuildContext context) { final localUserProvider = Provider.of(context); final hasManagePermission = localUserProvider.hasPermission('manage_equipment'); final isEditing = widget.equipment != null; return Scaffold( appBar: CustomAppBar( title: isEditing ? 'Modifier l\'équipement' : 'Nouvel équipement', ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 800), child: SingleChildScrollView( padding: const EdgeInsets.all(24.0), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Card 1: Informations Générales _buildCard( title: 'Informations générales', icon: Icons.info_outline, children: [ // ID row with padlock and warning ValueListenableBuilder( valueListenable: _autoGenerateIdNotifier, builder: (context, autoGenerateId, child) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: TextFormField( controller: _identifierController, decoration: InputDecoration( labelText: 'Identifiant *', border: const OutlineInputBorder(), prefixIcon: const Icon(Icons.tag), hintText: isEditing ? null : 'Généré automatiquement', helperText: isEditing ? 'Non modifiable' : 'Identifiant unique du matériel', ), enabled: !autoGenerateId && !isEditing, validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer un identifiant'; } if (value.toUpperCase().startsWith('BOX_')) { return 'Les ID commençant par BOX_ sont réservés aux boites'; } return null; }, ), ), if (!isEditing) ...[ const SizedBox(width: 8), IconButton( icon: Icon( autoGenerateId ? Icons.lock : Icons.lock_open, color: autoGenerateId ? AppColors.rouge : Colors.grey, ), tooltip: autoGenerateId ? 'Génération automatique' : 'Saisie manuelle', onPressed: () { _autoGenerateIdNotifier.value = !autoGenerateId; if (_autoGenerateIdNotifier.value) { _triggerCandidateIdsUpdate(); } }, ), ], ], ), if (_idConflictMessage != null && !isEditing) ...[ const SizedBox(height: 6), Row( children: [ Icon(Icons.warning_amber_rounded, color: Colors.orange.shade800, size: 16), const SizedBox(width: 6), Expanded( child: Text( _idConflictMessage!, style: TextStyle( color: Colors.orange.shade800, fontSize: 12, fontWeight: FontWeight.w500, ), ), ), ], ), ], ], ); }, ), const SizedBox(height: 16), // Marque & Modèle BrandModelSelector( brandController: _brandController, modelController: _modelController, selectedBrand: _selectedBrand, filteredModels: _filteredModels, onBrandChanged: (brand) { setState(() { _selectedBrand = brand; }); if (brand != null && brand.isNotEmpty) { _loadFilteredModels(brand); } else { setState(() { _filteredModels = []; }); } }, onModelsChanged: (models) { setState(() { _filteredModels = models; }); }, ), const SizedBox(height: 16), // Catégorie et Statut Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: DropdownButtonFormField( initialValue: _selectedCategory, decoration: const InputDecoration( labelText: 'Catégorie *', border: OutlineInputBorder(), prefixIcon: Icon(Icons.category), ), items: EquipmentCategory.values.map((category) { return DropdownMenuItem( value: category, child: Text(category.label), ); }).toList(), validator: (value) { if (value == null) { return 'Catégorie obligatoire'; } return null; }, onChanged: (value) { if (value != null) { setState(() { _selectedCategory = value; _subCategoryController.clear(); }); _loadFilteredSubCategories(value); } }, ), ), if (!_isConsumable) ...[ const SizedBox(width: 16), Expanded( child: DropdownButtonFormField( initialValue: _selectedStatus, decoration: const InputDecoration( labelText: 'Statut *', border: OutlineInputBorder(), prefixIcon: Icon(Icons.info_outline), ), items: EquipmentStatus.values.map((status) { return DropdownMenuItem( value: status, child: Text(status.label), ); }).toList(), onChanged: (value) { if (value != null) { setState(() { _selectedStatus = value; }); } }, ), ), ], ], ), const SizedBox(height: 16), // Sous-catégorie SubCategorySelector( controller: _subCategoryController, selectedCategory: _selectedCategory, filteredSubCategories: _filteredSubCategories, onChanged: (value) { setState(() {}); }, ), ], ), const SizedBox(height: 20), // Card 2: Quantité & Stock if (!isEditing || _isConsumable) ...[ _buildCard( title: 'Quantité & Stock', icon: Icons.inventory_2_outlined, children: [ if (!isEditing) ...[ TextFormField( controller: _quantityToAddController, decoration: const InputDecoration( labelText: 'Nombre d\'exemplaires à créer *', border: OutlineInputBorder(), prefixIcon: Icon(Icons.copy), helperText: 'Exemples de valeurs acceptées :\n- "5" : crée 5 exemplaires (de #1 à #5)\n- "3-6" : crée 4 exemplaires (de #3 à #6)', helperMaxLines: 3, ), keyboardType: TextInputType.text, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp(r'[0-9\s-]')), ], validator: (value) { if (value == null || value.trim().isEmpty) { return 'Veuillez entrer une quantité ou une plage'; } final parsed = _parseQuantityOrRange(value); if (parsed == null || parsed.isEmpty) { return 'Format invalide (ex: "5" ou "3-6")'; } if (parsed.length > 100) { return 'La quantité maximale autorisée est de 100 exemplaires'; } return null; }, ), if (_candidateIds.isNotEmpty) ...[ const SizedBox(height: 16), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Identifiants qui seront créés (${_candidateIds.length}) :', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Colors.grey.shade700, ), ), if (_isCalculatingIds) const SizedBox( width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 1.5), ), ], ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: _candidateIds.map((id) { return Chip( label: Text( id, style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), ), backgroundColor: AppColors.rouge.withValues(alpha: 0.08), side: BorderSide(color: AppColors.rouge.withValues(alpha: 0.2)), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), ); }).toList(), ), ], ), ), ], if (_isConsumable) const SizedBox(height: 16), ], if (_isConsumable) ...[ Row( children: [ Expanded( child: TextFormField( controller: _totalQuantityController, decoration: const InputDecoration( labelText: 'Quantité totale', border: OutlineInputBorder(), prefixIcon: Icon(Icons.format_list_numbered), ), keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], ), ), const SizedBox(width: 16), Expanded( child: TextFormField( controller: _criticalThresholdController, decoration: const InputDecoration( labelText: 'Seuil critique', border: OutlineInputBorder(), prefixIcon: Icon(Icons.warning_amber), ), keyboardType: TextInputType.number, inputFormatters: [FilteringTextInputFormatter.digitsOnly], ), ), ], ), ], ], ), const SizedBox(height: 20), ], // Card 3: Informations Financières if (hasManagePermission) ...[ _buildCard( title: 'Informations financières', icon: Icons.euro_outlined, children: [ Row( children: [ Expanded( child: TextFormField( controller: _purchasePriceController, decoration: const InputDecoration( labelText: 'Prix d\'achat (€)', border: OutlineInputBorder(), prefixIcon: Icon(Icons.shopping_bag_outlined), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))], ), ), const SizedBox(width: 16), Expanded( child: TextFormField( controller: _rentalPriceController, decoration: const InputDecoration( labelText: 'Prix de location (€)', border: OutlineInputBorder(), prefixIcon: Icon(Icons.sell_outlined), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))], ), ), ], ), ], ), const SizedBox(height: 20), ], // Card 4: Caractéristiques physiques (Amélioration) _buildCard( title: 'Caractéristiques physiques', icon: Icons.scale_outlined, children: [ TextFormField( controller: _weightController, decoration: const InputDecoration( labelText: 'Poids à vide (kg)', border: OutlineInputBorder(), prefixIcon: Icon(Icons.scale), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), ), const SizedBox(height: 16), Row( children: [ Expanded( child: TextFormField( controller: _lengthController, decoration: const InputDecoration( labelText: 'Longueur (cm)', border: OutlineInputBorder(), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), ), ), const SizedBox(width: 8), Expanded( child: TextFormField( controller: _widthController, decoration: const InputDecoration( labelText: 'Largeur (cm)', border: OutlineInputBorder(), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), ), ), const SizedBox(width: 8), Expanded( child: TextFormField( controller: _heightController, decoration: const InputDecoration( labelText: 'Hauteur (cm)', border: OutlineInputBorder(), ), keyboardType: const TextInputType.numberWithOptions(decimal: true), ), ), ], ), ], ), const SizedBox(height: 20), // Card 5: Dates _buildCard( title: 'Dates & Maintenance', icon: Icons.calendar_today_outlined, children: [ _buildDateField(label: 'Date d\'achat', icon: Icons.shopping_cart_outlined, value: _purchaseDate, onTap: () => _selectDate(context, 'purchase')), const SizedBox(height: 16), _buildDateField(label: 'Dernière maintenance', icon: Icons.build_outlined, value: _lastMaintenanceDate, onTap: () => _selectDate(context, 'lastMaintenance')), const SizedBox(height: 16), _buildDateField(label: 'Prochaine maintenance', icon: Icons.event_outlined, value: _nextMaintenanceDate, onTap: () => _selectDate(context, 'nextMaintenance')), ], ), const SizedBox(height: 20), // Card 6: Notes _buildCard( title: 'Notes & Remarques', icon: Icons.notes_outlined, children: [ TextFormField( controller: _notesController, decoration: const InputDecoration( labelText: 'Notes complémentaires', border: OutlineInputBorder(), prefixIcon: Icon(Icons.edit_note), ), maxLines: 3, ), ], ), const SizedBox(height: 32), // Actions Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('Annuler', style: TextStyle(fontSize: 16)), ), const SizedBox(width: 16), ElevatedButton( onPressed: _saveEquipment, style: ElevatedButton.styleFrom( backgroundColor: AppColors.rouge, padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: Text( isEditing ? 'Enregistrer' : 'Créer', style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold) ), ), ], ), const SizedBox(height: 24), ], ), ), ), ), ), ); } Widget _buildDateField({required String label, required IconData icon, required DateTime? value, required VoidCallback onTap}) { return InkWell( onTap: onTap, child: InputDecorator( decoration: InputDecoration( labelText: label, border: const OutlineInputBorder(), prefixIcon: Icon(icon), suffixIcon: value != null ? IconButton( icon: const Icon(Icons.clear), onPressed: () { setState(() { if (label.contains('achat')) { _purchaseDate = null; } else if (label.contains('Dernière')) { _lastMaintenanceDate = null; } else if (label.contains('Prochaine')) { _nextMaintenanceDate = null; } }); }, ) : null, ), child: Text( value != null ? DateFormat('dd/MM/yyyy').format(value) : 'Sélectionner une date', style: TextStyle(color: value != null ? Colors.black : Colors.grey), ), ), ); } Future _selectDate(BuildContext context, String field) async { final DateTime? picked = await showDatePicker( context: context, initialDate: DateTime.now(), firstDate: DateTime(2000), lastDate: DateTime(2100), ); if (picked != null) { setState(() { switch (field) { case 'purchase': _purchaseDate = picked; break; case 'lastMaintenance': _lastMaintenanceDate = picked; break; case 'nextMaintenance': _nextMaintenanceDate = picked; break; } }); } } Future _saveEquipment() async { if (!_formKey.currentState!.validate()) return; final scaffoldMessenger = ScaffoldMessenger.of(context); final navigator = Navigator.of(context); setState(() => _isLoading = true); try { final equipmentProvider = Provider.of(context, listen: false); final isEditing = widget.equipment != null; int? availableQuantity; if (_isConsumable && _totalQuantityController.text.isNotEmpty) { final totalQuantity = int.parse(_totalQuantityController.text); if (isEditing && widget.equipment!.availableQuantity != null) { availableQuantity = widget.equipment!.availableQuantity; } else { availableQuantity = totalQuantity; } } // Validation marque/modèle obligatoires final brand = _brandController.text.trim(); final model = _modelController.text.trim(); if (brand.isEmpty || model.isEmpty) { scaffoldMessenger.showSnackBar( const SnackBar(content: Text('La marque et le modèle sont obligatoires')), ); return; } // Génération d'identifiant List ids = []; if (!isEditing) { ids = await _calculateCandidateIds(); if (ids.isEmpty) { scaffoldMessenger.showSnackBar( const SnackBar(content: Text('Impossible de générer des identifiants valides')), ); setState(() => _isLoading = false); return; } } else { ids.add(_identifierController.text.trim().toUpperCase()); } // Création/Mise à jour des équipements for (final id in ids) { final now = DateTime.now(); final equipment = EquipmentModel( id: id, name: id, // Utilisation de l'identifiant comme nom brand: brand, model: model, category: _selectedCategory!, subCategory: _subCategoryController.text.trim().isNotEmpty ? _subCategoryController.text.trim() : null, status: _selectedStatus, purchasePrice: _purchasePriceController.text.isNotEmpty ? double.tryParse(_purchasePriceController.text) : null, rentalPrice: _rentalPriceController.text.isNotEmpty ? double.tryParse(_rentalPriceController.text) : null, totalQuantity: _isConsumable ? int.tryParse(_totalQuantityController.text) : null, criticalThreshold: _isConsumable ? int.tryParse(_criticalThresholdController.text) : null, purchaseDate: _purchaseDate, lastMaintenanceDate: _lastMaintenanceDate, nextMaintenanceDate: _nextMaintenanceDate, notes: _notesController.text, createdAt: isEditing ? (widget.equipment?.createdAt ?? now) : now, updatedAt: now, availableQuantity: availableQuantity, weight: _weightController.text.isNotEmpty ? double.tryParse(_weightController.text) : null, length: _lengthController.text.isNotEmpty ? double.tryParse(_lengthController.text) : null, width: _widthController.text.isNotEmpty ? double.tryParse(_widthController.text) : null, height: _heightController.text.isNotEmpty ? double.tryParse(_heightController.text) : null, ); if (isEditing) { await equipmentProvider.updateEquipment(equipment); } else { await equipmentProvider.addEquipment(equipment); } } if (mounted) { navigator.pop(true); } } catch (e) { if (mounted) { scaffoldMessenger.showSnackBar( SnackBar(content: Text('Erreur lors de l\'enregistrement : $e')), ); } } finally { if (mounted) setState(() => _isLoading = false); } } } class IdParseResult { final String baseId; final int? number; IdParseResult(this.baseId, this.number); }