refactor: Ajout des sous-catégories et refonte de la gestion de l'appartenance
Cette mise à jour structurelle améliore la classification des équipements en introduisant la notion de sous-catégories et supprime la gestion directe de l'appartenance d'un équipement à une boîte (`parentBoxIds`). L'appartenance est désormais uniquement définie côté conteneur. Une nouvelle catégorie "Régie / Backline" est également ajoutée.
**Changements majeurs :**
- **Suppression de `parentBoxIds` sur `EquipmentModel` :**
- Le champ `parentBoxIds` a été retiré du modèle de données `EquipmentModel` et de toutes les logiques associées (création, mise à jour, copie).
- La responsabilité de lier un équipement à un conteneur est désormais exclusivement gérée par le `ContainerModel` via sa liste `equipmentIds`.
- La logique de synchronisation complexe dans `EquipmentFormPage` qui mettait à jour les conteneurs lors de la modification d'un équipement a été entièrement supprimée, simplifiant considérablement le code.
- Le sélecteur de boîtes parentes (`ParentBoxesSelector`) a été retiré du formulaire d'équipement.
- **Ajout des sous-catégories :**
- Un champ optionnel `subCategory` (String) a été ajouté au `EquipmentModel`.
- Le formulaire de création/modification d'équipement inclut désormais un nouveau champ "Sous-catégorie" avec autocomplétion.
- Ce champ est contextuel : il propose des suggestions basées sur les sous-catégories existantes pour la catégorie principale sélectionnée (ex: "Console", "Micro" pour la catégorie "Son").
- La sous-catégorie est maintenant affichée sur les fiches de détail des équipements et dans les listes de la page de gestion, améliorant la visibilité du classement.
**Nouvelle catégorie d'équipement :**
- Une nouvelle catégorie `backline` ("Régie / Backline") a été ajoutée à `EquipmentCategory` avec une icône (`Icons.piano`) et une couleur associée.
**Refactorisation et nettoyage :**
- Le `EquipmentProvider` et `EquipmentService` ont été mis à jour pour charger et filtrer les sous-catégories.
- De nombreuses instanciations d'un `EquipmentModel` vide (`dummy`) à travers l'application ont été nettoyées pour retirer la référence à `parentBoxIds`.
- **Version de l'application :**
- La version a été incrémentée à `1.0.4`.
This commit is contained in:
@@ -32,16 +32,16 @@ assets/assets/images/tshirt-incrust.webp,1737393735487,af7cb34adfca19c0b41c8eb63
|
||||
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
|
||||
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
||||
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
||||
version.json,1768522475061,bddd1ea3e020d4aacf09b657c819bf5f36dc702aec076e506cb492d21cd8f52a
|
||||
index.html,1768522480171,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
||||
flutter_service_worker.js,1768522568665,eba5c918ba4c29ed004c54e9bd663630cb2c583c3c71f06a8f2020b5752e7b01
|
||||
assets/FontManifest.json,1768522564284,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
||||
flutter_bootstrap.js,1768522480160,97c0e7a0a2dc5def7e6753ab86c08de3efaae84ac9f0203e29554f4274511bb2
|
||||
assets/AssetManifest.json,1768522564284,1e1501af5844823ef215cf650f4cef4002c0389d88770225ac07576d57dc1067
|
||||
assets/AssetManifest.bin.json,1768522564284,f446eb3de964f3a6f9e76fcc98d79a81b0429e076c9c7bf30cf8edd0263a0b0a
|
||||
assets/AssetManifest.bin,1768522564284,72bbccb69d9a02d3885df0c5e58ebfed29e25a4919e10bf195b59542f4709ca3
|
||||
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1768522567845,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
||||
assets/shaders/ink_sparkle.frag,1768522564576,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
||||
assets/fonts/MaterialIcons-Regular.otf,1768522567854,33efc485968dd28630ace587c22d6df359c195821b1114aaa85383e4d5394eac
|
||||
assets/NOTICES,1768522564286,fc20c3c3c998057eb7e58ad2e009c7268bf748bfde685e95130431f4c54bd51c
|
||||
main.dart.js,1768522561626,c106504190e4bf61c5a50eb6a4826ea8cddccd2310ffa359df51f047bf891acd
|
||||
version.json,1768586208886,5a25871ae727f23c4b7258c34108085b8711aa94f6fcab512e0c3ca00a429a64
|
||||
index.html,1768586225248,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
||||
flutter_service_worker.js,1768586307073,4ea31c373e15f13c2916a12d9d799905af2a79ff7ed0bcceb4334707910c7721
|
||||
flutter_bootstrap.js,1768586225225,e95b1b0bd493a475c8eed0e630e413d898f2ceff11cd9b24c6c564bbc2c5f5e9
|
||||
assets/FontManifest.json,1768586302952,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
||||
assets/AssetManifest.json,1768586302952,1e1501af5844823ef215cf650f4cef4002c0389d88770225ac07576d57dc1067
|
||||
assets/AssetManifest.bin.json,1768586302952,f446eb3de964f3a6f9e76fcc98d79a81b0429e076c9c7bf30cf8edd0263a0b0a
|
||||
assets/AssetManifest.bin,1768586302952,72bbccb69d9a02d3885df0c5e58ebfed29e25a4919e10bf195b59542f4709ca3
|
||||
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1768586306083,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
||||
assets/shaders/ink_sparkle.frag,1768586303187,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
||||
assets/fonts/MaterialIcons-Regular.otf,1768586306096,33efc485968dd28630ace587c22d6df359c195821b1114aaa85383e4d5394eac
|
||||
assets/NOTICES,1768586302954,fc20c3c3c998057eb7e58ad2e009c7268bf748bfde685e95130431f4c54bd51c
|
||||
main.dart.js,1768586301774,9b399ba21ab3247d46cf7dbcd5873aa248636bcd7864a1a0cedf1aae08608f9a
|
||||
|
||||
@@ -56,6 +56,7 @@ enum EquipmentCategory {
|
||||
consumable, // Consommable
|
||||
cable, // Câble
|
||||
vehicle, // Véhicule
|
||||
backline, // Régie / Backline
|
||||
other // Autre
|
||||
}
|
||||
|
||||
@@ -75,6 +76,8 @@ String equipmentCategoryToString(EquipmentCategory category) {
|
||||
return 'CABLE';
|
||||
case EquipmentCategory.vehicle:
|
||||
return 'VEHICLE';
|
||||
case EquipmentCategory.backline:
|
||||
return 'BACKLINE';
|
||||
case EquipmentCategory.other:
|
||||
return 'OTHER';
|
||||
case EquipmentCategory.effect:
|
||||
@@ -98,6 +101,8 @@ EquipmentCategory equipmentCategoryFromString(String? category) {
|
||||
return EquipmentCategory.cable;
|
||||
case 'VEHICLE':
|
||||
return EquipmentCategory.vehicle;
|
||||
case 'BACKLINE':
|
||||
return EquipmentCategory.backline;
|
||||
case 'EFFECT':
|
||||
return EquipmentCategory.effect;
|
||||
case 'OTHER':
|
||||
@@ -127,6 +132,8 @@ extension EquipmentCategoryExtension on EquipmentCategory {
|
||||
return 'Câble';
|
||||
case EquipmentCategory.vehicle:
|
||||
return 'Véhicule';
|
||||
case EquipmentCategory.backline:
|
||||
return 'Régie / Backline';
|
||||
case EquipmentCategory.other:
|
||||
return 'Autre';
|
||||
}
|
||||
@@ -151,6 +158,8 @@ extension EquipmentCategoryExtension on EquipmentCategory {
|
||||
return Icons.cable;
|
||||
case EquipmentCategory.vehicle:
|
||||
return Icons.local_shipping;
|
||||
case EquipmentCategory.backline:
|
||||
return Icons.piano;
|
||||
case EquipmentCategory.other:
|
||||
return Icons.more_horiz;
|
||||
}
|
||||
@@ -175,6 +184,8 @@ extension EquipmentCategoryExtension on EquipmentCategory {
|
||||
return Colors.grey;
|
||||
case EquipmentCategory.vehicle:
|
||||
return Colors.teal;
|
||||
case EquipmentCategory.backline:
|
||||
return Colors.indigo;
|
||||
case EquipmentCategory.other:
|
||||
return Colors.blueGrey;
|
||||
}
|
||||
@@ -193,6 +204,7 @@ extension EquipmentCategoryExtension on EquipmentCategory {
|
||||
case EquipmentCategory.effect:
|
||||
case EquipmentCategory.cable:
|
||||
case EquipmentCategory.vehicle:
|
||||
case EquipmentCategory.backline:
|
||||
case EquipmentCategory.other:
|
||||
return null;
|
||||
}
|
||||
@@ -312,6 +324,7 @@ class EquipmentModel {
|
||||
final String? brand; // Marque (indexé)
|
||||
final String? model; // Modèle (indexé)
|
||||
final EquipmentCategory category; // Catégorie
|
||||
final String? subCategory; // Sous-catégorie (indexé par catégorie)
|
||||
final EquipmentStatus status; // Statut actuel
|
||||
|
||||
// Prix (visible uniquement avec manage_equipment)
|
||||
@@ -323,8 +336,6 @@ class EquipmentModel {
|
||||
final int? availableQuantity; // Quantité disponible
|
||||
final int? criticalThreshold; // Seuil critique pour alerte
|
||||
|
||||
// Boîtes parentes (plusieurs possibles)
|
||||
final List<String> parentBoxIds; // IDs des boîtes contenant cet équipement
|
||||
|
||||
// Caractéristiques physiques
|
||||
final double? weight; // Poids (kg)
|
||||
@@ -354,13 +365,13 @@ class EquipmentModel {
|
||||
this.brand,
|
||||
this.model,
|
||||
required this.category,
|
||||
this.subCategory,
|
||||
this.status = EquipmentStatus.available,
|
||||
this.purchasePrice,
|
||||
this.rentalPrice,
|
||||
this.totalQuantity,
|
||||
this.availableQuantity,
|
||||
this.criticalThreshold,
|
||||
this.parentBoxIds = const [],
|
||||
this.weight,
|
||||
this.length,
|
||||
this.width,
|
||||
@@ -385,9 +396,6 @@ class EquipmentModel {
|
||||
}
|
||||
|
||||
// Gestion des listes
|
||||
final List<dynamic> parentBoxIdsRaw = map['parentBoxIds'] ?? [];
|
||||
final List<String> parentBoxIds = parentBoxIdsRaw.map((e) => e.toString()).toList();
|
||||
|
||||
final List<dynamic> maintenanceIdsRaw = map['maintenanceIds'] ?? [];
|
||||
final List<String> maintenanceIds = maintenanceIdsRaw.map((e) => e.toString()).toList();
|
||||
|
||||
@@ -397,13 +405,13 @@ class EquipmentModel {
|
||||
brand: map['brand'],
|
||||
model: map['model'],
|
||||
category: equipmentCategoryFromString(map['category']),
|
||||
subCategory: map['subCategory'],
|
||||
status: equipmentStatusFromString(map['status']),
|
||||
purchasePrice: map['purchasePrice']?.toDouble(),
|
||||
rentalPrice: map['rentalPrice']?.toDouble(),
|
||||
totalQuantity: map['totalQuantity']?.toInt(),
|
||||
availableQuantity: map['availableQuantity']?.toInt(),
|
||||
criticalThreshold: map['criticalThreshold']?.toInt(),
|
||||
parentBoxIds: parentBoxIds,
|
||||
weight: map['weight']?.toDouble(),
|
||||
length: map['length']?.toDouble(),
|
||||
width: map['width']?.toDouble(),
|
||||
@@ -424,13 +432,13 @@ class EquipmentModel {
|
||||
'brand': brand,
|
||||
'model': model,
|
||||
'category': equipmentCategoryToString(category),
|
||||
'subCategory': subCategory,
|
||||
'status': equipmentStatusToString(status),
|
||||
'purchasePrice': purchasePrice,
|
||||
'rentalPrice': rentalPrice,
|
||||
'totalQuantity': totalQuantity,
|
||||
'availableQuantity': availableQuantity,
|
||||
'criticalThreshold': criticalThreshold,
|
||||
'parentBoxIds': parentBoxIds,
|
||||
'weight': weight,
|
||||
'length': length,
|
||||
'width': width,
|
||||
@@ -452,13 +460,13 @@ class EquipmentModel {
|
||||
String? name,
|
||||
String? model,
|
||||
EquipmentCategory? category,
|
||||
String? subCategory,
|
||||
EquipmentStatus? status,
|
||||
double? purchasePrice,
|
||||
double? rentalPrice,
|
||||
int? totalQuantity,
|
||||
int? availableQuantity,
|
||||
int? criticalThreshold,
|
||||
List<String>? parentBoxIds,
|
||||
double? weight,
|
||||
double? length,
|
||||
double? width,
|
||||
@@ -478,13 +486,13 @@ class EquipmentModel {
|
||||
name: name ?? this.name,
|
||||
model: model ?? this.model,
|
||||
category: category ?? this.category,
|
||||
subCategory: subCategory ?? this.subCategory,
|
||||
status: status ?? this.status,
|
||||
purchasePrice: purchasePrice ?? this.purchasePrice,
|
||||
rentalPrice: rentalPrice ?? this.rentalPrice,
|
||||
totalQuantity: totalQuantity ?? this.totalQuantity,
|
||||
availableQuantity: availableQuantity ?? this.availableQuantity,
|
||||
criticalThreshold: criticalThreshold ?? this.criticalThreshold,
|
||||
parentBoxIds: parentBoxIds ?? this.parentBoxIds,
|
||||
weight: weight ?? this.weight,
|
||||
length: length ?? this.length,
|
||||
width: width ?? this.width,
|
||||
|
||||
@@ -94,7 +94,6 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
name: '',
|
||||
category: EquipmentCategory.other,
|
||||
status: EquipmentStatus.available,
|
||||
parentBoxIds: [],
|
||||
maintenanceIds: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
@@ -287,6 +286,18 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
return modelsByBrand;
|
||||
}
|
||||
|
||||
/// Charger les sous-catégories d'une catégorie spécifique
|
||||
Future<List<String>> loadSubCategoriesByCategory(EquipmentCategory category) async {
|
||||
// Filtrer les sous-catégories par catégorie
|
||||
final subCategoriesByCategory = _equipment
|
||||
.where((eq) => eq.category == category && eq.subCategory != null && eq.subCategory!.isNotEmpty)
|
||||
.map((eq) => eq.subCategory!)
|
||||
.toSet()
|
||||
.toList()
|
||||
..sort();
|
||||
return subCategoriesByCategory;
|
||||
}
|
||||
|
||||
/// Calculer le statut réel d'un équipement (compatibilité)
|
||||
Future<EquipmentStatus> calculateRealStatus(EquipmentModel equipment) async {
|
||||
// Pour l'instant, retourner le statut stocké
|
||||
|
||||
@@ -313,6 +313,30 @@ class EquipmentService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupérer les sous-catégories filtrées par catégorie
|
||||
Future<List<String>> getSubCategoriesByCategory(EquipmentCategory category) async {
|
||||
try {
|
||||
final equipmentsData = await _dataService.getEquipments();
|
||||
final subCategories = <String>{};
|
||||
|
||||
final categoryString = equipmentCategoryToString(category);
|
||||
|
||||
for (var data in equipmentsData) {
|
||||
if (data['category'] == categoryString) {
|
||||
final subCategory = data['subCategory'] as String?;
|
||||
if (subCategory != null && subCategory.isNotEmpty) {
|
||||
subCategories.add(subCategory);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return subCategories.toList()..sort();
|
||||
} catch (e) {
|
||||
print('Error getting subcategories by category: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifier si un ID existe déjà
|
||||
Future<bool> isIdUnique(String id) async {
|
||||
try {
|
||||
|
||||
@@ -624,6 +624,8 @@ class _ContainerDetailPageState extends State<ContainerDetailPage> {
|
||||
return 'Câble';
|
||||
case EquipmentCategory.vehicle:
|
||||
return 'Véhicule';
|
||||
case EquipmentCategory.backline:
|
||||
return 'Régie / Backline';
|
||||
case EquipmentCategory.other:
|
||||
return 'Autre';
|
||||
}
|
||||
|
||||
@@ -914,6 +914,8 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
||||
return 'Câble';
|
||||
case EquipmentCategory.vehicle:
|
||||
return 'Véhicule';
|
||||
case EquipmentCategory.backline:
|
||||
return 'Régie / Backline';
|
||||
case EquipmentCategory.other:
|
||||
return 'Autre';
|
||||
}
|
||||
@@ -937,6 +939,8 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
||||
return Icons.cable;
|
||||
case EquipmentCategory.vehicle:
|
||||
return Icons.local_shipping;
|
||||
case EquipmentCategory.backline:
|
||||
return Icons.piano;
|
||||
case EquipmentCategory.other:
|
||||
return Icons.category;
|
||||
}
|
||||
|
||||
@@ -707,7 +707,6 @@ class _ContainerManagementPageState extends State<ContainerManagementPage>
|
||||
name: '',
|
||||
category: EquipmentCategory.other,
|
||||
status: EquipmentStatus.available,
|
||||
parentBoxIds: [],
|
||||
maintenanceIds: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
|
||||
@@ -249,6 +249,17 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
|
||||
'${widget.equipment.brand ?? ''} ${widget.equipment.model ?? ''}'.trim(),
|
||||
style: TextStyle(color: Colors.grey[700]),
|
||||
),
|
||||
if (widget.equipment.subCategory != null && widget.equipment.subCategory!.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'📁 ${widget.equipment.subCategory}',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 13,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
66
em2rp/lib/views/equipment_form/subcategory_selector.dart
Normal file
66
em2rp/lib/views/equipment_form/subcategory_selector.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/equipment_model.dart';
|
||||
|
||||
/// Widget de sélection de sous-catégorie avec autocomplétion
|
||||
/// Similaire au système Brand/Model mais filtré par catégorie
|
||||
class SubCategorySelector extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final EquipmentCategory? selectedCategory;
|
||||
final List<String> filteredSubCategories;
|
||||
final ValueChanged<String?>? onChanged;
|
||||
|
||||
const SubCategorySelector({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.selectedCategory,
|
||||
required this.filteredSubCategories,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Autocomplete<String>(
|
||||
initialValue: TextEditingValue(text: controller.text),
|
||||
optionsBuilder: (TextEditingValue textEditingValue) {
|
||||
if (selectedCategory == null) {
|
||||
return const Iterable<String>.empty();
|
||||
}
|
||||
if (textEditingValue.text.isEmpty) {
|
||||
return filteredSubCategories;
|
||||
}
|
||||
return filteredSubCategories.where((String subCategory) {
|
||||
return subCategory.toLowerCase().contains(
|
||||
textEditingValue.text.toLowerCase(),
|
||||
);
|
||||
});
|
||||
},
|
||||
onSelected: (String selection) {
|
||||
controller.text = selection;
|
||||
onChanged?.call(selection);
|
||||
},
|
||||
fieldViewBuilder: (context, fieldController, focusNode, onEditingComplete) {
|
||||
if (fieldController.text != controller.text) {
|
||||
fieldController.text = controller.text;
|
||||
}
|
||||
return TextFormField(
|
||||
controller: fieldController,
|
||||
focusNode: focusNode,
|
||||
enabled: selectedCategory != null,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Sous-catégorie',
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.category_outlined),
|
||||
hintText: selectedCategory == null
|
||||
? 'Catégorie requise'
|
||||
: 'Saisissez la sous-catégorie',
|
||||
helperText: 'Optionnel - Permet un classement plus précis',
|
||||
),
|
||||
onChanged: (value) {
|
||||
controller.text = value;
|
||||
onChanged?.call(value.isNotEmpty ? value : null);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,16 @@ 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/models/container_model.dart';
|
||||
import 'package:em2rp/providers/equipment_provider.dart';
|
||||
import 'package:em2rp/providers/container_provider.dart';
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:em2rp/services/equipment_service.dart';
|
||||
import 'package:em2rp/services/container_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/views/widgets/equipment/parent_boxes_selector.dart';
|
||||
|
||||
class EquipmentFormPage extends StatefulWidget {
|
||||
final EquipmentModel? equipment;
|
||||
@@ -33,6 +30,7 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
||||
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();
|
||||
@@ -46,18 +44,15 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
||||
DateTime? _purchaseDate;
|
||||
DateTime? _lastMaintenanceDate;
|
||||
DateTime? _nextMaintenanceDate;
|
||||
List<String> _selectedParentBoxIds = [];
|
||||
List<ContainerModel> _availableBoxes = [];
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingBoxes = true;
|
||||
bool _addMultiple = false;
|
||||
String? _selectedBrand;
|
||||
List<String> _filteredModels = [];
|
||||
List<String> _filteredSubCategories = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadAvailableBoxes();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final provider = Provider.of<EquipmentProvider>(context, listen: false);
|
||||
provider.loadBrands();
|
||||
@@ -75,6 +70,7 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
||||
_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) ?? '';
|
||||
@@ -89,51 +85,15 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
||||
|
||||
DebugLog.info('[EquipmentForm] Populating fields for equipment: ${equipment.id}');
|
||||
|
||||
// Charger les containers contenant cet équipement depuis Firestore
|
||||
_loadCurrentContainers(equipment.id);
|
||||
|
||||
if (_selectedBrand != null && _selectedBrand!.isNotEmpty) {
|
||||
_loadFilteredModels(_selectedBrand!);
|
||||
}
|
||||
|
||||
// Charger les sous-catégories pour la catégorie sélectionnée
|
||||
_loadFilteredSubCategories(_selectedCategory);
|
||||
}
|
||||
|
||||
/// Charge les containers qui contiennent actuellement cet équipement
|
||||
Future<void> _loadCurrentContainers(String equipmentId) async {
|
||||
try {
|
||||
final containers = await containerEquipmentService.getContainersByEquipment(equipmentId);
|
||||
setState(() {
|
||||
_selectedParentBoxIds = containers.map((c) => c.id).toList();
|
||||
});
|
||||
DebugLog.info('[EquipmentForm] Loaded ${containers.length} containers for equipment $equipmentId');
|
||||
DebugLog.info('[EquipmentForm] Selected container IDs: $_selectedParentBoxIds');
|
||||
} catch (e) {
|
||||
DebugLog.error('[EquipmentForm] Error loading containers for equipment', e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadAvailableBoxes() async {
|
||||
try {
|
||||
final boxes = await _equipmentService.getBoxes();
|
||||
DebugLog.info('[EquipmentForm] Loaded ${boxes.length} boxes from service');
|
||||
for (var box in boxes) {
|
||||
DebugLog.info('[EquipmentForm] Box loaded - ID: ${box.id}, Name: ${box.name}');
|
||||
}
|
||||
setState(() {
|
||||
_availableBoxes = boxes;
|
||||
_isLoadingBoxes = false;
|
||||
});
|
||||
} catch (e) {
|
||||
DebugLog.error('[EquipmentForm] Error loading boxes', e);
|
||||
setState(() {
|
||||
_isLoadingBoxes = false;
|
||||
});
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Erreur lors du chargement des boîtes : $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadFilteredModels(String brand) async {
|
||||
try {
|
||||
@@ -149,11 +109,26 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadFilteredSubCategories(EquipmentCategory category) async {
|
||||
try {
|
||||
final equipmentProvider = Provider.of<EquipmentProvider>(context, listen: false);
|
||||
final subCategories = await equipmentProvider.loadSubCategoriesByCategory(category);
|
||||
setState(() {
|
||||
_filteredSubCategories = subCategories;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_filteredSubCategories = [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_identifierController.dispose();
|
||||
_brandController.dispose();
|
||||
_modelController.dispose();
|
||||
_subCategoryController.dispose();
|
||||
_purchasePriceController.dispose();
|
||||
_rentalPriceController.dispose();
|
||||
_totalQuantityController.dispose();
|
||||
@@ -312,7 +287,9 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedCategory = value;
|
||||
_subCategoryController.clear();
|
||||
});
|
||||
_loadFilteredSubCategories(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -348,6 +325,19 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Sous-catégorie
|
||||
SubCategorySelector(
|
||||
controller: _subCategoryController,
|
||||
selectedCategory: _selectedCategory,
|
||||
filteredSubCategories: _filteredSubCategories,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
// La valeur est déjà dans le controller
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Prix
|
||||
if (hasManagePermission) ...[
|
||||
Row(
|
||||
@@ -419,18 +409,6 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Boîtes parentes
|
||||
const SizedBox(height: 8),
|
||||
_isLoadingBoxes
|
||||
? const Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
)
|
||||
: _buildParentBoxesSelector(),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Dates
|
||||
const Divider(),
|
||||
const Text('Dates', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
@@ -481,17 +459,6 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildParentBoxesSelector() {
|
||||
return ParentBoxesSelector(
|
||||
availableBoxes: _availableBoxes,
|
||||
selectedBoxIds: _selectedParentBoxIds,
|
||||
onSelectionChanged: (newSelection) {
|
||||
setState(() {
|
||||
_selectedParentBoxIds = newSelection;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDateField({required String label, required IconData icon, required DateTime? value, required VoidCallback onTap}) {
|
||||
return InkWell(
|
||||
@@ -629,6 +596,7 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
||||
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,
|
||||
@@ -637,7 +605,6 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
||||
purchaseDate: _purchaseDate,
|
||||
lastMaintenanceDate: _lastMaintenanceDate,
|
||||
nextMaintenanceDate: _nextMaintenanceDate,
|
||||
parentBoxIds: [], // On ne stocke plus les parentBoxIds dans l'équipement
|
||||
notes: _notesController.text,
|
||||
createdAt: isEditing ? (widget.equipment?.createdAt ?? now) : now,
|
||||
updatedAt: now,
|
||||
@@ -645,58 +612,8 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
||||
);
|
||||
if (isEditing) {
|
||||
await equipmentProvider.updateEquipment(equipment);
|
||||
|
||||
// Synchroniser les containers : mettre à jour equipmentIds des containers
|
||||
// Charger les anciens containers depuis Firestore
|
||||
final oldContainers = await containerEquipmentService.getContainersByEquipment(equipment.id);
|
||||
final oldParentBoxIds = oldContainers.map((c) => c.id).toList();
|
||||
final newParentBoxIds = _selectedParentBoxIds;
|
||||
|
||||
// Boîtes ajoutées : ajouter cet équipement à leur equipmentIds
|
||||
final addedBoxes = newParentBoxIds.where((id) => !oldParentBoxIds.contains(id));
|
||||
for (final boxId in addedBoxes) {
|
||||
try {
|
||||
final containerProvider = Provider.of<ContainerProvider>(context, listen: false);
|
||||
await containerProvider.addEquipmentToContainer(
|
||||
containerId: boxId,
|
||||
equipmentId: equipment.id,
|
||||
);
|
||||
DebugLog.info('[EquipmentForm] Added equipment ${equipment.id} to container $boxId');
|
||||
} catch (e) {
|
||||
DebugLog.error('[EquipmentForm] Error adding equipment to container $boxId', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Boîtes retirées : retirer cet équipement de leur equipmentIds
|
||||
final removedBoxes = oldParentBoxIds.where((id) => !newParentBoxIds.contains(id));
|
||||
for (final boxId in removedBoxes) {
|
||||
try {
|
||||
final containerProvider = Provider.of<ContainerProvider>(context, listen: false);
|
||||
await containerProvider.removeEquipmentFromContainer(
|
||||
containerId: boxId,
|
||||
equipmentId: equipment.id,
|
||||
);
|
||||
DebugLog.info('[EquipmentForm] Removed equipment ${equipment.id} from container $boxId');
|
||||
} catch (e) {
|
||||
DebugLog.error('[EquipmentForm] Error removing equipment from container $boxId', e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await equipmentProvider.addEquipment(equipment);
|
||||
|
||||
// Pour un nouvel équipement, ajouter à tous les containers sélectionnés
|
||||
for (final boxId in _selectedParentBoxIds) {
|
||||
try {
|
||||
final containerProvider = Provider.of<ContainerProvider>(context, listen: false);
|
||||
await containerProvider.addEquipmentToContainer(
|
||||
containerId: boxId,
|
||||
equipmentId: equipment.id,
|
||||
);
|
||||
DebugLog.info('[EquipmentForm] Added new equipment ${equipment.id} to container $boxId');
|
||||
} catch (e) {
|
||||
DebugLog.error('[EquipmentForm] Error adding new equipment to container $boxId', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -559,6 +559,18 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
||||
: 'Marque/Modèle non défini',
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
||||
),
|
||||
// Afficher la sous-catégorie si elle existe
|
||||
if (equipment.subCategory != null && equipment.subCategory!.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'📁 ${equipment.subCategory}',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[500],
|
||||
fontSize: 12,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
],
|
||||
// Afficher la quantité disponible pour les consommables/câbles
|
||||
if (equipment.category == EquipmentCategory.consumable ||
|
||||
equipment.category == EquipmentCategory.cable) ...[
|
||||
@@ -1099,7 +1111,6 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
||||
name: '',
|
||||
category: EquipmentCategory.other,
|
||||
status: EquipmentStatus.available,
|
||||
parentBoxIds: [],
|
||||
maintenanceIds: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
|
||||
@@ -165,7 +165,6 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
||||
name: 'Équipement inconnu',
|
||||
category: EquipmentCategory.other,
|
||||
status: EquipmentStatus.available,
|
||||
parentBoxIds: [],
|
||||
maintenanceIds: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
|
||||
@@ -104,6 +104,8 @@ class ContainerEquipmentTile extends StatelessWidget {
|
||||
return 'Câble';
|
||||
case EquipmentCategory.vehicle:
|
||||
return 'Véhicule';
|
||||
case EquipmentCategory.backline:
|
||||
return 'Régie / Backline';
|
||||
case EquipmentCategory.other:
|
||||
return 'Autre';
|
||||
}
|
||||
|
||||
@@ -766,7 +766,6 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
name: 'Inconnu',
|
||||
category: EquipmentCategory.other,
|
||||
status: EquipmentStatus.available,
|
||||
parentBoxIds: [],
|
||||
maintenanceIds: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
@@ -1156,7 +1155,6 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
name: '',
|
||||
category: EquipmentCategory.other,
|
||||
status: EquipmentStatus.available,
|
||||
parentBoxIds: [],
|
||||
maintenanceIds: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
|
||||
@@ -84,7 +84,6 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
name: 'Équipement inconnu',
|
||||
category: EquipmentCategory.other,
|
||||
status: EquipmentStatus.available,
|
||||
parentBoxIds: [],
|
||||
maintenanceIds: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
@@ -179,7 +178,6 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
name: 'Inconnu',
|
||||
category: EquipmentCategory.other,
|
||||
status: EquipmentStatus.available,
|
||||
parentBoxIds: [],
|
||||
maintenanceIds: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
@@ -259,7 +257,6 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
name: 'Inconnu',
|
||||
category: EquipmentCategory.other,
|
||||
status: EquipmentStatus.available,
|
||||
parentBoxIds: [],
|
||||
maintenanceIds: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"updateUrl": "https://app.em2events.fr",
|
||||
"forceUpdate": true,
|
||||
"releaseNotes": "Cette version apporte des outils majeurs pour faciliter la gestion de votre parc et de vos événements :\r\n\r\n* **Scanner QR Code :** Retrouvez instantanément la fiche d'un équipement ou d'un conteneur en scannant son code directement depuis l'application. La génération des codes a également été rendue plus fluide.\r\n* **Centre de Notifications & Alertes :** Ne ratez plus rien ! Un nouveau système d'alertes (dans l'app et par email) vous prévient des maintenances, équipements manquants ou conflits. Vous pouvez configurer vos préférences d'envoi.",
|
||||
"timestamp": "2026-01-16T00:14:35.059Z"
|
||||
"timestamp": "2026-01-16T17:56:48.878Z"
|
||||
}
|
||||
Reference in New Issue
Block a user