feat(ui): improve mobile equipment card and unify travel cost dialog

This commit is contained in:
ElPoyo
2026-06-05 19:26:07 +02:00
parent cb94badafe
commit 752b86ec58
8 changed files with 179 additions and 53 deletions
+14 -13
View File
@@ -34,16 +34,17 @@ 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,1779802456392,7626a7c596308bd2eb1add2ed984cd6dda5d4a3f0dedb3338244d2ae45c496cf version.json,1780665878737,68547869ca05df156c49f1b529d27d6b3f9aa32f5e172ba673c2d6bfd369a89e
index.html,1779802461141,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10 index.html,1780665885661,7dd070b4e8d51aa49d899690a8e2603bf8de2bd9e31f202a84d3f19ff280ae4b
flutter_service_worker.js,1779802555396,fadec2c9a1e8e16c22e332aef080b0b2aacc3998c4e260e5821b79afb9e000da flutter_service_worker.js,1780665975318,9533de2726499249f727cb931dcb97b27a8ac01f13ccf6aa299c8d7eafb5e71e
flutter_bootstrap.js,1779802461128,ad20054b92acf16bb75fbffd65f81c63c6d3cb6d752f799230dca5f2118af783 assets/FontManifest.json,1780665970544,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
assets/FontManifest.json,1779802551869,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5 assets/AssetManifest.json,1780665970542,a75793b913dabecfa1b5eade2735ec6f8f280210572cf25d0ce010d2a6d95e8f
assets/AssetManifest.json,1779802551869,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6 assets/AssetManifest.bin,1780665970544,61f45b0f17a664717759ebf0f1271212f4658bd4b83e35bd5d134689b73a01d6
assets/AssetManifest.bin.json,1779802551869,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53 assets/AssetManifest.bin.json,1780665970542,a26149597dbbee9aeaec0377943061e2b76c73bf974246f6af0b1a82e7455eaa
assets/AssetManifest.bin,1779802551869,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907 flutter_bootstrap.js,1780665885636,0806f900f0da893ed0ee72b8fc42b6bed886a3c74165d69e1db4cedf41559e2a
assets/shaders/ink_sparkle.frag,1779802552052,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406 assets/packages/flutter_map/lib/assets/flutter_map_logo.png,1780664966224,26fe50c9203ccf93512b80d4ee1a7578184a910457b36a6a5b7d41b799efb966
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1779802554433,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1780665974258,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
assets/fonts/MaterialIcons-Regular.otf,1779802554440,710dc8fc35289048b52970355f64206fb1b2c5e67c71ae77a46b53f0e2daecd6 assets/shaders/ink_sparkle.frag,1780665970843,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
assets/NOTICES,1779802551871,5522e1307c65771d1fbf26fcd9dc0548c751413f42196c4acaba5ee674eede1e assets/fonts/MaterialIcons-Regular.otf,1780665974268,1551d6f32ad2bc8ed771c39fdef3fc01f0188517d44127e47ad826188ca8c82f
main.dart.js,1779802550741,ab892e930c97940c1ea4ff33079922082c7f688047d307acad0644a78cfda2d7 assets/NOTICES,1780665970544,102c38a40984e6287801c2c98c5920dab4edead49226e54933bf706c4d1bb18e
main.dart.js,1780665969500,785425f0c4c27c7197b0ed160f75ef9f98b250e39141df71b471d0138bf38888
+3
View File
@@ -2,6 +2,9 @@
Toutes les modifications notables de ce projet seront documentées dans ce fichier. 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 ## 27/05/2026
Ajout de la fonction retour en arriere pour la validation du chargement des equipements et des conteneurs. Ajout de la fonction retour en arriere pour la validation du chargement des equipements et des conteneurs.
+1 -3
View File
@@ -1,12 +1,10 @@
/// Configuration de la version de l'application /// Configuration de la version de l'application
class AppVersion { class AppVersion {
static const String version = '1.2.4'; static const String version = '1.2.7';
/// Retourne la version complète de l'application /// Retourne la version complète de l'application
static String get fullVersion => 'v$version'; static String get fullVersion => 'v$version';
/// Retourne la version avec un préfixe personnalisé /// Retourne la version avec un préfixe personnalisé
static String getVersionWithPrefix(String prefix) => '$prefix $version'; static String getVersionWithPrefix(String prefix) => '$prefix $version';
} }
+5 -7
View File
@@ -72,24 +72,22 @@ class RouteResult {
required double maintenanceCostPerKm, required double maintenanceCostPerKm,
required int nbTechnicians, required int nbTechnicians,
required double hourlyRate, required double hourlyRate,
bool applyFreeZone = false, double freeZoneKm = 0,
double freeZoneMinutes = 0,
}) { }) {
const freeKm = 20.0;
const freeMinutes = 20.0;
return fuelCost( return fuelCost(
consumptionPer100km: consumptionPer100km, consumptionPer100km: consumptionPer100km,
fuelPricePerLiter: fuelPricePerLiter, fuelPricePerLiter: fuelPricePerLiter,
freeZoneKm: applyFreeZone ? freeKm : 0, freeZoneKm: freeZoneKm,
) + ) +
maintenanceCost( maintenanceCost(
costPerKm: maintenanceCostPerKm, costPerKm: maintenanceCostPerKm,
freeZoneKm: applyFreeZone ? freeKm : 0, freeZoneKm: freeZoneKm,
) + ) +
laborCost( laborCost(
nbTechnicians: nbTechnicians, nbTechnicians: nbTechnicians,
hourlyRate: hourlyRate, hourlyRate: hourlyRate,
freeZoneMinutes: applyFreeZone ? freeMinutes : 0, freeZoneMinutes: freeZoneMinutes,
) + ) +
tollCost; tollCost;
} }
+72 -5
View File
@@ -523,6 +523,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
Widget _buildEquipmentCard(EquipmentModel equipment) { Widget _buildEquipmentCard(EquipmentModel equipment) {
final isSelected = isItemSelected(equipment.id); final isSelected = isItemSelected(equipment.id);
final isMobile = MediaQuery.of(context).size.width < 800;
// ✅ RepaintBoundary pour isoler le repaint de chaque carte // ✅ RepaintBoundary pour isoler le repaint de chaque carte
return RepaintBoundary( return RepaintBoundary(
@@ -563,9 +564,12 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
}, },
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 1. leading selection or icon // 1. leading selection or icon
if (isSelectionMode) if (isSelectionMode)
Padding( Padding(
@@ -642,15 +646,15 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
// 3. Status Badge OR Quantity Display (centered vertically in the row!) // 3. Status Badge OR Quantity Display (centered vertically in the row!)
Padding( Padding(
padding: const EdgeInsets.only(right: 16), padding: EdgeInsets.only(right: (!isSelectionMode && !isMobile) ? 16 : 0),
child: (equipment.category == EquipmentCategory.consumable || child: (equipment.category == EquipmentCategory.consumable ||
equipment.category == EquipmentCategory.cable) equipment.category == EquipmentCategory.cable)
? _buildQuantityDisplay(equipment) ? _buildQuantityDisplay(equipment)
: EquipmentStatusBadge(equipment: equipment), : EquipmentStatusBadge(equipment: equipment),
), ),
// 4. Trailing Action Buttons // 4. Trailing Action Buttons (Desktop inline)
if (!isSelectionMode) if (!isSelectionMode && !isMobile)
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -711,6 +715,69 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
), ),
], ],
), ),
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),
),
),
],
),
],
],
),
), ),
), ),
), ),
@@ -46,6 +46,8 @@ class _TravelCostDialogState extends State<TravelCostDialog> {
int _nbTechnicians = 2; int _nbTechnicians = 2;
double _hourlyRate = 25.0; double _hourlyRate = 25.0;
bool _applyFreeZone = false; bool _applyFreeZone = false;
double _freeZoneKm = 20.0;
int _freeZoneMinutes = 20;
// Résultats // Résultats
List<RouteResult> _routes = []; List<RouteResult> _routes = [];
@@ -142,7 +144,8 @@ class _TravelCostDialogState extends State<TravelCostDialog> {
maintenanceCostPerKm: _selectedVehicle!.maintenanceCostPerKm, maintenanceCostPerKm: _selectedVehicle!.maintenanceCostPerKm,
nbTechnicians: _nbTechnicians, nbTechnicians: _nbTechnicians,
hourlyRate: _hourlyRate, hourlyRate: _hourlyRate,
applyFreeZone: _applyFreeZone, freeZoneKm: _applyFreeZone ? _freeZoneKm : 0.0,
freeZoneMinutes: _applyFreeZone ? _freeZoneMinutes.toDouble() : 0.0,
); );
Navigator.of(context).pop(total); Navigator.of(context).pop(total);
} }
@@ -230,29 +233,32 @@ class _TravelCostDialogState extends State<TravelCostDialog> {
); );
} }
if (_routes.isEmpty) {
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: _buildConfigPanel(),
);
}
// Résultats
final screenW = MediaQuery.of(context).size.width; final screenW = MediaQuery.of(context).size.width;
final isWide = screenW > 900; final isWide = screenW > 900;
if (isWide) { if (isWide) {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
flex: 3, 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( Expanded(
flex: 2, flex: 2,
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(16), 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<TravelCostDialog> {
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
SizedBox( if (_routes.isNotEmpty)
height: 260, SizedBox(
child: RouteMapWidget(routes: _routes, selectedRoute: _selectedRoute), height: 260,
), child: RouteMapWidget(routes: _routes, selectedRoute: _selectedRoute),
),
Padding( Padding(
padding: const EdgeInsets.all(16), 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<TravelCostDialog> {
activeColor: AppColors.rouge, activeColor: AppColors.rouge,
title: const Text('Appliquer la zone de gratuité'), title: const Text('Appliquer la zone de gratuité'),
subtitle: 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, contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading, 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), const SizedBox(height: 24),
@@ -492,21 +550,22 @@ class _TravelCostDialogState extends State<TravelCostDialog> {
maintenanceCostPerKm: vehicle.maintenanceCostPerKm, maintenanceCostPerKm: vehicle.maintenanceCostPerKm,
nbTechnicians: _nbTechnicians, nbTechnicians: _nbTechnicians,
hourlyRate: _hourlyRate, hourlyRate: _hourlyRate,
applyFreeZone: _applyFreeZone, freeZoneKm: _applyFreeZone ? _freeZoneKm : 0.0,
freeZoneMinutes: _applyFreeZone ? _freeZoneMinutes.toDouble() : 0.0,
); );
final fuel = route.fuelCost( final fuel = route.fuelCost(
consumptionPer100km: vehicle.consumptionPer100km, consumptionPer100km: vehicle.consumptionPer100km,
fuelPricePerLiter: fuelPrice, fuelPricePerLiter: fuelPrice,
freeZoneKm: _applyFreeZone ? 20 : 0, freeZoneKm: _applyFreeZone ? _freeZoneKm : 0.0,
); );
final maint = route.maintenanceCost( final maint = route.maintenanceCost(
costPerKm: vehicle.maintenanceCostPerKm, costPerKm: vehicle.maintenanceCostPerKm,
freeZoneKm: _applyFreeZone ? 20 : 0, freeZoneKm: _applyFreeZone ? _freeZoneKm : 0.0,
); );
final labor = route.laborCost( final labor = route.laborCost(
nbTechnicians: _nbTechnicians, nbTechnicians: _nbTechnicians,
hourlyRate: _hourlyRate, hourlyRate: _hourlyRate,
freeZoneMinutes: _applyFreeZone ? 20 : 0, freeZoneMinutes: _applyFreeZone ? _freeZoneMinutes.toDouble() : 0.0,
); );
final isToll = route.routeType == 'TOLL'; final isToll = route.routeType == 'TOLL';
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"version": "1.2.4", "version": "1.2.7",
"updateUrl": "https://app.em2events.fr", "updateUrl": "https://app.em2events.fr",
"forceUpdate": true, "forceUpdate": true,
"releaseNotes": "Ajout de la fonction retour en arriere pour la validation du chargement des equipements et des conteneurs.", "releaseNotes": "Ajout de la fonctionnalité de génération d'itinéraire avec calcul de coût.",
"timestamp": "2026-05-27T20:00:59.431Z" "timestamp": "2026-06-05T14:03:37.401Z"
} }