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/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
||||
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
||||
version.json,1779800968600,00f600f01984c1e371af870e40f78fd44ba53d05596f2f92f9b4fc56a85f52b6
|
||||
index.html,1779800974065,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
||||
flutter_service_worker.js,1779801065196,879f42a05578f24ea45dc23326fdda6246d38dc59de0824ef8d4edfa4715e571
|
||||
flutter_bootstrap.js,1779800974054,977a20d5caac8da21af648cae8fa7dba00a5cd959fd2fafa7ef538a012fe87c3
|
||||
assets/FontManifest.json,1779801061892,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
||||
assets/AssetManifest.json,1779801061891,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6
|
||||
assets/AssetManifest.bin.json,1779801061892,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53
|
||||
assets/AssetManifest.bin,1779801061891,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907
|
||||
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1779801064441,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
||||
assets/shaders/ink_sparkle.frag,1779801062097,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
||||
assets/fonts/MaterialIcons-Regular.otf,1779801064446,710dc8fc35289048b52970355f64206fb1b2c5e67c71ae77a46b53f0e2daecd6
|
||||
assets/NOTICES,1779801061893,5522e1307c65771d1fbf26fcd9dc0548c751413f42196c4acaba5ee674eede1e
|
||||
main.dart.js,1779801060964,e87fb4dfca93c3384b5cb63d627186e48b0c15d78ed63bc7f5e61544ef292dd9
|
||||
version.json,1779802456392,7626a7c596308bd2eb1add2ed984cd6dda5d4a3f0dedb3338244d2ae45c496cf
|
||||
index.html,1779802461141,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
||||
flutter_service_worker.js,1779802555396,fadec2c9a1e8e16c22e332aef080b0b2aacc3998c4e260e5821b79afb9e000da
|
||||
flutter_bootstrap.js,1779802461128,ad20054b92acf16bb75fbffd65f81c63c6d3cb6d752f799230dca5f2118af783
|
||||
assets/FontManifest.json,1779802551869,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
||||
assets/AssetManifest.json,1779802551869,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6
|
||||
assets/AssetManifest.bin.json,1779802551869,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53
|
||||
assets/AssetManifest.bin,1779802551869,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907
|
||||
assets/shaders/ink_sparkle.frag,1779802552052,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
||||
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1779802554433,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
||||
assets/fonts/MaterialIcons-Regular.otf,1779802554440,710dc8fc35289048b52970355f64206fb1b2c5e67c71ae77a46b53f0e2daecd6
|
||||
assets/NOTICES,1779802551871,5522e1307c65771d1fbf26fcd9dc0548c751413f42196c4acaba5ee674eede1e
|
||||
main.dart.js,1779802550741,ab892e930c97940c1ea4ff33079922082c7f688047d307acad0644a78cfda2d7
|
||||
|
||||
+54
-34
@@ -39,46 +39,66 @@ void main() {
|
||||
// Ne pas effectuer d'initialisations asynchrones lourdes ici.
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
// Fournisseur d'initialisation de l'application (initialise Firebase et cache en tâche de fond)
|
||||
ChangeNotifierProvider<AppInitializer>(
|
||||
create: (_) => AppInitializer(),
|
||||
),
|
||||
// LocalUserProvider pour la gestion de l'authentification
|
||||
ChangeNotifierProvider<LocalUserProvider>(
|
||||
create: (context) => LocalUserProvider()),
|
||||
if (kReleaseMode) {
|
||||
debugPrint = (String? message, {int? wrapWidth}) {};
|
||||
}
|
||||
|
||||
// UsersProvider migré vers l'API
|
||||
ChangeNotifierProvider<UsersProvider>(
|
||||
create: (context) => UsersProvider(),
|
||||
),
|
||||
runZonedGuarded(
|
||||
() {
|
||||
runApp(
|
||||
MultiProvider(
|
||||
providers: [
|
||||
// Fournisseur d'initialisation de l'application (initialise Firebase et cache en tâche de fond)
|
||||
ChangeNotifierProvider<AppInitializer>(
|
||||
create: (_) => AppInitializer(),
|
||||
),
|
||||
// LocalUserProvider pour la gestion de l'authentification
|
||||
ChangeNotifierProvider<LocalUserProvider>(
|
||||
create: (context) => LocalUserProvider()),
|
||||
|
||||
// EventProvider migré vers l'API
|
||||
ChangeNotifierProvider<EventProvider>(
|
||||
create: (context) => EventProvider(),
|
||||
),
|
||||
// UsersProvider migré vers l'API
|
||||
ChangeNotifierProvider<UsersProvider>(
|
||||
create: (context) => UsersProvider(),
|
||||
),
|
||||
|
||||
// EquipmentProvider migré vers l'API
|
||||
ChangeNotifierProvider<EquipmentProvider>(
|
||||
create: (context) => EquipmentProvider(),
|
||||
),
|
||||
// EventProvider migré vers l'API
|
||||
ChangeNotifierProvider<EventProvider>(
|
||||
create: (context) => EventProvider(),
|
||||
),
|
||||
|
||||
// ContainerProvider migré vers l'API
|
||||
ChangeNotifierProvider<ContainerProvider>(
|
||||
create: (context) => ContainerProvider(),
|
||||
),
|
||||
// EquipmentProvider migré vers l'API
|
||||
ChangeNotifierProvider<EquipmentProvider>(
|
||||
create: (context) => EquipmentProvider(),
|
||||
),
|
||||
|
||||
// MaintenanceProvider migré vers l'API
|
||||
ChangeNotifierProvider<MaintenanceProvider>(
|
||||
create: (context) => MaintenanceProvider(),
|
||||
// ContainerProvider migré vers l'API
|
||||
ChangeNotifierProvider<ContainerProvider>(
|
||||
create: (context) => ContainerProvider(),
|
||||
),
|
||||
|
||||
// MaintenanceProvider migré vers l'API
|
||||
ChangeNotifierProvider<MaintenanceProvider>(
|
||||
create: (context) => MaintenanceProvider(),
|
||||
),
|
||||
ChangeNotifierProvider<AlertProvider>(
|
||||
create: (context) => AlertProvider(),
|
||||
),
|
||||
],
|
||||
child: const MyApp(),
|
||||
),
|
||||
ChangeNotifierProvider<AlertProvider>(
|
||||
create: (context) => AlertProvider(),
|
||||
),
|
||||
],
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -242,34 +242,55 @@ class ContainerModel {
|
||||
|
||||
/// Factory depuis Firestore
|
||||
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) {
|
||||
if (value == null) return null;
|
||||
if (value is Timestamp) return value.toDate();
|
||||
if (value is String) return DateTime.tryParse(value);
|
||||
if (value is int) return DateTime.fromMillisecondsSinceEpoch(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
final List<dynamic> equipmentIdsRaw = map['equipmentIds'] ?? [];
|
||||
final List<String> equipmentIds = equipmentIdsRaw.map((e) => e.toString()).toList();
|
||||
// Gestion sécurisée de la liste d'IDs d'équipements
|
||||
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'] ?? [];
|
||||
final List<ContainerHistoryEntry> history = historyRaw
|
||||
.map((e) => ContainerHistoryEntry.fromMap(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
// Gestion sécurisée de l'historique
|
||||
final List<ContainerHistoryEntry> history = [];
|
||||
if (map['history'] is List) {
|
||||
for (final e in map['history'] as List) {
|
||||
if (e is Map<String, dynamic>) {
|
||||
history.add(ContainerHistoryEntry.fromMap(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ContainerModel(
|
||||
id: id,
|
||||
name: map['name'] ?? '',
|
||||
type: containerTypeFromString(map['type']),
|
||||
status: equipmentStatusFromString(map['status']),
|
||||
weight: map['weight']?.toDouble(),
|
||||
length: map['length']?.toDouble(),
|
||||
width: map['width']?.toDouble(),
|
||||
height: map['height']?.toDouble(),
|
||||
name: (map['name'] ?? '').toString(),
|
||||
type: containerTypeFromString(map['type']?.toString()),
|
||||
status: equipmentStatusFromString(map['status']?.toString()),
|
||||
weight: parseDouble(map['weight']),
|
||||
length: parseDouble(map['length']),
|
||||
width: parseDouble(map['width']),
|
||||
height: parseDouble(map['height']),
|
||||
equipmentIds: equipmentIds,
|
||||
eventId: map['eventId'],
|
||||
notes: map['notes'],
|
||||
eventId: map['eventId']?.toString(),
|
||||
notes: map['notes']?.toString(),
|
||||
createdAt: parseDate(map['createdAt']) ?? DateTime.now(),
|
||||
updatedAt: parseDate(map['updatedAt']) ?? DateTime.now(),
|
||||
history: history,
|
||||
@@ -355,16 +376,17 @@ class ContainerHistoryEntry {
|
||||
if (value == null) return DateTime.now();
|
||||
if (value is Timestamp) return value.toDate();
|
||||
if (value is String) return DateTime.tryParse(value) ?? DateTime.now();
|
||||
if (value is int) return DateTime.fromMillisecondsSinceEpoch(value);
|
||||
return DateTime.now();
|
||||
}
|
||||
|
||||
return ContainerHistoryEntry(
|
||||
timestamp: parseDate(map['timestamp']),
|
||||
action: map['action'] ?? '',
|
||||
equipmentId: map['equipmentId'],
|
||||
previousValue: map['previousValue'],
|
||||
newValue: map['newValue'],
|
||||
userId: map['userId'],
|
||||
action: (map['action'] ?? '').toString(),
|
||||
equipmentId: map['equipmentId']?.toString(),
|
||||
previousValue: map['previousValue']?.toString(),
|
||||
newValue: map['newValue']?.toString(),
|
||||
userId: map['userId']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -387,40 +387,64 @@ class EquipmentModel {
|
||||
});
|
||||
|
||||
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) {
|
||||
if (value == null) return null;
|
||||
if (value is Timestamp) return value.toDate();
|
||||
if (value is String) return DateTime.tryParse(value);
|
||||
if (value is int) return DateTime.fromMillisecondsSinceEpoch(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Gestion des listes
|
||||
final List<dynamic> maintenanceIdsRaw = map['maintenanceIds'] ?? [];
|
||||
final List<String> maintenanceIds = maintenanceIdsRaw.map((e) => e.toString()).toList();
|
||||
// Gestion sécurisée des listes d'IDs de maintenance
|
||||
final List<String> maintenanceIds = [];
|
||||
if (map['maintenanceIds'] is List) {
|
||||
for (final e in map['maintenanceIds'] as List) {
|
||||
if (e != null) {
|
||||
maintenanceIds.add(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EquipmentModel(
|
||||
id: id,
|
||||
name: map['name'] ?? '',
|
||||
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(),
|
||||
weight: map['weight']?.toDouble(),
|
||||
length: map['length']?.toDouble(),
|
||||
width: map['width']?.toDouble(),
|
||||
height: map['height']?.toDouble(),
|
||||
name: (map['name'] ?? '').toString(),
|
||||
brand: map['brand']?.toString(),
|
||||
model: map['model']?.toString(),
|
||||
category: equipmentCategoryFromString(map['category']?.toString()),
|
||||
subCategory: map['subCategory']?.toString(),
|
||||
status: equipmentStatusFromString(map['status']?.toString()),
|
||||
purchasePrice: parseDouble(map['purchasePrice']),
|
||||
rentalPrice: parseDouble(map['rentalPrice']),
|
||||
totalQuantity: parseInt(map['totalQuantity']),
|
||||
availableQuantity: parseInt(map['availableQuantity']),
|
||||
criticalThreshold: parseInt(map['criticalThreshold']),
|
||||
weight: parseDouble(map['weight']),
|
||||
length: parseDouble(map['length']),
|
||||
width: parseDouble(map['width']),
|
||||
height: parseDouble(map['height']),
|
||||
purchaseDate: parseDate(map['purchaseDate']),
|
||||
lastMaintenanceDate: parseDate(map['lastMaintenanceDate']),
|
||||
nextMaintenanceDate: parseDate(map['nextMaintenanceDate']),
|
||||
maintenanceIds: maintenanceIds,
|
||||
imageUrl: map['imageUrl'],
|
||||
notes: map['notes'],
|
||||
imageUrl: map['imageUrl']?.toString(),
|
||||
notes: map['notes']?.toString(),
|
||||
createdAt: parseDate(map['createdAt']) ?? DateTime.now(),
|
||||
updatedAt: parseDate(map['updatedAt']) ?? DateTime.now(),
|
||||
);
|
||||
|
||||
@@ -60,29 +60,44 @@ class MaintenanceModel {
|
||||
});
|
||||
|
||||
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) {
|
||||
if (value == null) return null;
|
||||
if (value is Timestamp) return value.toDate();
|
||||
if (value is String) return DateTime.tryParse(value);
|
||||
if (value is int) return DateTime.fromMillisecondsSinceEpoch(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Gestion de la liste des équipements
|
||||
final List<dynamic> equipmentIdsRaw = map['equipmentIds'] ?? [];
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MaintenanceModel(
|
||||
id: id,
|
||||
equipmentIds: equipmentIds,
|
||||
type: maintenanceTypeFromString(map['type']),
|
||||
type: maintenanceTypeFromString(map['type']?.toString()),
|
||||
scheduledDate: parseDate(map['scheduledDate']) ?? DateTime.now(),
|
||||
completedDate: parseDate(map['completedDate']),
|
||||
name: map['name'] ?? '',
|
||||
description: map['description'] ?? '',
|
||||
performedBy: map['performedBy'],
|
||||
cost: map['cost']?.toDouble(),
|
||||
notes: map['notes'],
|
||||
name: (map['name'] ?? '').toString(),
|
||||
description: (map['description'] ?? '').toString(),
|
||||
performedBy: map['performedBy']?.toString(),
|
||||
cost: parseDouble(map['cost']),
|
||||
notes: map['notes']?.toString(),
|
||||
createdAt: parseDate(map['createdAt']) ?? DateTime.now(),
|
||||
updatedAt: parseDate(map['updatedAt']) ?? DateTime.now(),
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
String _searchQuery = '';
|
||||
bool _isLoading = false;
|
||||
bool _isInitialized = false;
|
||||
bool _isFullListLoaded = false;
|
||||
|
||||
// Mode de chargement (pagination vs full)
|
||||
bool _usePagination = false;
|
||||
@@ -48,6 +49,7 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
bool get isLoadingMore => _isLoadingMore;
|
||||
bool get hasMore => _hasMore;
|
||||
bool get isInitialized => _isInitialized;
|
||||
bool get isFullListLoaded => _isFullListLoaded;
|
||||
bool get usePagination => _usePagination;
|
||||
|
||||
/// S'assure que les équipements sont chargés (charge si nécessaire)
|
||||
@@ -58,16 +60,8 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
// Si initialisé MAIS _equipment est vide, forcer le rechargement
|
||||
if (_isInitialized && _equipment.isEmpty) {
|
||||
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) {
|
||||
// Si déjà initialisé avec le cache complet des données, ne rien faire
|
||||
if (_isFullListLoaded) {
|
||||
print('[EquipmentProvider] Equipment already loaded (${_equipment.length} items), skipping...');
|
||||
return;
|
||||
}
|
||||
@@ -120,7 +114,7 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
|
||||
// Extraire les modèles et marques uniques
|
||||
_extractUniqueValues();
|
||||
|
||||
_isFullListLoaded = true;
|
||||
_isInitialized = true;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
@@ -436,6 +430,8 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
Future<void> deleteEquipment(String equipmentId, {bool forceDelete = false}) async {
|
||||
try {
|
||||
await _dataService.deleteEquipment(equipmentId, forceDelete: forceDelete);
|
||||
_isFullListLoaded = false;
|
||||
_equipment.clear();
|
||||
if (_usePagination) {
|
||||
await reload();
|
||||
} else {
|
||||
@@ -451,6 +447,8 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
Future<void> addEquipment(EquipmentModel equipment) async {
|
||||
try {
|
||||
await _dataService.createEquipment(equipment.id, equipment.toMap());
|
||||
_isFullListLoaded = false;
|
||||
_equipment.clear();
|
||||
if (_usePagination) {
|
||||
await reload();
|
||||
} else {
|
||||
@@ -466,6 +464,8 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
Future<void> updateEquipment(EquipmentModel equipment) async {
|
||||
try {
|
||||
await _dataService.updateEquipment(equipment.id, equipment.toMap());
|
||||
_isFullListLoaded = false;
|
||||
_equipment.clear();
|
||||
if (_usePagination) {
|
||||
await reload();
|
||||
} else {
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:em2rp/utils/id_generator.dart';
|
||||
import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
import 'package:em2rp/utils/debouncer.dart';
|
||||
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
||||
|
||||
class ContainerFormPage extends StatefulWidget {
|
||||
final ContainerModel? container;
|
||||
@@ -90,369 +91,436 @@ 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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_isEditing ? 'Modifier boite' : 'Nouvelle boite'),
|
||||
backgroundColor: AppColors.rouge,
|
||||
foregroundColor: Colors.white,
|
||||
appBar: CustomAppBar(
|
||||
title: _isEditing ? 'Modifier boîte' : 'Nouvelle boîte',
|
||||
),
|
||||
body: Form(
|
||||
key: _formKey,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
// Nom
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nom du container *',
|
||||
hintText: 'ex: Flight Case Beam 7R',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.label),
|
||||
),
|
||||
onChanged: (_) => _updateIdFromName(),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer un nom';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ID
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _autoGenerateIdNotifier,
|
||||
builder: (context, autoGenerateId, child) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _idController,
|
||||
body: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 800),
|
||||
child: SingleChildScrollView(
|
||||
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: [
|
||||
// Nom
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Identifiant *',
|
||||
hintText: 'ex: FLIGHTCASE_BEAM',
|
||||
labelText: 'Nom du container *',
|
||||
hintText: 'ex: Flight Case Beam 7R',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.qr_code),
|
||||
prefixIcon: Icon(Icons.label),
|
||||
),
|
||||
enabled: !autoGenerateId || _isEditing,
|
||||
onChanged: (_) => _updateIdFromName(),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer un identifiant';
|
||||
return 'Veuillez entrer un nom';
|
||||
}
|
||||
final validation = IdGenerator.validateContainerId(value);
|
||||
return validation;
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!_isEditing) ...[
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
autoGenerateId ? Icons.lock : Icons.lock_open,
|
||||
color: autoGenerateId ? AppColors.rouge : Colors.grey,
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ID
|
||||
ValueListenableBuilder<bool>(
|
||||
valueListenable: _autoGenerateIdNotifier,
|
||||
builder: (context, autoGenerateId, child) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _idController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Identifiant *',
|
||||
hintText: 'ex: FLIGHTCASE_BEAM',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.qr_code),
|
||||
),
|
||||
enabled: !autoGenerateId || _isEditing,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Veuillez entrer un identifiant';
|
||||
}
|
||||
final validation = IdGenerator.validateContainerId(value);
|
||||
return validation;
|
||||
},
|
||||
),
|
||||
),
|
||||
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) {
|
||||
_updateIdFromName();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Type
|
||||
DropdownButtonFormField<ContainerType>(
|
||||
initialValue: _selectedType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type de container *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.category),
|
||||
),
|
||||
tooltip: autoGenerateId
|
||||
? 'Génération automatique'
|
||||
: 'Saisie manuelle',
|
||||
onPressed: () {
|
||||
_autoGenerateIdNotifier.value = !autoGenerateId;
|
||||
if (_autoGenerateIdNotifier.value) {
|
||||
_updateIdFromName();
|
||||
items: ContainerType.values.map((type) {
|
||||
return DropdownMenuItem(
|
||||
value: type,
|
||||
child: Row(
|
||||
children: [
|
||||
type.getIcon(size: 20, color: AppColors.rouge),
|
||||
const SizedBox(width: 8),
|
||||
Text(type.label),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedType = value;
|
||||
_updateIdFromType();
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Statut
|
||||
DropdownButtonFormField<EquipmentStatus>(
|
||||
initialValue: _selectedStatus,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Statut *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.info_outline),
|
||||
),
|
||||
items: [
|
||||
EquipmentStatus.available,
|
||||
EquipmentStatus.inUse,
|
||||
EquipmentStatus.maintenance,
|
||||
EquipmentStatus.outOfService,
|
||||
].map((status) {
|
||||
String label;
|
||||
switch (status) {
|
||||
case EquipmentStatus.available:
|
||||
label = 'Disponible';
|
||||
break;
|
||||
case EquipmentStatus.inUse:
|
||||
label = 'En prestation';
|
||||
break;
|
||||
case EquipmentStatus.maintenance:
|
||||
label = 'En maintenance';
|
||||
break;
|
||||
case EquipmentStatus.outOfService:
|
||||
label = 'Hors service';
|
||||
break;
|
||||
default:
|
||||
label = 'Autre';
|
||||
}
|
||||
return DropdownMenuItem(
|
||||
value: status,
|
||||
child: Text(label),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedStatus = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Type
|
||||
DropdownButtonFormField<ContainerType>(
|
||||
initialValue: _selectedType,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Type de container *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.category),
|
||||
),
|
||||
items: ContainerType.values.map((type) {
|
||||
return DropdownMenuItem(
|
||||
value: type,
|
||||
child: Text(type.label),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedType = value;
|
||||
_updateIdFromType();
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Statut
|
||||
DropdownButtonFormField<EquipmentStatus>(
|
||||
initialValue: _selectedStatus,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Statut *',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.info),
|
||||
),
|
||||
items: [
|
||||
EquipmentStatus.available,
|
||||
EquipmentStatus.inUse,
|
||||
EquipmentStatus.maintenance,
|
||||
EquipmentStatus.outOfService,
|
||||
].map((status) {
|
||||
String label;
|
||||
switch (status) {
|
||||
case EquipmentStatus.available:
|
||||
label = 'Disponible';
|
||||
break;
|
||||
case EquipmentStatus.inUse:
|
||||
label = 'En prestation';
|
||||
break;
|
||||
case EquipmentStatus.maintenance:
|
||||
label = 'En maintenance';
|
||||
break;
|
||||
case EquipmentStatus.outOfService:
|
||||
label = 'Hors service';
|
||||
break;
|
||||
default:
|
||||
label = 'Autre';
|
||||
}
|
||||
return DropdownMenuItem(
|
||||
value: status,
|
||||
child: Text(label),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedStatus = value;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Section Caractéristiques physiques
|
||||
Text(
|
||||
'Caractéristiques physiques',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Poids
|
||||
TextFormField(
|
||||
controller: _weightController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Poids à vide (kg)',
|
||||
hintText: 'ex: 15.5',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.scale),
|
||||
),
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
if (double.tryParse(value) == null) {
|
||||
return 'Veuillez entrer un nombre valide';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Card 2: Caractéristiques Physiques
|
||||
_buildCard(
|
||||
title: 'Caractéristiques physiques',
|
||||
icon: Icons.scale_outlined,
|
||||
children: [
|
||||
// Poids
|
||||
TextFormField(
|
||||
controller: _weightController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Poids à vide (kg)',
|
||||
hintText: 'ex: 15.5',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.scale),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
if (double.tryParse(value) == null) {
|
||||
return 'Veuillez entrer un nombre valide';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Dimensions
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _lengthController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Longueur (cm)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType:
|
||||
TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
if (double.tryParse(value) == null) {
|
||||
return 'Nombre invalide';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// Dimensions
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _lengthController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Longueur (cm)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
if (double.tryParse(value) == null) {
|
||||
return 'Nombre invalide';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _widthController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Largeur (cm)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
if (double.tryParse(value) == null) {
|
||||
return 'Nombre invalide';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _heightController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Hauteur (cm)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
if (double.tryParse(value) == null) {
|
||||
return 'Nombre invalide';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _widthController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Largeur (cm)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType:
|
||||
TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
if (double.tryParse(value) == null) {
|
||||
return 'Nombre invalide';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
controller: _heightController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Hauteur (cm)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType:
|
||||
TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
if (double.tryParse(value) == null) {
|
||||
return 'Nombre invalide';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Section Équipements
|
||||
Text(
|
||||
'Équipements dans ce container',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
const SizedBox(height: 16),
|
||||
// Card 3: Équipements dans ce container
|
||||
_buildCard(
|
||||
title: 'Équipements dans ce container',
|
||||
icon: Icons.inventory_2_outlined,
|
||||
children: [
|
||||
if (_selectedEquipmentIds.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.grey.shade50,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${_selectedEquipmentIds.length} équipement(s) sélectionné(s) :',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _selectedEquipmentIds.map((id) {
|
||||
return Chip(
|
||||
label: Text(
|
||||
id,
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||
),
|
||||
deleteIcon: const Icon(Icons.close, size: 16),
|
||||
onDeleted: () {
|
||||
setState(() {
|
||||
_selectedEquipmentIds.remove(id);
|
||||
});
|
||||
},
|
||||
backgroundColor: AppColors.rouge.withValues(alpha: 0.08),
|
||||
side: BorderSide(color: AppColors.rouge.withValues(alpha: 0.2)),
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.grey.shade50,
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Aucun équipement sélectionné',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Liste des équipements sélectionnés
|
||||
if (_selectedEquipmentIds.isNotEmpty)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${_selectedEquipmentIds.length} équipement(s) sélectionné(s)',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: _selectedEquipmentIds.map((id) {
|
||||
return Chip(
|
||||
label: Text(id),
|
||||
deleteIcon: const Icon(Icons.close, size: 18),
|
||||
onDeleted: () {
|
||||
setState(() {
|
||||
_selectedEquipmentIds.remove(id);
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.grey.shade50,
|
||||
),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
'Aucun équipement sélectionné',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
// Bouton pour ajouter des équipements
|
||||
OutlinedButton.icon(
|
||||
onPressed: _selectEquipment,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Ajouter des équipements'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
side: BorderSide(color: AppColors.rouge),
|
||||
foregroundColor: AppColors.rouge,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Bouton pour ajouter des équipements
|
||||
OutlinedButton.icon(
|
||||
onPressed: _selectEquipment,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Ajouter des équipements'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
// Card 4: Notes
|
||||
_buildCard(
|
||||
title: 'Notes & Remarques',
|
||||
icon: Icons.notes_outlined,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _notesController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Notes complémentaires',
|
||||
hintText: 'Informations additionnelles...',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.edit_note),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Actions
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler', style: TextStyle(fontSize: 16)),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _saveContainer,
|
||||
icon: const Icon(Icons.save, color: Colors.white),
|
||||
label: Text(
|
||||
_isEditing ? 'Enregistrer' : 'Créer',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.rouge,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Notes
|
||||
TextFormField(
|
||||
controller: _notesController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Notes',
|
||||
hintText: 'Informations additionnelles...',
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.notes),
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Boutons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _saveContainer,
|
||||
icon: const Icon(Icons.save, color: Colors.white),
|
||||
label: Text(
|
||||
_isEditing ? 'Mettre à jour' : 'Créer',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.rouge,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -923,17 +923,8 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
||||
try {
|
||||
// Récupérer les équipements sélectionnés
|
||||
final provider = context.read<EquipmentProvider>();
|
||||
final List<EquipmentModel> selectedEquipment = [];
|
||||
|
||||
// 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;
|
||||
}
|
||||
final List<EquipmentModel> selectedEquipment =
|
||||
await provider.getEquipmentsByIds(selectedIds.toList());
|
||||
|
||||
// Fermer l'indicateur de chargement
|
||||
if (mounted) {
|
||||
|
||||
@@ -36,12 +36,15 @@ class MonthView extends StatelessWidget {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
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 -
|
||||
(_calendarPadding * 2) -
|
||||
_headerHeight -
|
||||
_headerVerticalPadding -
|
||||
_daysOfWeekHeight;
|
||||
final rowHeight = availableHeight / rowCount;
|
||||
_daysOfWeekHeight -
|
||||
16.0;
|
||||
final rowHeight = (availableHeight > 0 ? availableHeight : 200.0) / rowCount;
|
||||
|
||||
return Container(
|
||||
height: constraints.maxHeight,
|
||||
|
||||
@@ -40,7 +40,9 @@ class _EquipmentAssociatedEventsSectionState
|
||||
}
|
||||
|
||||
Future<void> _loadEvents() async {
|
||||
setState(() => _isLoading = true);
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = true);
|
||||
}
|
||||
|
||||
try {
|
||||
// Récupérer TOUS les événements via l'API
|
||||
@@ -128,12 +130,16 @@ class _EquipmentAssociatedEventsSectionState
|
||||
// Trier par date
|
||||
filteredEvents.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||
|
||||
setState(() {
|
||||
_events = filteredEvents;
|
||||
_isLoading = false;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_events = filteredEvents;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _isLoading = false);
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -207,7 +213,9 @@ class _EquipmentAssociatedEventsSectionState
|
||||
],
|
||||
),
|
||||
onSelected: (filter) {
|
||||
setState(() => _selectedFilter = filter);
|
||||
if (mounted) {
|
||||
setState(() => _selectedFilter = filter);
|
||||
}
|
||||
_loadEvents();
|
||||
},
|
||||
itemBuilder: (context) => EventFilter.values.map((filter) {
|
||||
|
||||
@@ -34,7 +34,9 @@ class _EquipmentCurrentEventsSectionState
|
||||
}
|
||||
|
||||
Future<void> _loadCurrentEvents() async {
|
||||
setState(() => _isLoading = true);
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = true);
|
||||
}
|
||||
|
||||
try {
|
||||
// Récupérer TOUS les événements via l'API
|
||||
@@ -106,12 +108,16 @@ class _EquipmentCurrentEventsSectionState
|
||||
// Trier par date
|
||||
events.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||
|
||||
setState(() {
|
||||
_events = events;
|
||||
_isLoading = false;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_events = events;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _isLoading = false);
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
|
||||
@@ -24,10 +24,8 @@
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
<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 -->
|
||||
<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-title" content="EM2RP">
|
||||
<link rel="apple-touch-icon" href="favicon.jpg">
|
||||
|
||||
Reference in New Issue
Block a user