diff --git a/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache b/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache index 5961b56..17e02a3 100644 --- a/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache +++ b/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache @@ -34,16 +34,17 @@ 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,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 +version.json,1780665878737,68547869ca05df156c49f1b529d27d6b3f9aa32f5e172ba673c2d6bfd369a89e +index.html,1780665885661,7dd070b4e8d51aa49d899690a8e2603bf8de2bd9e31f202a84d3f19ff280ae4b +flutter_service_worker.js,1780665975318,9533de2726499249f727cb931dcb97b27a8ac01f13ccf6aa299c8d7eafb5e71e +assets/FontManifest.json,1780665970544,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5 +assets/AssetManifest.json,1780665970542,a75793b913dabecfa1b5eade2735ec6f8f280210572cf25d0ce010d2a6d95e8f +assets/AssetManifest.bin,1780665970544,61f45b0f17a664717759ebf0f1271212f4658bd4b83e35bd5d134689b73a01d6 +assets/AssetManifest.bin.json,1780665970542,a26149597dbbee9aeaec0377943061e2b76c73bf974246f6af0b1a82e7455eaa +flutter_bootstrap.js,1780665885636,0806f900f0da893ed0ee72b8fc42b6bed886a3c74165d69e1db4cedf41559e2a +assets/packages/flutter_map/lib/assets/flutter_map_logo.png,1780664966224,26fe50c9203ccf93512b80d4ee1a7578184a910457b36a6a5b7d41b799efb966 +assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1780665974258,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb +assets/shaders/ink_sparkle.frag,1780665970843,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406 +assets/fonts/MaterialIcons-Regular.otf,1780665974268,1551d6f32ad2bc8ed771c39fdef3fc01f0188517d44127e47ad826188ca8c82f +assets/NOTICES,1780665970544,102c38a40984e6287801c2c98c5920dab4edead49226e54933bf706c4d1bb18e +main.dart.js,1780665969500,785425f0c4c27c7197b0ed160f75ef9f98b250e39141df71b471d0138bf38888 diff --git a/em2rp/CHANGELOG.md b/em2rp/CHANGELOG.md index 70fcb1c..7457d2f 100644 --- a/em2rp/CHANGELOG.md +++ b/em2rp/CHANGELOG.md @@ -2,6 +2,9 @@ Toutes les modifications notables de ce projet seront documentées dans ce fichier. +## 05/06/2026 +Ajout de la fonctionnalité de génération d'itinéraire avec calcul de coût. + ## 27/05/2026 Ajout de la fonction retour en arriere pour la validation du chargement des equipements et des conteneurs. diff --git a/em2rp/functions/.env b/em2rp/functions/.env index edfa99f..5b7ae65 100644 --- a/em2rp/functions/.env +++ b/em2rp/functions/.env @@ -8,4 +8,4 @@ SMTP_PASS="aL8@Rx8xqFrNij$a" APP_URL="https://app.em2events.fr" GEMINI_API_KEY="AIzaSyB0hOvBjWeWjdrxVARzfErZ_uGuArlvmQc" -API_MAPS="AIzaSyDt2d-T9YRmHO3-QEq1uWomdqVbJqXfO04" +API_MAPS="AIzaSyDt2d-T9YRmHO3-QEq1uWomdqVbJqXfO04" \ No newline at end of file diff --git a/em2rp/lib/config/app_version.dart b/em2rp/lib/config/app_version.dart index 08e8a69..7628252 100644 --- a/em2rp/lib/config/app_version.dart +++ b/em2rp/lib/config/app_version.dart @@ -1,12 +1,10 @@ /// Configuration de la version de l'application class AppVersion { - static const String version = '1.2.4'; + static const String version = '1.2.7'; /// Retourne la version complète de l'application static String get fullVersion => 'v$version'; - /// Retourne la version avec un préfixe personnalisé static String getVersionWithPrefix(String prefix) => '$prefix $version'; } - diff --git a/em2rp/lib/models/route_result_model.dart b/em2rp/lib/models/route_result_model.dart index 5c4380b..84f3cef 100644 --- a/em2rp/lib/models/route_result_model.dart +++ b/em2rp/lib/models/route_result_model.dart @@ -72,24 +72,22 @@ class RouteResult { required double maintenanceCostPerKm, required int nbTechnicians, required double hourlyRate, - bool applyFreeZone = false, + double freeZoneKm = 0, + double freeZoneMinutes = 0, }) { - const freeKm = 20.0; - const freeMinutes = 20.0; - return fuelCost( consumptionPer100km: consumptionPer100km, fuelPricePerLiter: fuelPricePerLiter, - freeZoneKm: applyFreeZone ? freeKm : 0, + freeZoneKm: freeZoneKm, ) + maintenanceCost( costPerKm: maintenanceCostPerKm, - freeZoneKm: applyFreeZone ? freeKm : 0, + freeZoneKm: freeZoneKm, ) + laborCost( nbTechnicians: nbTechnicians, hourlyRate: hourlyRate, - freeZoneMinutes: applyFreeZone ? freeMinutes : 0, + freeZoneMinutes: freeZoneMinutes, ) + tollCost; } diff --git a/em2rp/lib/views/equipment_management_page.dart b/em2rp/lib/views/equipment_management_page.dart index 415e1aa..66f2b83 100644 --- a/em2rp/lib/views/equipment_management_page.dart +++ b/em2rp/lib/views/equipment_management_page.dart @@ -523,6 +523,7 @@ class _EquipmentManagementPageState extends State Widget _buildEquipmentCard(EquipmentModel equipment) { final isSelected = isItemSelected(equipment.id); + final isMobile = MediaQuery.of(context).size.width < 800; // ✅ RepaintBoundary pour isoler le repaint de chaque carte return RepaintBoundary( @@ -563,9 +564,12 @@ class _EquipmentManagementPageState extends State }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ // 1. leading selection or icon if (isSelectionMode) Padding( @@ -642,15 +646,15 @@ class _EquipmentManagementPageState extends State // 3. Status Badge OR Quantity Display (centered vertically in the row!) Padding( - padding: const EdgeInsets.only(right: 16), + padding: EdgeInsets.only(right: (!isSelectionMode && !isMobile) ? 16 : 0), child: (equipment.category == EquipmentCategory.consumable || equipment.category == EquipmentCategory.cable) ? _buildQuantityDisplay(equipment) : EquipmentStatusBadge(equipment: equipment), ), - // 4. Trailing Action Buttons - if (!isSelectionMode) + // 4. Trailing Action Buttons (Desktop inline) + if (!isSelectionMode && !isMobile) Row( mainAxisSize: MainAxisSize.min, children: [ @@ -711,6 +715,69 @@ class _EquipmentManagementPageState extends State ), ], ), + if (!isSelectionMode && isMobile) ...[ + const Divider(height: 24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + // Bouton Restock (uniquement pour consommables/câbles avec permission) + if (equipment.category == EquipmentCategory.consumable || + equipment.category == EquipmentCategory.cable) + PermissionGate( + requiredPermissions: const ['manage_equipment'], + child: IconButton( + icon: const Icon(Icons.add_shopping_cart, + color: AppColors.rouge, size: 20), + tooltip: 'Restock', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () => _showRestockDialog(equipment), + ), + ), + if (equipment.category == EquipmentCategory.consumable || + equipment.category == EquipmentCategory.cable) + const SizedBox(width: 16), + // Bouton QR Code + IconButton( + icon: const Icon(Icons.qr_code, color: AppColors.rouge, size: 20), + tooltip: 'QR Code', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () => showDialog( + context: context, + builder: (context) => + QRCodeDialog.forEquipment(equipment), + ), + ), + const SizedBox(width: 16), + // Bouton Modifier (permission required) + PermissionGate( + requiredPermissions: const ['manage_equipment'], + child: IconButton( + icon: const Icon(Icons.edit, color: AppColors.rouge, size: 20), + tooltip: 'Modifier', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () => _editEquipment(equipment), + ), + ), + const SizedBox(width: 16), + // Bouton Supprimer (permission required) + PermissionGate( + requiredPermissions: const ['manage_equipment'], + child: IconButton( + icon: const Icon(Icons.delete, color: Colors.red, size: 20), + tooltip: 'Supprimer', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () => _deleteEquipment(equipment), + ), + ), + ], + ), + ], + ], + ), ), ), ), diff --git a/em2rp/lib/views/widgets/event_form/travel_cost_dialog.dart b/em2rp/lib/views/widgets/event_form/travel_cost_dialog.dart index 22ed375..bdb46f2 100644 --- a/em2rp/lib/views/widgets/event_form/travel_cost_dialog.dart +++ b/em2rp/lib/views/widgets/event_form/travel_cost_dialog.dart @@ -46,6 +46,8 @@ class _TravelCostDialogState extends State { int _nbTechnicians = 2; double _hourlyRate = 25.0; bool _applyFreeZone = false; + double _freeZoneKm = 20.0; + int _freeZoneMinutes = 20; // Résultats List _routes = []; @@ -142,7 +144,8 @@ class _TravelCostDialogState extends State { maintenanceCostPerKm: _selectedVehicle!.maintenanceCostPerKm, nbTechnicians: _nbTechnicians, hourlyRate: _hourlyRate, - applyFreeZone: _applyFreeZone, + freeZoneKm: _applyFreeZone ? _freeZoneKm : 0.0, + freeZoneMinutes: _applyFreeZone ? _freeZoneMinutes.toDouble() : 0.0, ); Navigator.of(context).pop(total); } @@ -230,29 +233,32 @@ class _TravelCostDialogState extends State { ); } - if (_routes.isEmpty) { - return SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: _buildConfigPanel(), - ); - } - - // Résultats final screenW = MediaQuery.of(context).size.width; final isWide = screenW > 900; if (isWide) { return Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 3, - child: RouteMapWidget(routes: _routes, selectedRoute: _selectedRoute), + child: _routes.isEmpty + ? const Center(child: Text("Configurez le trajet et cliquez sur Calculer")) + : RouteMapWidget(routes: _routes, selectedRoute: _selectedRoute), ), Expanded( flex: 2, child: SingleChildScrollView( padding: const EdgeInsets.all(16), - child: _buildResultsPanel(), + child: Column( + children: [ + _buildConfigPanel(), + if (_routes.isNotEmpty) ...[ + const Divider(height: 32), + _buildResultsPanel(), + ] + ], + ), ), ), ], @@ -261,13 +267,22 @@ class _TravelCostDialogState extends State { return SingleChildScrollView( child: Column( children: [ - SizedBox( - height: 260, - child: RouteMapWidget(routes: _routes, selectedRoute: _selectedRoute), - ), + if (_routes.isNotEmpty) + SizedBox( + height: 260, + child: RouteMapWidget(routes: _routes, selectedRoute: _selectedRoute), + ), Padding( padding: const EdgeInsets.all(16), - child: _buildResultsPanel(), + child: Column( + children: [ + _buildConfigPanel(), + if (_routes.isNotEmpty) ...[ + const Divider(height: 32), + _buildResultsPanel(), + ] + ], + ), ), ], ), @@ -413,10 +428,53 @@ class _TravelCostDialogState extends State { activeColor: AppColors.rouge, title: const Text('Appliquer la zone de gratuité'), subtitle: - const Text('Déduit 20 km (carburant + maintenance) et 20 min (main-d\'œuvre)'), + Text('Déduit ${_freeZoneKm.toStringAsFixed(0)} km (carburant + maintenance) et $_freeZoneMinutes min (main-d\'œuvre)'), contentPadding: EdgeInsets.zero, controlAffinity: ListTileControlAffinity.leading, ), + if (_applyFreeZone) + Padding( + padding: const EdgeInsets.only(left: 32, top: 8), + child: Row( + children: [ + Expanded( + child: TextFormField( + initialValue: _freeZoneKm.toStringAsFixed(0), + decoration: const InputDecoration( + labelText: 'Distance déduite', + border: OutlineInputBorder(), + suffixText: 'km', + ), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')) + ], + onChanged: (v) { + final parsed = double.tryParse(v); + if (parsed != null) setState(() => _freeZoneKm = parsed); + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: TextFormField( + initialValue: _freeZoneMinutes.toString(), + decoration: const InputDecoration( + labelText: 'Temps déduit', + border: OutlineInputBorder(), + suffixText: 'min', + ), + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + onChanged: (v) { + final parsed = int.tryParse(v); + if (parsed != null) setState(() => _freeZoneMinutes = parsed); + }, + ), + ), + ], + ), + ), const SizedBox(height: 24), @@ -492,21 +550,22 @@ class _TravelCostDialogState extends State { maintenanceCostPerKm: vehicle.maintenanceCostPerKm, nbTechnicians: _nbTechnicians, hourlyRate: _hourlyRate, - applyFreeZone: _applyFreeZone, + freeZoneKm: _applyFreeZone ? _freeZoneKm : 0.0, + freeZoneMinutes: _applyFreeZone ? _freeZoneMinutes.toDouble() : 0.0, ); final fuel = route.fuelCost( consumptionPer100km: vehicle.consumptionPer100km, fuelPricePerLiter: fuelPrice, - freeZoneKm: _applyFreeZone ? 20 : 0, + freeZoneKm: _applyFreeZone ? _freeZoneKm : 0.0, ); final maint = route.maintenanceCost( costPerKm: vehicle.maintenanceCostPerKm, - freeZoneKm: _applyFreeZone ? 20 : 0, + freeZoneKm: _applyFreeZone ? _freeZoneKm : 0.0, ); final labor = route.laborCost( nbTechnicians: _nbTechnicians, hourlyRate: _hourlyRate, - freeZoneMinutes: _applyFreeZone ? 20 : 0, + freeZoneMinutes: _applyFreeZone ? _freeZoneMinutes.toDouble() : 0.0, ); final isToll = route.routeType == 'TOLL'; diff --git a/em2rp/web/version.json b/em2rp/web/version.json index fb0b3b2..40f6533 100644 --- a/em2rp/web/version.json +++ b/em2rp/web/version.json @@ -1,7 +1,7 @@ { - "version": "1.2.4", + "version": "1.2.7", "updateUrl": "https://app.em2events.fr", "forceUpdate": true, - "releaseNotes": "Ajout de la fonction retour en arriere pour la validation du chargement des equipements et des conteneurs.", - "timestamp": "2026-05-27T20:00:59.431Z" + "releaseNotes": "Ajout de la fonctionnalité de génération d'itinéraire avec calcul de coût.", + "timestamp": "2026-06-05T14:03:37.401Z" } \ No newline at end of file