feat: updated container management system with core models, providers, and UI pages
This commit is contained in:
@@ -34,16 +34,16 @@ assets/assets/images/tshirt-incrust.webp,1737393735487,af7cb34adfca19c0b41c8eb63
|
|||||||
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
|
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
|
||||||
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
||||||
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
||||||
version.json,1779800968600,00f600f01984c1e371af870e40f78fd44ba53d05596f2f92f9b4fc56a85f52b6
|
version.json,1779802456392,7626a7c596308bd2eb1add2ed984cd6dda5d4a3f0dedb3338244d2ae45c496cf
|
||||||
index.html,1779800974065,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
index.html,1779802461141,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
||||||
flutter_service_worker.js,1779801065196,879f42a05578f24ea45dc23326fdda6246d38dc59de0824ef8d4edfa4715e571
|
flutter_service_worker.js,1779802555396,fadec2c9a1e8e16c22e332aef080b0b2aacc3998c4e260e5821b79afb9e000da
|
||||||
flutter_bootstrap.js,1779800974054,977a20d5caac8da21af648cae8fa7dba00a5cd959fd2fafa7ef538a012fe87c3
|
flutter_bootstrap.js,1779802461128,ad20054b92acf16bb75fbffd65f81c63c6d3cb6d752f799230dca5f2118af783
|
||||||
assets/FontManifest.json,1779801061892,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
assets/FontManifest.json,1779802551869,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
||||||
assets/AssetManifest.json,1779801061891,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6
|
assets/AssetManifest.json,1779802551869,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6
|
||||||
assets/AssetManifest.bin.json,1779801061892,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53
|
assets/AssetManifest.bin.json,1779802551869,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53
|
||||||
assets/AssetManifest.bin,1779801061891,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907
|
assets/AssetManifest.bin,1779802551869,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907
|
||||||
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1779801064441,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
assets/shaders/ink_sparkle.frag,1779802552052,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
||||||
assets/shaders/ink_sparkle.frag,1779801062097,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1779802554433,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
||||||
assets/fonts/MaterialIcons-Regular.otf,1779801064446,710dc8fc35289048b52970355f64206fb1b2c5e67c71ae77a46b53f0e2daecd6
|
assets/fonts/MaterialIcons-Regular.otf,1779802554440,710dc8fc35289048b52970355f64206fb1b2c5e67c71ae77a46b53f0e2daecd6
|
||||||
assets/NOTICES,1779801061893,5522e1307c65771d1fbf26fcd9dc0548c751413f42196c4acaba5ee674eede1e
|
assets/NOTICES,1779802551871,5522e1307c65771d1fbf26fcd9dc0548c751413f42196c4acaba5ee674eede1e
|
||||||
main.dart.js,1779801060964,e87fb4dfca93c3384b5cb63d627186e48b0c15d78ed63bc7f5e61544ef292dd9
|
main.dart.js,1779802550741,ab892e930c97940c1ea4ff33079922082c7f688047d307acad0644a78cfda2d7
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ void main() {
|
|||||||
// Ne pas effectuer d'initialisations asynchrones lourdes ici.
|
// Ne pas effectuer d'initialisations asynchrones lourdes ici.
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
if (kReleaseMode) {
|
||||||
|
debugPrint = (String? message, {int? wrapWidth}) {};
|
||||||
|
}
|
||||||
|
|
||||||
|
runZonedGuarded(
|
||||||
|
() {
|
||||||
runApp(
|
runApp(
|
||||||
MultiProvider(
|
MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
@@ -81,6 +87,20 @@ void main() {
|
|||||||
child: const MyApp(),
|
child: const MyApp(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
(error, stackTrace) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print('Uncaught error: $error\n$stackTrace');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
zoneSpecification: ZoneSpecification(
|
||||||
|
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
|
||||||
|
if (!kReleaseMode) {
|
||||||
|
parent.print(zone, line);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatefulWidget {
|
class MyApp extends StatefulWidget {
|
||||||
|
|||||||
@@ -242,34 +242,55 @@ class ContainerModel {
|
|||||||
|
|
||||||
/// Factory depuis Firestore
|
/// Factory depuis Firestore
|
||||||
factory ContainerModel.fromMap(Map<String, dynamic> map, String id) {
|
factory ContainerModel.fromMap(Map<String, dynamic> map, String id) {
|
||||||
// Fonction helper pour convertir Timestamp ou String ISO en DateTime
|
// Fonction helper pour convertir de manière sécurisée en double
|
||||||
|
double? parseDouble(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is num) return value.toDouble();
|
||||||
|
if (value is String) return double.tryParse(value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction helper pour convertir Timestamp ou String ISO ou int epoch en DateTime
|
||||||
DateTime? parseDate(dynamic value) {
|
DateTime? parseDate(dynamic value) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
if (value is Timestamp) return value.toDate();
|
if (value is Timestamp) return value.toDate();
|
||||||
if (value is String) return DateTime.tryParse(value);
|
if (value is String) return DateTime.tryParse(value);
|
||||||
|
if (value is int) return DateTime.fromMillisecondsSinceEpoch(value);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<dynamic> equipmentIdsRaw = map['equipmentIds'] ?? [];
|
// Gestion sécurisée de la liste d'IDs d'équipements
|
||||||
final List<String> equipmentIds = equipmentIdsRaw.map((e) => e.toString()).toList();
|
final List<String> equipmentIds = [];
|
||||||
|
if (map['equipmentIds'] is List) {
|
||||||
|
for (final e in map['equipmentIds'] as List) {
|
||||||
|
if (e != null) {
|
||||||
|
equipmentIds.add(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final List<dynamic> historyRaw = map['history'] ?? [];
|
// Gestion sécurisée de l'historique
|
||||||
final List<ContainerHistoryEntry> history = historyRaw
|
final List<ContainerHistoryEntry> history = [];
|
||||||
.map((e) => ContainerHistoryEntry.fromMap(e as Map<String, dynamic>))
|
if (map['history'] is List) {
|
||||||
.toList();
|
for (final e in map['history'] as List) {
|
||||||
|
if (e is Map<String, dynamic>) {
|
||||||
|
history.add(ContainerHistoryEntry.fromMap(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ContainerModel(
|
return ContainerModel(
|
||||||
id: id,
|
id: id,
|
||||||
name: map['name'] ?? '',
|
name: (map['name'] ?? '').toString(),
|
||||||
type: containerTypeFromString(map['type']),
|
type: containerTypeFromString(map['type']?.toString()),
|
||||||
status: equipmentStatusFromString(map['status']),
|
status: equipmentStatusFromString(map['status']?.toString()),
|
||||||
weight: map['weight']?.toDouble(),
|
weight: parseDouble(map['weight']),
|
||||||
length: map['length']?.toDouble(),
|
length: parseDouble(map['length']),
|
||||||
width: map['width']?.toDouble(),
|
width: parseDouble(map['width']),
|
||||||
height: map['height']?.toDouble(),
|
height: parseDouble(map['height']),
|
||||||
equipmentIds: equipmentIds,
|
equipmentIds: equipmentIds,
|
||||||
eventId: map['eventId'],
|
eventId: map['eventId']?.toString(),
|
||||||
notes: map['notes'],
|
notes: map['notes']?.toString(),
|
||||||
createdAt: parseDate(map['createdAt']) ?? DateTime.now(),
|
createdAt: parseDate(map['createdAt']) ?? DateTime.now(),
|
||||||
updatedAt: parseDate(map['updatedAt']) ?? DateTime.now(),
|
updatedAt: parseDate(map['updatedAt']) ?? DateTime.now(),
|
||||||
history: history,
|
history: history,
|
||||||
@@ -355,16 +376,17 @@ class ContainerHistoryEntry {
|
|||||||
if (value == null) return DateTime.now();
|
if (value == null) return DateTime.now();
|
||||||
if (value is Timestamp) return value.toDate();
|
if (value is Timestamp) return value.toDate();
|
||||||
if (value is String) return DateTime.tryParse(value) ?? DateTime.now();
|
if (value is String) return DateTime.tryParse(value) ?? DateTime.now();
|
||||||
|
if (value is int) return DateTime.fromMillisecondsSinceEpoch(value);
|
||||||
return DateTime.now();
|
return DateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ContainerHistoryEntry(
|
return ContainerHistoryEntry(
|
||||||
timestamp: parseDate(map['timestamp']),
|
timestamp: parseDate(map['timestamp']),
|
||||||
action: map['action'] ?? '',
|
action: (map['action'] ?? '').toString(),
|
||||||
equipmentId: map['equipmentId'],
|
equipmentId: map['equipmentId']?.toString(),
|
||||||
previousValue: map['previousValue'],
|
previousValue: map['previousValue']?.toString(),
|
||||||
newValue: map['newValue'],
|
newValue: map['newValue']?.toString(),
|
||||||
userId: map['userId'],
|
userId: map['userId']?.toString(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -387,40 +387,64 @@ class EquipmentModel {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory EquipmentModel.fromMap(Map<String, dynamic> map, String id) {
|
factory EquipmentModel.fromMap(Map<String, dynamic> map, String id) {
|
||||||
// Fonction helper pour convertir Timestamp ou String ISO en DateTime
|
// Fonction helper pour convertir de manière sécurisée en double
|
||||||
|
double? parseDouble(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is num) return value.toDouble();
|
||||||
|
if (value is String) return double.tryParse(value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction helper pour convertir de manière sécurisée en int
|
||||||
|
int? parseInt(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is num) return value.toInt();
|
||||||
|
if (value is String) return int.tryParse(value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction helper pour convertir Timestamp ou String ISO ou int epoch en DateTime
|
||||||
DateTime? parseDate(dynamic value) {
|
DateTime? parseDate(dynamic value) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
if (value is Timestamp) return value.toDate();
|
if (value is Timestamp) return value.toDate();
|
||||||
if (value is String) return DateTime.tryParse(value);
|
if (value is String) return DateTime.tryParse(value);
|
||||||
|
if (value is int) return DateTime.fromMillisecondsSinceEpoch(value);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion des listes
|
// Gestion sécurisée des listes d'IDs de maintenance
|
||||||
final List<dynamic> maintenanceIdsRaw = map['maintenanceIds'] ?? [];
|
final List<String> maintenanceIds = [];
|
||||||
final List<String> maintenanceIds = maintenanceIdsRaw.map((e) => e.toString()).toList();
|
if (map['maintenanceIds'] is List) {
|
||||||
|
for (final e in map['maintenanceIds'] as List) {
|
||||||
|
if (e != null) {
|
||||||
|
maintenanceIds.add(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return EquipmentModel(
|
return EquipmentModel(
|
||||||
id: id,
|
id: id,
|
||||||
name: map['name'] ?? '',
|
name: (map['name'] ?? '').toString(),
|
||||||
brand: map['brand'],
|
brand: map['brand']?.toString(),
|
||||||
model: map['model'],
|
model: map['model']?.toString(),
|
||||||
category: equipmentCategoryFromString(map['category']),
|
category: equipmentCategoryFromString(map['category']?.toString()),
|
||||||
subCategory: map['subCategory'],
|
subCategory: map['subCategory']?.toString(),
|
||||||
status: equipmentStatusFromString(map['status']),
|
status: equipmentStatusFromString(map['status']?.toString()),
|
||||||
purchasePrice: map['purchasePrice']?.toDouble(),
|
purchasePrice: parseDouble(map['purchasePrice']),
|
||||||
rentalPrice: map['rentalPrice']?.toDouble(),
|
rentalPrice: parseDouble(map['rentalPrice']),
|
||||||
totalQuantity: map['totalQuantity']?.toInt(),
|
totalQuantity: parseInt(map['totalQuantity']),
|
||||||
availableQuantity: map['availableQuantity']?.toInt(),
|
availableQuantity: parseInt(map['availableQuantity']),
|
||||||
criticalThreshold: map['criticalThreshold']?.toInt(),
|
criticalThreshold: parseInt(map['criticalThreshold']),
|
||||||
weight: map['weight']?.toDouble(),
|
weight: parseDouble(map['weight']),
|
||||||
length: map['length']?.toDouble(),
|
length: parseDouble(map['length']),
|
||||||
width: map['width']?.toDouble(),
|
width: parseDouble(map['width']),
|
||||||
height: map['height']?.toDouble(),
|
height: parseDouble(map['height']),
|
||||||
purchaseDate: parseDate(map['purchaseDate']),
|
purchaseDate: parseDate(map['purchaseDate']),
|
||||||
|
lastMaintenanceDate: parseDate(map['lastMaintenanceDate']),
|
||||||
nextMaintenanceDate: parseDate(map['nextMaintenanceDate']),
|
nextMaintenanceDate: parseDate(map['nextMaintenanceDate']),
|
||||||
maintenanceIds: maintenanceIds,
|
maintenanceIds: maintenanceIds,
|
||||||
imageUrl: map['imageUrl'],
|
imageUrl: map['imageUrl']?.toString(),
|
||||||
notes: map['notes'],
|
notes: map['notes']?.toString(),
|
||||||
createdAt: parseDate(map['createdAt']) ?? DateTime.now(),
|
createdAt: parseDate(map['createdAt']) ?? DateTime.now(),
|
||||||
updatedAt: parseDate(map['updatedAt']) ?? DateTime.now(),
|
updatedAt: parseDate(map['updatedAt']) ?? DateTime.now(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -60,29 +60,44 @@ class MaintenanceModel {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory MaintenanceModel.fromMap(Map<String, dynamic> map, String id) {
|
factory MaintenanceModel.fromMap(Map<String, dynamic> map, String id) {
|
||||||
// Fonction helper pour convertir Timestamp ou String ISO en DateTime
|
// Fonction helper pour convertir de manière sécurisée en double
|
||||||
|
double? parseDouble(dynamic value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value is num) return value.toDouble();
|
||||||
|
if (value is String) return double.tryParse(value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction helper pour convertir Timestamp ou String ISO ou int epoch en DateTime
|
||||||
DateTime? parseDate(dynamic value) {
|
DateTime? parseDate(dynamic value) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
if (value is Timestamp) return value.toDate();
|
if (value is Timestamp) return value.toDate();
|
||||||
if (value is String) return DateTime.tryParse(value);
|
if (value is String) return DateTime.tryParse(value);
|
||||||
|
if (value is int) return DateTime.fromMillisecondsSinceEpoch(value);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion de la liste des équipements
|
// Gestion de la liste des équipements
|
||||||
final List<dynamic> equipmentIdsRaw = map['equipmentIds'] ?? [];
|
final List<String> equipmentIds = [];
|
||||||
final List<String> equipmentIds = equipmentIdsRaw.map((e) => e.toString()).toList();
|
if (map['equipmentIds'] is List) {
|
||||||
|
for (final e in map['equipmentIds'] as List) {
|
||||||
|
if (e != null) {
|
||||||
|
equipmentIds.add(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return MaintenanceModel(
|
return MaintenanceModel(
|
||||||
id: id,
|
id: id,
|
||||||
equipmentIds: equipmentIds,
|
equipmentIds: equipmentIds,
|
||||||
type: maintenanceTypeFromString(map['type']),
|
type: maintenanceTypeFromString(map['type']?.toString()),
|
||||||
scheduledDate: parseDate(map['scheduledDate']) ?? DateTime.now(),
|
scheduledDate: parseDate(map['scheduledDate']) ?? DateTime.now(),
|
||||||
completedDate: parseDate(map['completedDate']),
|
completedDate: parseDate(map['completedDate']),
|
||||||
name: map['name'] ?? '',
|
name: (map['name'] ?? '').toString(),
|
||||||
description: map['description'] ?? '',
|
description: (map['description'] ?? '').toString(),
|
||||||
performedBy: map['performedBy'],
|
performedBy: map['performedBy']?.toString(),
|
||||||
cost: map['cost']?.toDouble(),
|
cost: parseDouble(map['cost']),
|
||||||
notes: map['notes'],
|
notes: map['notes']?.toString(),
|
||||||
createdAt: parseDate(map['createdAt']) ?? DateTime.now(),
|
createdAt: parseDate(map['createdAt']) ?? DateTime.now(),
|
||||||
updatedAt: parseDate(map['updatedAt']) ?? DateTime.now(),
|
updatedAt: parseDate(map['updatedAt']) ?? DateTime.now(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class EquipmentProvider extends ChangeNotifier {
|
|||||||
String _searchQuery = '';
|
String _searchQuery = '';
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
bool _isFullListLoaded = false;
|
||||||
|
|
||||||
// Mode de chargement (pagination vs full)
|
// Mode de chargement (pagination vs full)
|
||||||
bool _usePagination = false;
|
bool _usePagination = false;
|
||||||
@@ -48,6 +49,7 @@ class EquipmentProvider extends ChangeNotifier {
|
|||||||
bool get isLoadingMore => _isLoadingMore;
|
bool get isLoadingMore => _isLoadingMore;
|
||||||
bool get hasMore => _hasMore;
|
bool get hasMore => _hasMore;
|
||||||
bool get isInitialized => _isInitialized;
|
bool get isInitialized => _isInitialized;
|
||||||
|
bool get isFullListLoaded => _isFullListLoaded;
|
||||||
bool get usePagination => _usePagination;
|
bool get usePagination => _usePagination;
|
||||||
|
|
||||||
/// S'assure que les équipements sont chargés (charge si nécessaire)
|
/// S'assure que les équipements sont chargés (charge si nécessaire)
|
||||||
@@ -58,16 +60,8 @@ class EquipmentProvider extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si initialisé MAIS _equipment est vide, forcer le rechargement
|
// Si déjà initialisé avec le cache complet des données, ne rien faire
|
||||||
if (_isInitialized && _equipment.isEmpty) {
|
if (_isFullListLoaded) {
|
||||||
print('[EquipmentProvider] Equipment marked as initialized but _equipment is empty! Force reloading...');
|
|
||||||
_isInitialized = false; // Réinitialiser le flag
|
|
||||||
await loadEquipments();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si déjà initialisé avec des données, ne rien faire
|
|
||||||
if (_isInitialized) {
|
|
||||||
print('[EquipmentProvider] Equipment already loaded (${_equipment.length} items), skipping...');
|
print('[EquipmentProvider] Equipment already loaded (${_equipment.length} items), skipping...');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -120,7 +114,7 @@ class EquipmentProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
// Extraire les modèles et marques uniques
|
// Extraire les modèles et marques uniques
|
||||||
_extractUniqueValues();
|
_extractUniqueValues();
|
||||||
|
_isFullListLoaded = true;
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -436,6 +430,8 @@ class EquipmentProvider extends ChangeNotifier {
|
|||||||
Future<void> deleteEquipment(String equipmentId, {bool forceDelete = false}) async {
|
Future<void> deleteEquipment(String equipmentId, {bool forceDelete = false}) async {
|
||||||
try {
|
try {
|
||||||
await _dataService.deleteEquipment(equipmentId, forceDelete: forceDelete);
|
await _dataService.deleteEquipment(equipmentId, forceDelete: forceDelete);
|
||||||
|
_isFullListLoaded = false;
|
||||||
|
_equipment.clear();
|
||||||
if (_usePagination) {
|
if (_usePagination) {
|
||||||
await reload();
|
await reload();
|
||||||
} else {
|
} else {
|
||||||
@@ -451,6 +447,8 @@ class EquipmentProvider extends ChangeNotifier {
|
|||||||
Future<void> addEquipment(EquipmentModel equipment) async {
|
Future<void> addEquipment(EquipmentModel equipment) async {
|
||||||
try {
|
try {
|
||||||
await _dataService.createEquipment(equipment.id, equipment.toMap());
|
await _dataService.createEquipment(equipment.id, equipment.toMap());
|
||||||
|
_isFullListLoaded = false;
|
||||||
|
_equipment.clear();
|
||||||
if (_usePagination) {
|
if (_usePagination) {
|
||||||
await reload();
|
await reload();
|
||||||
} else {
|
} else {
|
||||||
@@ -466,6 +464,8 @@ class EquipmentProvider extends ChangeNotifier {
|
|||||||
Future<void> updateEquipment(EquipmentModel equipment) async {
|
Future<void> updateEquipment(EquipmentModel equipment) async {
|
||||||
try {
|
try {
|
||||||
await _dataService.updateEquipment(equipment.id, equipment.toMap());
|
await _dataService.updateEquipment(equipment.id, equipment.toMap());
|
||||||
|
_isFullListLoaded = false;
|
||||||
|
_equipment.clear();
|
||||||
if (_usePagination) {
|
if (_usePagination) {
|
||||||
await reload();
|
await reload();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:em2rp/utils/id_generator.dart';
|
|||||||
import 'package:em2rp/services/data_service.dart';
|
import 'package:em2rp/services/data_service.dart';
|
||||||
import 'package:em2rp/services/api_service.dart';
|
import 'package:em2rp/services/api_service.dart';
|
||||||
import 'package:em2rp/utils/debouncer.dart';
|
import 'package:em2rp/utils/debouncer.dart';
|
||||||
|
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
||||||
|
|
||||||
class ContainerFormPage extends StatefulWidget {
|
class ContainerFormPage extends StatefulWidget {
|
||||||
final ContainerModel? container;
|
final ContainerModel? container;
|
||||||
@@ -90,18 +91,65 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildCard({
|
||||||
|
required String title,
|
||||||
|
required IconData icon,
|
||||||
|
required List<Widget> 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: CustomAppBar(
|
||||||
title: Text(_isEditing ? 'Modifier boite' : 'Nouvelle boite'),
|
title: _isEditing ? 'Modifier boîte' : 'Nouvelle boîte',
|
||||||
backgroundColor: AppColors.rouge,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
),
|
),
|
||||||
body: Form(
|
body: Center(
|
||||||
key: _formKey,
|
child: ConstrainedBox(
|
||||||
child: ListView(
|
constraints: const BoxConstraints(maxWidth: 800),
|
||||||
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
|
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: [
|
children: [
|
||||||
// Nom
|
// Nom
|
||||||
TextFormField(
|
TextFormField(
|
||||||
@@ -183,7 +231,13 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
|||||||
items: ContainerType.values.map((type) {
|
items: ContainerType.values.map((type) {
|
||||||
return DropdownMenuItem(
|
return DropdownMenuItem(
|
||||||
value: type,
|
value: type,
|
||||||
child: Text(type.label),
|
child: Row(
|
||||||
|
children: [
|
||||||
|
type.getIcon(size: 20, color: AppColors.rouge),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(type.label),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
@@ -203,7 +257,7 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Statut *',
|
labelText: 'Statut *',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
prefixIcon: Icon(Icons.info),
|
prefixIcon: Icon(Icons.info_outline),
|
||||||
),
|
),
|
||||||
items: [
|
items: [
|
||||||
EquipmentStatus.available,
|
EquipmentStatus.available,
|
||||||
@@ -241,18 +295,15 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
],
|
||||||
|
|
||||||
// Section Caractéristiques physiques
|
|
||||||
Text(
|
|
||||||
'Caractéristiques physiques',
|
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
|
// Card 2: Caractéristiques Physiques
|
||||||
|
_buildCard(
|
||||||
|
title: 'Caractéristiques physiques',
|
||||||
|
icon: Icons.scale_outlined,
|
||||||
|
children: [
|
||||||
// Poids
|
// Poids
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _weightController,
|
controller: _weightController,
|
||||||
@@ -262,8 +313,7 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
|||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
prefixIcon: Icon(Icons.scale),
|
prefixIcon: Icon(Icons.scale),
|
||||||
),
|
),
|
||||||
keyboardType:
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value != null && value.isNotEmpty) {
|
if (value != null && value.isNotEmpty) {
|
||||||
if (double.tryParse(value) == null) {
|
if (double.tryParse(value) == null) {
|
||||||
@@ -285,8 +335,7 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
|||||||
labelText: 'Longueur (cm)',
|
labelText: 'Longueur (cm)',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
keyboardType:
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
TextInputType.numberWithOptions(decimal: true),
|
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value != null && value.isNotEmpty) {
|
if (value != null && value.isNotEmpty) {
|
||||||
if (double.tryParse(value) == null) {
|
if (double.tryParse(value) == null) {
|
||||||
@@ -305,8 +354,7 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
|||||||
labelText: 'Largeur (cm)',
|
labelText: 'Largeur (cm)',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
keyboardType:
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
TextInputType.numberWithOptions(decimal: true),
|
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value != null && value.isNotEmpty) {
|
if (value != null && value.isNotEmpty) {
|
||||||
if (double.tryParse(value) == null) {
|
if (double.tryParse(value) == null) {
|
||||||
@@ -325,8 +373,7 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
|||||||
labelText: 'Hauteur (cm)',
|
labelText: 'Hauteur (cm)',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
keyboardType:
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||||
TextInputType.numberWithOptions(decimal: true),
|
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value != null && value.isNotEmpty) {
|
if (value != null && value.isNotEmpty) {
|
||||||
if (double.tryParse(value) == null) {
|
if (double.tryParse(value) == null) {
|
||||||
@@ -339,32 +386,33 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
],
|
||||||
|
|
||||||
// Section Équipements
|
|
||||||
Text(
|
|
||||||
'Équipements dans ce container',
|
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 20),
|
||||||
const Divider(),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Liste des équipements sélectionnés
|
// Card 3: Équipements dans ce container
|
||||||
|
_buildCard(
|
||||||
|
title: 'Équipements dans ce container',
|
||||||
|
icon: Icons.inventory_2_outlined,
|
||||||
|
children: [
|
||||||
if (_selectedEquipmentIds.isNotEmpty)
|
if (_selectedEquipmentIds.isNotEmpty)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
color: Colors.grey.shade50,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${_selectedEquipmentIds.length} équipement(s) sélectionné(s)',
|
'${_selectedEquipmentIds.length} équipement(s) sélectionné(s) :',
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Wrap(
|
Wrap(
|
||||||
@@ -372,13 +420,19 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
|||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: _selectedEquipmentIds.map((id) {
|
children: _selectedEquipmentIds.map((id) {
|
||||||
return Chip(
|
return Chip(
|
||||||
label: Text(id),
|
label: Text(
|
||||||
deleteIcon: const Icon(Icons.close, size: 18),
|
id,
|
||||||
|
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
deleteIcon: const Icon(Icons.close, size: 16),
|
||||||
onDeleted: () {
|
onDeleted: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedEquipmentIds.remove(id);
|
_selectedEquipmentIds.remove(id);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
backgroundColor: AppColors.rouge.withValues(alpha: 0.08),
|
||||||
|
side: BorderSide(color: AppColors.rouge.withValues(alpha: 0.2)),
|
||||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
@@ -387,9 +441,9 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: Colors.grey.shade50,
|
color: Colors.grey.shade50,
|
||||||
),
|
),
|
||||||
@@ -400,7 +454,7 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Bouton pour ajouter des équipements
|
// Bouton pour ajouter des équipements
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
@@ -409,52 +463,66 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
|||||||
label: const Text('Ajouter des équipements'),
|
label: const Text('Ajouter des équipements'),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
minimumSize: const Size(double.infinity, 48),
|
minimumSize: const Size(double.infinity, 48),
|
||||||
|
side: BorderSide(color: AppColors.rouge),
|
||||||
|
foregroundColor: AppColors.rouge,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Notes
|
// Card 4: Notes
|
||||||
|
_buildCard(
|
||||||
|
title: 'Notes & Remarques',
|
||||||
|
icon: Icons.notes_outlined,
|
||||||
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _notesController,
|
controller: _notesController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Notes',
|
labelText: 'Notes complémentaires',
|
||||||
hintText: 'Informations additionnelles...',
|
hintText: 'Informations additionnelles...',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
prefixIcon: Icon(Icons.notes),
|
prefixIcon: Icon(Icons.edit_note),
|
||||||
),
|
),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
// Boutons
|
// Actions
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text('Annuler'),
|
child: const Text('Annuler', style: TextStyle(fontSize: 16)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: _saveContainer,
|
onPressed: _saveContainer,
|
||||||
icon: const Icon(Icons.save, color: Colors.white),
|
icon: const Icon(Icons.save, color: Colors.white),
|
||||||
label: Text(
|
label: Text(
|
||||||
_isEditing ? 'Mettre à jour' : 'Créer',
|
_isEditing ? 'Enregistrer' : 'Créer',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.rouge,
|
backgroundColor: AppColors.rouge,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||||
horizontal: 24,
|
shape: RoundedRectangleBorder(
|
||||||
vertical: 12,
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:em2rp/views/equipment_form/brand_model_selector.dart';
|
|||||||
import 'package:em2rp/views/equipment_form/subcategory_selector.dart';
|
import 'package:em2rp/views/equipment_form/subcategory_selector.dart';
|
||||||
import 'package:em2rp/utils/id_generator.dart';
|
import 'package:em2rp/utils/id_generator.dart';
|
||||||
import 'package:em2rp/utils/debug_log.dart';
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
|
import 'package:em2rp/utils/debouncer.dart';
|
||||||
|
|
||||||
class EquipmentFormPage extends StatefulWidget {
|
class EquipmentFormPage extends StatefulWidget {
|
||||||
final EquipmentModel? equipment;
|
final EquipmentModel? equipment;
|
||||||
@@ -38,21 +39,42 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
final TextEditingController _notesController = TextEditingController();
|
final TextEditingController _notesController = TextEditingController();
|
||||||
final TextEditingController _quantityToAddController = TextEditingController(text: '1');
|
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
|
// State variables
|
||||||
EquipmentCategory _selectedCategory = EquipmentCategory.other;
|
EquipmentCategory? _selectedCategory; // Nullable by default to force selection
|
||||||
EquipmentStatus _selectedStatus = EquipmentStatus.available;
|
EquipmentStatus _selectedStatus = EquipmentStatus.available;
|
||||||
DateTime? _purchaseDate;
|
DateTime? _purchaseDate;
|
||||||
DateTime? _lastMaintenanceDate;
|
DateTime? _lastMaintenanceDate;
|
||||||
DateTime? _nextMaintenanceDate;
|
DateTime? _nextMaintenanceDate;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _addMultiple = false;
|
|
||||||
String? _selectedBrand;
|
String? _selectedBrand;
|
||||||
List<String> _filteredModels = [];
|
List<String> _filteredModels = [];
|
||||||
List<String> _filteredSubCategories = [];
|
List<String> _filteredSubCategories = [];
|
||||||
|
|
||||||
|
// ID auto-generation and check
|
||||||
|
final ValueNotifier<bool> _autoGenerateIdNotifier = ValueNotifier<bool>(true);
|
||||||
|
final _idCheckDebouncer = Debouncer(delay: const Duration(milliseconds: 500));
|
||||||
|
String? _idConflictMessage;
|
||||||
|
List<String> _candidateIds = [];
|
||||||
|
bool _isCalculatingIds = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
_candidateIds = [];
|
||||||
|
_isCalculatingIds = false;
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
// Set default dates to today for new equipment
|
||||||
|
if (widget.equipment == null) {
|
||||||
|
_purchaseDate = DateTime.now();
|
||||||
|
_lastMaintenanceDate = DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
final provider = Provider.of<EquipmentProvider>(context, listen: false);
|
final provider = Provider.of<EquipmentProvider>(context, listen: false);
|
||||||
provider.loadBrands();
|
provider.loadBrands();
|
||||||
@@ -61,11 +83,25 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
if (_selectedBrand != null && _selectedBrand!.isNotEmpty) {
|
if (_selectedBrand != null && _selectedBrand!.isNotEmpty) {
|
||||||
_loadFilteredModels(_selectedBrand!);
|
_loadFilteredModels(_selectedBrand!);
|
||||||
}
|
}
|
||||||
_loadFilteredSubCategories(_selectedCategory);
|
if (_selectedCategory != null) {
|
||||||
|
_loadFilteredSubCategories(_selectedCategory!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (widget.equipment != null) {
|
if (widget.equipment != null) {
|
||||||
_populateFields();
|
_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();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,11 +123,172 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
_lastMaintenanceDate = equipment.lastMaintenanceDate;
|
_lastMaintenanceDate = equipment.lastMaintenanceDate;
|
||||||
_nextMaintenanceDate = equipment.nextMaintenanceDate;
|
_nextMaintenanceDate = equipment.nextMaintenanceDate;
|
||||||
_notesController.text = equipment.notes ?? '';
|
_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}');
|
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<int>? _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<int>.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<int>.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<List<String>> _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<int> targetNumbers = numbers;
|
||||||
|
final isSingleNumberInput = int.tryParse(quantityText) != null;
|
||||||
|
if (isSingleNumberInput && initialNumber != null) {
|
||||||
|
targetNumbers = List<int>.generate(numbers.length, (i) => initialNumber! + i);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> resultIds = [];
|
||||||
|
final Set<String> 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<void> _loadFilteredModels(String brand) async {
|
Future<void> _loadFilteredModels(String brand) async {
|
||||||
try {
|
try {
|
||||||
@@ -123,6 +320,11 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_brandController.removeListener(_triggerCandidateIdsUpdate);
|
||||||
|
_modelController.removeListener(_triggerCandidateIdsUpdate);
|
||||||
|
_quantityToAddController.removeListener(_triggerCandidateIdsUpdate);
|
||||||
|
_identifierController.removeListener(_onIdentifierManualChanged);
|
||||||
|
|
||||||
_identifierController.dispose();
|
_identifierController.dispose();
|
||||||
_brandController.dispose();
|
_brandController.dispose();
|
||||||
_modelController.dispose();
|
_modelController.dispose();
|
||||||
@@ -133,10 +335,57 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
_criticalThresholdController.dispose();
|
_criticalThresholdController.dispose();
|
||||||
_notesController.dispose();
|
_notesController.dispose();
|
||||||
_quantityToAddController.dispose();
|
_quantityToAddController.dispose();
|
||||||
|
|
||||||
|
_weightController.dispose();
|
||||||
|
_lengthController.dispose();
|
||||||
|
_widthController.dispose();
|
||||||
|
_heightController.dispose();
|
||||||
|
|
||||||
|
_autoGenerateIdNotifier.dispose();
|
||||||
|
_idCheckDebouncer.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _isConsumable => _selectedCategory == EquipmentCategory.consumable || _selectedCategory == EquipmentCategory.cable;
|
bool get _isConsumable => _selectedCategory != null && (_selectedCategory == EquipmentCategory.consumable || _selectedCategory == EquipmentCategory.cable);
|
||||||
|
|
||||||
|
Widget _buildCard({
|
||||||
|
required String title,
|
||||||
|
required IconData icon,
|
||||||
|
required List<Widget> 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -150,95 +399,99 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
),
|
),
|
||||||
body: _isLoading
|
body: _isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: SingleChildScrollView(
|
: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 800),
|
||||||
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// Identifiant (généré ou saisi)
|
// Card 1: Informations Générales
|
||||||
TextFormField(
|
_buildCard(
|
||||||
|
title: 'Informations générales',
|
||||||
|
icon: Icons.info_outline,
|
||||||
|
children: [
|
||||||
|
// ID row with padlock and warning
|
||||||
|
ValueListenableBuilder<bool>(
|
||||||
|
valueListenable: _autoGenerateIdNotifier,
|
||||||
|
builder: (context, autoGenerateId, child) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
controller: _identifierController,
|
controller: _identifierController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Identifiant (Laissez vide pour auto-génération) *',
|
labelText: 'Identifiant *',
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
prefixIcon: const Icon(Icons.tag),
|
prefixIcon: const Icon(Icons.tag),
|
||||||
hintText: isEditing ? null : 'Auto-attribué par défaut',
|
hintText: isEditing ? null : 'Généré automatiquement',
|
||||||
helperText: isEditing ? 'Non modifiable' : 'Génération auto recommandée basée sur Marque/Modèle',
|
helperText: isEditing ? 'Non modifiable' : 'Identifiant unique du matériel',
|
||||||
),
|
),
|
||||||
enabled: !isEditing,
|
enabled: !autoGenerateId && !isEditing,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value != null && value.isNotEmpty) {
|
if (value == null || value.isEmpty) {
|
||||||
// Empêcher les ID commençant par BOX_ (réservé aux containers)
|
return 'Veuillez entrer un identifiant';
|
||||||
|
}
|
||||||
if (value.toUpperCase().startsWith('BOX_')) {
|
if (value.toUpperCase().startsWith('BOX_')) {
|
||||||
return 'Les ID commençant par BOX_ sont réservés aux boites';
|
return 'Les ID commençant par BOX_ sont réservés aux boites';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
|
|
||||||
// Case à cocher "Ajouter plusieurs" (uniquement en mode création)
|
|
||||||
if (!isEditing) ...[
|
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(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
Icon(Icons.warning_amber_rounded, color: Colors.orange.shade800, size: 16),
|
||||||
|
const SizedBox(width: 6),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
child: Text(
|
||||||
child: CheckboxListTile(
|
_idConflictMessage!,
|
||||||
title: const Text('Ajouter plusieurs équipements'),
|
style: TextStyle(
|
||||||
subtitle: const Text('Créer plusieurs équipements numérotés'),
|
color: Colors.orange.shade800,
|
||||||
value: _addMultiple,
|
fontSize: 12,
|
||||||
contentPadding: EdgeInsets.zero,
|
fontWeight: FontWeight.w500,
|
||||||
onChanged: (bool? value) {
|
|
||||||
setState(() {
|
|
||||||
_addMultiple = value ?? false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_addMultiple) ...[
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: TextFormField(
|
|
||||||
controller: _quantityToAddController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Quantité ou range',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
prefixIcon: Icon(Icons.plus_one),
|
|
||||||
hintText: '5 ou 6-18',
|
|
||||||
helperText: 'Ex: 5 ou 6-18',
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.text,
|
|
||||||
validator: (value) {
|
|
||||||
if (_addMultiple) {
|
|
||||||
if (value == null || value.isEmpty) return 'Requis';
|
|
||||||
// Vérifier si c'est un nombre simple ou une range
|
|
||||||
if (value.contains('-')) {
|
|
||||||
final parts = value.split('-');
|
|
||||||
if (parts.length != 2) return 'Format invalide';
|
|
||||||
final start = int.tryParse(parts[0].trim());
|
|
||||||
final end = int.tryParse(parts[1].trim());
|
|
||||||
if (start == null || end == null) return 'Nombres invalides';
|
|
||||||
if (start >= end) return 'Le début doit être < fin';
|
|
||||||
if (end - start > 100) return 'Max 100 équipements';
|
|
||||||
} else {
|
|
||||||
final num = int.tryParse(value);
|
|
||||||
if (num == null || num < 1 || num > 100) return '1-100';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
|
||||||
|
|
||||||
// Sélecteur Marque/Modèle
|
// Marque & Modèle
|
||||||
BrandModelSelector(
|
BrandModelSelector(
|
||||||
brandController: _brandController,
|
brandController: _brandController,
|
||||||
modelController: _modelController,
|
modelController: _modelController,
|
||||||
@@ -266,6 +519,7 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
|
|
||||||
// Catégorie et Statut
|
// Catégorie et Statut
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: DropdownButtonFormField<EquipmentCategory>(
|
child: DropdownButtonFormField<EquipmentCategory>(
|
||||||
@@ -281,6 +535,12 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
child: Text(category.label),
|
child: Text(category.label),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null) {
|
||||||
|
return 'Catégorie obligatoire';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -292,7 +552,6 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Afficher le statut uniquement si ce n'est pas un consommable ou câble
|
|
||||||
if (!_isConsumable) ...[
|
if (!_isConsumable) ...[
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -301,7 +560,7 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Statut *',
|
labelText: 'Statut *',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
prefixIcon: Icon(Icons.info),
|
prefixIcon: Icon(Icons.info_outline),
|
||||||
),
|
),
|
||||||
items: EquipmentStatus.values.map((status) {
|
items: EquipmentStatus.values.map((status) {
|
||||||
return DropdownMenuItem(
|
return DropdownMenuItem(
|
||||||
@@ -329,52 +588,102 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
selectedCategory: _selectedCategory,
|
selectedCategory: _selectedCategory,
|
||||||
filteredSubCategories: _filteredSubCategories,
|
filteredSubCategories: _filteredSubCategories,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {});
|
||||||
// La valeur est déjà dans le controller
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Prix
|
// Card 2: Quantité & Stock
|
||||||
if (hasManagePermission) ...[
|
if (!isEditing || _isConsumable) ...[
|
||||||
Row(
|
_buildCard(
|
||||||
|
title: 'Quantité & Stock',
|
||||||
|
icon: Icons.inventory_2_outlined,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
if (!isEditing) ...[
|
||||||
child: TextFormField(
|
TextFormField(
|
||||||
controller: _purchasePriceController,
|
controller: _quantityToAddController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Prix d\'achat (€)',
|
labelText: 'Nombre d\'exemplaires à créer *',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
prefixIcon: Icon(Icons.euro),
|
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)',
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
helperMaxLines: 3,
|
||||||
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.attach_money),
|
|
||||||
),
|
|
||||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
||||||
inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}'))],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
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),
|
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),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Quantités pour consommables
|
|
||||||
if (_isConsumable) ...[
|
if (_isConsumable) ...[
|
||||||
const Divider(),
|
|
||||||
const Text('Gestion des quantités', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -383,7 +692,7 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Quantité totale',
|
labelText: 'Quantité totale',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
prefixIcon: Icon(Icons.inventory),
|
prefixIcon: Icon(Icons.format_list_numbered),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
@@ -396,7 +705,7 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Seuil critique',
|
labelText: 'Seuil critique',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
prefixIcon: Icon(Icons.warning),
|
prefixIcon: Icon(Icons.warning_amber),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
@@ -404,40 +713,146 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Dates
|
// Card 3: Informations Financières
|
||||||
const Divider(),
|
if (hasManagePermission) ...[
|
||||||
const Text('Dates', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
_buildCard(
|
||||||
const SizedBox(height: 16),
|
title: 'Informations financières',
|
||||||
_buildDateField(label: 'Date d\'achat', icon: Icons.shopping_cart, value: _purchaseDate, onTap: () => _selectDate(context, 'purchase')),
|
icon: Icons.euro_outlined,
|
||||||
const SizedBox(height: 16),
|
children: [
|
||||||
_buildDateField(label: 'Dernière maintenance', icon: Icons.build, value: _lastMaintenanceDate, onTap: () => _selectDate(context, 'lastMaintenance')),
|
Row(
|
||||||
const SizedBox(height: 16),
|
children: [
|
||||||
_buildDateField(label: 'Prochaine maintenance', icon: Icons.event, value: _nextMaintenanceDate, onTap: () => _selectDate(context, 'nextMaintenance')),
|
Expanded(
|
||||||
const SizedBox(height: 16),
|
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),
|
||||||
|
],
|
||||||
|
|
||||||
// Notes
|
// Card 4: Caractéristiques physiques (Amélioration)
|
||||||
const Divider(),
|
_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(
|
TextFormField(
|
||||||
controller: _notesController,
|
controller: _notesController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Notes',
|
labelText: 'Notes complémentaires',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
prefixIcon: Icon(Icons.notes),
|
prefixIcon: Icon(Icons.edit_note),
|
||||||
),
|
),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
// Boutons
|
// Actions
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(context),
|
||||||
child: const Text('Annuler'),
|
child: const Text('Annuler', style: TextStyle(fontSize: 16)),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
@@ -445,19 +860,27 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.rouge,
|
backgroundColor: AppColors.rouge,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
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)
|
||||||
),
|
),
|
||||||
child: Text(isEditing ? 'Enregistrer' : 'Créer', style: const TextStyle(color: Colors.white)),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Widget _buildDateField({required String label, required IconData icon, required DateTime? value, required VoidCallback onTap}) {
|
Widget _buildDateField({required String label, required IconData icon, required DateTime? value, required VoidCallback onTap}) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@@ -519,6 +942,8 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
Future<void> _saveEquipment() async {
|
Future<void> _saveEquipment() async {
|
||||||
if (!_formKey.currentState!.validate()) return;
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
|
final navigator = Navigator.of(context);
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -536,56 +961,32 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validation marque/modèle obligatoires
|
// Validation marque/modèle obligatoires
|
||||||
String brand = _brandController.text.trim();
|
final brand = _brandController.text.trim();
|
||||||
String model = _modelController.text.trim();
|
final model = _modelController.text.trim();
|
||||||
|
|
||||||
if (brand.isEmpty || model.isEmpty) {
|
if (brand.isEmpty || model.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
const SnackBar(content: Text('La marque et le modèle sont obligatoires')),
|
const SnackBar(content: Text('La marque et le modèle sont obligatoires')),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Génération d'identifiant si vide
|
// Génération d'identifiant
|
||||||
List<String> ids = [];
|
List<String> ids = [];
|
||||||
List<int> numbers = [];
|
if (!isEditing) {
|
||||||
|
ids = await _calculateCandidateIds();
|
||||||
if (!isEditing && _identifierController.text.isEmpty) {
|
if (ids.isEmpty) {
|
||||||
// Gérer la range ou nombre simple
|
scaffoldMessenger.showSnackBar(
|
||||||
final quantityText = _quantityToAddController.text.trim();
|
const SnackBar(content: Text('Impossible de générer des identifiants valides')),
|
||||||
if (_addMultiple && quantityText.contains('-')) {
|
);
|
||||||
// Range: ex "6-18"
|
setState(() => _isLoading = false);
|
||||||
final parts = quantityText.split('-');
|
return;
|
||||||
final start = int.parse(parts[0].trim());
|
|
||||||
final end = int.parse(parts[1].trim());
|
|
||||||
for (int i = start; i <= end; i++) {
|
|
||||||
numbers.add(i);
|
|
||||||
}
|
|
||||||
} else if (_addMultiple) {
|
|
||||||
// Nombre simple
|
|
||||||
final nbToAdd = int.tryParse(quantityText) ?? 1;
|
|
||||||
for (int i = 1; i <= nbToAdd; i++) {
|
|
||||||
numbers.add(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Générer les IDs
|
|
||||||
if (numbers.isEmpty) {
|
|
||||||
String baseId = IdGenerator.generateEquipmentId(brand: brand, model: model, number: null);
|
|
||||||
String uniqueId = await IdGenerator.ensureUniqueEquipmentId(baseId, _equipmentService);
|
|
||||||
ids.add(uniqueId);
|
|
||||||
} else {
|
|
||||||
for (final num in numbers) {
|
|
||||||
String baseId = IdGenerator.generateEquipmentId(brand: brand, model: model, number: num);
|
|
||||||
String uniqueId = await IdGenerator.ensureUniqueEquipmentId(baseId, _equipmentService);
|
|
||||||
ids.add(uniqueId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ids.add(_identifierController.text.trim());
|
ids.add(_identifierController.text.trim().toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Création des équipements
|
// Création/Mise à jour des équipements
|
||||||
for (final id in ids) {
|
for (final id in ids) {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final equipment = EquipmentModel(
|
final equipment = EquipmentModel(
|
||||||
@@ -593,7 +994,7 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
name: id, // Utilisation de l'identifiant comme nom
|
name: id, // Utilisation de l'identifiant comme nom
|
||||||
brand: brand,
|
brand: brand,
|
||||||
model: model,
|
model: model,
|
||||||
category: _selectedCategory,
|
category: _selectedCategory!,
|
||||||
subCategory: _subCategoryController.text.trim().isNotEmpty ? _subCategoryController.text.trim() : null,
|
subCategory: _subCategoryController.text.trim().isNotEmpty ? _subCategoryController.text.trim() : null,
|
||||||
status: _selectedStatus,
|
status: _selectedStatus,
|
||||||
purchasePrice: _purchasePriceController.text.isNotEmpty ? double.tryParse(_purchasePriceController.text) : null,
|
purchasePrice: _purchasePriceController.text.isNotEmpty ? double.tryParse(_purchasePriceController.text) : null,
|
||||||
@@ -607,6 +1008,10 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
createdAt: isEditing ? (widget.equipment?.createdAt ?? now) : now,
|
createdAt: isEditing ? (widget.equipment?.createdAt ?? now) : now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
availableQuantity: availableQuantity,
|
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) {
|
if (isEditing) {
|
||||||
await equipmentProvider.updateEquipment(equipment);
|
await equipmentProvider.updateEquipment(equipment);
|
||||||
@@ -616,11 +1021,11 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.pop(context, true);
|
navigator.pop(true);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(content: Text('Erreur lors de l\'enregistrement : $e')),
|
SnackBar(content: Text('Erreur lors de l\'enregistrement : $e')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -629,3 +1034,9 @@ class _EquipmentFormPageState extends State<EquipmentFormPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class IdParseResult {
|
||||||
|
final String baseId;
|
||||||
|
final int? number;
|
||||||
|
IdParseResult(this.baseId, this.number);
|
||||||
|
}
|
||||||
|
|||||||
@@ -923,17 +923,8 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
try {
|
try {
|
||||||
// Récupérer les équipements sélectionnés
|
// Récupérer les équipements sélectionnés
|
||||||
final provider = context.read<EquipmentProvider>();
|
final provider = context.read<EquipmentProvider>();
|
||||||
final List<EquipmentModel> selectedEquipment = [];
|
final List<EquipmentModel> selectedEquipment =
|
||||||
|
await provider.getEquipmentsByIds(selectedIds.toList());
|
||||||
// On doit récupérer les équipements depuis le stream
|
|
||||||
await for (final equipmentList in provider.equipmentStream.take(1)) {
|
|
||||||
for (final equipment in equipmentList) {
|
|
||||||
if (isItemSelected(equipment.id)) {
|
|
||||||
selectedEquipment.add(equipment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fermer l'indicateur de chargement
|
// Fermer l'indicateur de chargement
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|||||||
@@ -36,12 +36,15 @@ class MonthView extends StatelessWidget {
|
|||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final rowCount = _computeRowCount(focusedDay);
|
final rowCount = _computeRowCount(focusedDay);
|
||||||
|
// TableCalendar has internal vertical padding and margins (approx 16px) that cause overflow
|
||||||
|
// if not accounted for. We subtract an extra 16.0 pixels to be safe.
|
||||||
final availableHeight = constraints.maxHeight -
|
final availableHeight = constraints.maxHeight -
|
||||||
(_calendarPadding * 2) -
|
(_calendarPadding * 2) -
|
||||||
_headerHeight -
|
_headerHeight -
|
||||||
_headerVerticalPadding -
|
_headerVerticalPadding -
|
||||||
_daysOfWeekHeight;
|
_daysOfWeekHeight -
|
||||||
final rowHeight = availableHeight / rowCount;
|
16.0;
|
||||||
|
final rowHeight = (availableHeight > 0 ? availableHeight : 200.0) / rowCount;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: constraints.maxHeight,
|
height: constraints.maxHeight,
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ class _EquipmentAssociatedEventsSectionState
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadEvents() async {
|
Future<void> _loadEvents() async {
|
||||||
|
if (mounted) {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Récupérer TOUS les événements via l'API
|
// Récupérer TOUS les événements via l'API
|
||||||
@@ -128,12 +130,16 @@ class _EquipmentAssociatedEventsSectionState
|
|||||||
// Trier par date
|
// Trier par date
|
||||||
filteredEvents.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
filteredEvents.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_events = filteredEvents;
|
_events = filteredEvents;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@@ -207,7 +213,9 @@ class _EquipmentAssociatedEventsSectionState
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
onSelected: (filter) {
|
onSelected: (filter) {
|
||||||
|
if (mounted) {
|
||||||
setState(() => _selectedFilter = filter);
|
setState(() => _selectedFilter = filter);
|
||||||
|
}
|
||||||
_loadEvents();
|
_loadEvents();
|
||||||
},
|
},
|
||||||
itemBuilder: (context) => EventFilter.values.map((filter) {
|
itemBuilder: (context) => EventFilter.values.map((filter) {
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ class _EquipmentCurrentEventsSectionState
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadCurrentEvents() async {
|
Future<void> _loadCurrentEvents() async {
|
||||||
|
if (mounted) {
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Récupérer TOUS les événements via l'API
|
// Récupérer TOUS les événements via l'API
|
||||||
@@ -106,12 +108,16 @@ class _EquipmentCurrentEventsSectionState
|
|||||||
// Trier par date
|
// Trier par date
|
||||||
events.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
events.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_events = events;
|
_events = events;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|||||||
@@ -24,10 +24,8 @@
|
|||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
<meta http-equiv="Pragma" content="no-cache">
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
<meta http-equiv="Expires" content="0">
|
<meta http-equiv="Expires" content="0">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="EM2RP">
|
<meta name="apple-mobile-web-app-title" content="EM2RP">
|
||||||
<link rel="apple-touch-icon" href="favicon.jpg">
|
<link rel="apple-touch-icon" href="favicon.jpg">
|
||||||
|
|||||||
Reference in New Issue
Block a user