From 0551f0b9c1bd06b52128d74c136dfffd7fc413a2 Mon Sep 17 00:00:00 2001 From: ElPoyo Date: Mon, 30 Mar 2026 17:12:48 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Mise=20=C3=A0=20jour=20=C3=A0=20la=20ve?= =?UTF-8?q?rsion=201.1.20=20et=20am=C3=A9lioration=20de=20la=20recherche?= =?UTF-8?q?=20d'=C3=A9quipements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mise à jour de la version de l'application à `1.1.20` dans `app_version.dart`, `version.json` et `CHANGELOG.md`. - Optimisation de la fonction Cloud `getEquipmentsPaginated` pour supporter la recherche par ID exact (document ID ou ID legacy) et améliorer la recherche textuelle avec filtrage par lots. - Amélioration de la gestion des alertes dans `processEquipmentValidation.js` : - Ajout d'un statut `NOT_TAKEN` pour éviter les fausses alertes d'équipements perdus s'ils n'ont jamais été emportés. - Refonte complète du parsing des dates Firestore pour une meilleure robustesse dans les alertes. - Correction de la validation des quantités (vérification du type `number`). - Ajout de méthodes statiques dans `EventPreparationService` (`shouldMarkEquipmentAsLost`, `isEquipmentNotTakenToEvent`) pour centraliser la logique de détermination du statut des équipements au retour. - Mise à jour de `EventPreparationPage` pour intégrer le nouveau statut `NOT_TAKEN` et utiliser la logique centralisée du service de préparation. - Mise à jour des fichiers de cache Firebase Hosting. --- em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache | 26 +- em2rp/CHANGELOG.md | 3 + em2rp/functions/index.js | 225 +++++++++++++++--- em2rp/functions/processEquipmentValidation.js | 57 ++++- em2rp/lib/config/app_version.dart | 2 +- .../services/event_preparation_service.dart | 38 +++ em2rp/lib/views/event_preparation_page.dart | 32 ++- em2rp/web/version.json | 6 +- 8 files changed, 323 insertions(+), 66 deletions(-) diff --git a/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache b/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache index 4795fc5..fb2728c 100644 --- a/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache +++ b/em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache @@ -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,1773324020831,d5cd7334d7c3a990dbff0821b9aaab39129803e306b0d96599b8adc6d4f433a6 -index.html,1773324025840,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10 -flutter_service_worker.js,1773324116910,8582e401e070055f59183c207cf7a7e6a9219a50f5089a24a77d91d3ff77dcbc -flutter_bootstrap.js,1773324025827,74eaa66055c715df232ee96fc4114d5473f67717278fb4effa38d8b1b362e303 -assets/FontManifest.json,1773324113335,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5 -assets/AssetManifest.json,1773324113335,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6 -assets/AssetManifest.bin.json,1773324113335,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53 -assets/AssetManifest.bin,1773324113335,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907 -assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1773324115847,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb -assets/shaders/ink_sparkle.frag,1773324113551,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406 -assets/fonts/MaterialIcons-Regular.otf,1773324115852,d1409c3c8050990bdc63a413539d600245a27c9794a053c211299cc86d4f6a5c -assets/NOTICES,1773324113339,1d9a08da58db7959b9607f0f1f342f96243af76dc608ed659614d586ec58cd79 -main.dart.js,1773324112059,bfc66ab7e817db63dee4b996af3dea0629c4c4e87ba91070c15b133ab5104848 +version.json,1774883074073,049c47e9089dc5497475a6cf7733e11235bc9cfa30d458cc9a8eae761214c2b8 +flutter_service_worker.js,1774883173949,00cc791f6cc0d2beb4b16cc382b049268125aa6a7c5b73cd4bc89a003fc70f3a +index.html,1774883102020,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10 +flutter_bootstrap.js,1774883102005,80bbca812eb76632e250fe5c6b726db647443cbabc7f90010618e6a6f445d222 +assets/FontManifest.json,1774883170660,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5 +assets/AssetManifest.bin,1774883170657,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907 +assets/AssetManifest.bin.json,1774883170660,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53 +assets/shaders/ink_sparkle.frag,1774883170848,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406 +assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1774883173201,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb +assets/AssetManifest.json,1774883170657,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6 +assets/fonts/MaterialIcons-Regular.otf,1774883173207,d1409c3c8050990bdc63a413539d600245a27c9794a053c211299cc86d4f6a5c +assets/NOTICES,1774883170660,1d9a08da58db7959b9607f0f1f342f96243af76dc608ed659614d586ec58cd79 +main.dart.js,1774883168025,bc4bc60206728a982496fe5977f48e690fe8abdfd1167a9226de18fe0052cdcf diff --git a/em2rp/CHANGELOG.md b/em2rp/CHANGELOG.md index a0b12ee..0e559c3 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. +## 30/03/2026 +Patch bug envoi d'alerte equipement perdu, date dans les alertes, recherche par ID d'équipement. + ## 24/03/2026 Fix BUG : Problème de cache avec les équipements non affichés dans le dialog de sélection d'équipement. Amélioration de la gestion du cache pour éviter les problèmes d'affichage. diff --git a/em2rp/functions/index.js b/em2rp/functions/index.js index 2bec9a4..4d7c843 100644 --- a/em2rp/functions/index.js +++ b/em2rp/functions/index.js @@ -3825,18 +3825,97 @@ exports.getEquipmentsPaginated = onRequest(httpOptions, withCors(async (req, res // Convertir en majuscules pour correspondre au format Firestore const category = params.category ? params.category.toUpperCase() : null; const status = params.status ? params.status.toUpperCase() : null; - const searchQuery = params.searchQuery?.toLowerCase() || null; + const rawSearchQuery = typeof params.searchQuery === 'string' ? params.searchQuery.trim() : ''; + const searchQuery = rawSearchQuery ? rawSearchQuery.toLowerCase() : null; + const compactSearchQuery = searchQuery ? searchQuery.replace(/[\s_-]+/g, '') : null; const sortBy = params.sortBy || 'id'; const sortOrder = params.sortOrder === 'desc' ? 'desc' : 'asc'; logger.info(`[getEquipmentsPaginated] Params: limit=${limit}, startAfter=${startAfterId}, category=${category}, status=${status}, search=${searchQuery}`); + // Fast-path pour une recherche d'ID exact: évite le cap queryLimit lors d'une recherche précise. + if (searchQuery && !startAfterId) { + const exactIdCandidates = Array.from(new Set([ + rawSearchQuery, + rawSearchQuery.toUpperCase(), + rawSearchQuery.toLowerCase() + ].filter(Boolean))); + + for (const candidateId of exactIdCandidates) { + const exactDoc = await db.collection('equipments').doc(candidateId).get(); + if (!exactDoc.exists) { + continue; + } + + const exactData = exactDoc.data() || {}; + const matchesCategory = !category || exactData.category === category; + const matchesStatus = !status || exactData.status === status; + if (!matchesCategory || !matchesStatus) { + continue; + } + + if (!canManage) { + delete exactData.purchasePrice; + delete exactData.rentalPrice; + } + + const exactEquipment = { + ...helpers.serializeTimestamps(exactData, ['purchaseDate', 'nextMaintenanceDate', 'lastMaintenanceDate', 'createdAt', 'updatedAt']), + id: exactDoc.id + }; + + logger.info(`[getEquipmentsPaginated] Exact ID hit for ${exactDoc.id}`); + res.status(200).json({ + equipments: [exactEquipment], + hasMore: false, + lastVisible: exactDoc.id, + total: 1 + }); + return; + } + + // Compatibilité legacy: certains documents peuvent stocker un ancien champ `id` différent du document ID. + for (const legacyId of exactIdCandidates) { + let legacyIdQuery = db.collection('equipments').where('id', '==', legacyId); + if (category) { + legacyIdQuery = legacyIdQuery.where('category', '==', category); + } + if (status) { + legacyIdQuery = legacyIdQuery.where('status', '==', status); + } + + const legacySnapshot = await legacyIdQuery.limit(1).get(); + if (legacySnapshot.empty) { + continue; + } + + const exactDoc = legacySnapshot.docs[0]; + const exactData = exactDoc.data() || {}; + + if (!canManage) { + delete exactData.purchasePrice; + delete exactData.rentalPrice; + } + + const exactEquipment = { + ...helpers.serializeTimestamps(exactData, ['purchaseDate', 'nextMaintenanceDate', 'lastMaintenanceDate', 'createdAt', 'updatedAt']), + id: exactDoc.id + }; + + logger.info(`[getEquipmentsPaginated] Exact legacy ID hit for ${legacyId} -> ${exactDoc.id}`); + res.status(200).json({ + equipments: [exactEquipment], + hasMore: false, + lastVisible: exactDoc.id, + total: 1 + }); + return; + } + } + // Construire la requête Firestore let query = db.collection('equipments'); - // Si recherche textuelle, on augmente la limite pour filtrer ensuite - const queryLimit = searchQuery ? Math.min(limit * 10, 200) : limit; - // Appliquer les filtres if (category) { query = query.where('category', '==', category); @@ -3861,20 +3940,10 @@ exports.getEquipmentsPaginated = onRequest(httpOptions, withCors(async (req, res } } - // Limiter les résultats - query = query.limit(queryLimit + 1); + const timestampFields = ['purchaseDate', 'nextMaintenanceDate', 'lastMaintenanceDate', 'createdAt', 'updatedAt']; - const snapshot = await query.get(); - - // Déterminer hasMore basé sur le nombre de documents Firestore - const rawDocCount = snapshot.docs.length; - const hasMoreDocs = rawDocCount > queryLimit; - const docsToProcess = hasMoreDocs ? snapshot.docs.slice(0, queryLimit) : snapshot.docs; - - logger.info(`[getEquipmentsPaginated] Firestore returned ${rawDocCount} docs, hasMore=${hasMoreDocs}`); - - let equipments = docsToProcess.map(doc => { - const data = doc.data(); + const mapEquipmentDoc = (doc) => { + const data = {...(doc.data() || {})}; // Masquer les prix si l'utilisateur n'a pas manage_equipment if (!canManage) { @@ -3882,36 +3951,116 @@ exports.getEquipmentsPaginated = onRequest(httpOptions, withCors(async (req, res delete data.rentalPrice; } - return { - id: doc.id, - ...helpers.serializeTimestamps(data, ['purchaseDate', 'nextMaintenanceDate', 'lastMaintenanceDate', 'createdAt', 'updatedAt']) - }; - }); + const legacyId = typeof data.id === 'string' ? data.id : ''; - // Filtrage textuel côté serveur - if (searchQuery) { - equipments = equipments.filter(eq => { - const searchableText = [ - eq.name || '', - eq.id || '', - eq.model || '', - eq.brand || '', - eq.subCategory || '' - ].join(' ').toLowerCase(); - return searchableText.includes(searchQuery); + return { + ...helpers.serializeTimestamps(data, timestampFields), + id: doc.id, + _legacyId: legacyId + }; + }; + + const matchesSearchQuery = (equipment) => { + const searchableText = [ + equipment.name || '', + equipment.id || '', + equipment._legacyId || '', + equipment.model || '', + equipment.brand || '', + equipment.subCategory || '' + ].join(' ').toLowerCase(); + + if (searchableText.includes(searchQuery)) { + return true; + } + + if (!compactSearchQuery) { + return false; + } + + const compactSearchableText = searchableText.replace(/[\s_-]+/g, ''); + return compactSearchableText.includes(compactSearchQuery); + }; + + if (!searchQuery) { + const snapshot = await query.limit(limit + 1).get(); + const rawDocCount = snapshot.docs.length; + const hasMoreDocs = rawDocCount > limit; + const docsToProcess = hasMoreDocs ? snapshot.docs.slice(0, limit) : snapshot.docs; + + const limitedEquipments = docsToProcess + .map(mapEquipmentDoc) + .map(({_legacyId, ...equipment}) => equipment); + const lastVisible = limitedEquipments.length > 0 ? limitedEquipments[limitedEquipments.length - 1].id : null; + + logger.info(`[getEquipmentsPaginated] Firestore returned ${rawDocCount} docs, hasMore=${hasMoreDocs}`); + logger.info(`[getEquipmentsPaginated] Returning ${limitedEquipments.length} equipments, hasMore=${hasMoreDocs}`); + + res.status(200).json({ + equipments: limitedEquipments, + hasMore: hasMoreDocs, + lastVisible, + total: limitedEquipments.length }); + return; } - // Pour la limite finale après filtrage textuel - const limitedEquipments = equipments.slice(0, limit); + // En mode recherche, scanner la collection par lots jusqu'à obtenir `limit + 1` matchs + // afin de garantir des résultats même si les documents pertinents sont loin dans l'ordre de tri. + const searchBatchSize = Math.min(Math.max(limit * 10, limit), 200); + const matchedEquipments = []; + let scannedDocuments = 0; + let searchQueryRef = query; + let hasMoreMatches = false; + let hasMoreDocsToScan = true; + + while (hasMoreDocsToScan && !hasMoreMatches) { + const snapshot = await searchQueryRef.limit(searchBatchSize).get(); + + if (snapshot.empty) { + hasMoreDocsToScan = false; + break; + } + + scannedDocuments += snapshot.docs.length; + + for (const doc of snapshot.docs) { + const equipment = mapEquipmentDoc(doc); + if (!matchesSearchQuery(equipment)) { + continue; + } + + matchedEquipments.push(equipment); + if (matchedEquipments.length > limit) { + hasMoreMatches = true; + break; + } + } + + if (hasMoreMatches) { + break; + } + + if (snapshot.docs.length < searchBatchSize) { + hasMoreDocsToScan = false; + break; + } + + const lastDocInBatch = snapshot.docs[snapshot.docs.length - 1]; + searchQueryRef = query.startAfter(lastDocInBatch); + } + + const limitedEquipments = matchedEquipments + .slice(0, limit) + .map(({_legacyId, ...equipment}) => equipment); const lastVisible = limitedEquipments.length > 0 ? limitedEquipments[limitedEquipments.length - 1].id : null; - // hasMore reste basé sur le nombre de docs Firestore, pas sur le filtrage textuel - logger.info(`[getEquipmentsPaginated] Returning ${limitedEquipments.length} equipments (filtered from ${equipments.length}), hasMore=${hasMoreDocs}`); + logger.info(`[getEquipmentsPaginated] Search scan read ${scannedDocuments} docs and found ${matchedEquipments.length} matches`); + logger.info(`[getEquipmentsPaginated] Returning ${limitedEquipments.length} equipments, hasMore=${hasMoreMatches}`); res.status(200).json({ equipments: limitedEquipments, - hasMore: hasMoreDocs, + hasMore: hasMoreMatches, lastVisible, total: limitedEquipments.length }); diff --git a/em2rp/functions/processEquipmentValidation.js b/em2rp/functions/processEquipmentValidation.js index 8aa424c..d7c800f 100644 --- a/em2rp/functions/processEquipmentValidation.js +++ b/em2rp/functions/processEquipmentValidation.js @@ -50,6 +50,11 @@ exports.processEquipmentValidation = onCall({ for (const equipment of equipmentList) { const {equipmentId, status, quantity, expectedQuantity} = equipment; + // Équipement non emporté: pas d'alerte de perte/manquant au retour. + if (status === 'NOT_TAKEN') { + continue; + } + // Cas 1: Équipement PERDU if (status === 'LOST') { const alertData = await createAlertInFirestore({ @@ -91,7 +96,9 @@ exports.processEquipmentValidation = onCall({ } // Cas 3: Quantité incorrecte - if (expectedQuantity && quantity !== expectedQuantity) { + const hasExpectedQuantity = typeof expectedQuantity === 'number'; + const hasActualQuantity = typeof quantity === 'number'; + if (hasExpectedQuantity && hasActualQuantity && quantity !== expectedQuantity) { const alertData = await createAlertInFirestore({ type: 'QUANTITY_MISMATCH', severity: 'INFO', @@ -409,10 +416,48 @@ async function sendAlertEmails(alert, userIds) { * Formate la date d'un événement */ function formatEventDate(event) { - if (event.startDate) { - const date = event.startDate.toDate ? event.startDate.toDate() : new Date(event.startDate); - return date.toLocaleDateString('fr-FR', {day: 'numeric', month: 'numeric', year: 'numeric'}); - } - return 'Date inconnue'; + const rawDate = + event?.StartDateTime || + event?.startDateTime || + event?.startDate || + event?.eventDate; + + const parsedDate = parseFirestoreDate(rawDate); + const safeDate = parsedDate || new Date(); + + return safeDate.toLocaleDateString('fr-FR', { + day: 'numeric', + month: 'numeric', + year: 'numeric', + }); +} + +function parseFirestoreDate(value) { + if (!value) { + return null; + } + + if (typeof value.toDate === 'function') { + return value.toDate(); + } + + if (value instanceof Date) { + return value; + } + + if (typeof value === 'string' || typeof value === 'number') { + const date = new Date(value); + return Number.isNaN(date.getTime()) ? null : date; + } + + if (typeof value === 'object' && typeof value.seconds === 'number') { + return new Date(value.seconds * 1000); + } + + if (typeof value === 'object' && typeof value._seconds === 'number') { + return new Date(value._seconds * 1000); + } + + return null; } diff --git a/em2rp/lib/config/app_version.dart b/em2rp/lib/config/app_version.dart index c0fc0ae..7799eb6 100644 --- a/em2rp/lib/config/app_version.dart +++ b/em2rp/lib/config/app_version.dart @@ -1,6 +1,6 @@ /// Configuration de la version de l'application class AppVersion { - static const String version = '1.1.19'; + static const String version = '1.1.20'; /// Retourne la version complète de l'application static String get fullVersion => 'v$version'; diff --git a/em2rp/lib/services/event_preparation_service.dart b/em2rp/lib/services/event_preparation_service.dart index f5a8150..479bb11 100644 --- a/em2rp/lib/services/event_preparation_service.dart +++ b/em2rp/lib/services/event_preparation_service.dart @@ -4,6 +4,44 @@ import 'package:em2rp/services/api_service.dart'; class EventPreparationService { final ApiService _apiService = apiService; + /// Retourne true si l'équipement était absent du flux événementiel. + /// + /// Cas typique: matériel jamais emporté au départ, donc absent au retour, + /// mais qui ne doit jamais être classé en [LOST]. + static bool isEquipmentNotTakenToEvent({ + required bool isMissingAtReturn, + required bool isLoaded, + required bool isMissingAtLoading, + int? quantityAtLoading, + }) { + if (!isMissingAtReturn) { + return false; + } + + final loadedQuantity = quantityAtLoading ?? 0; + return !isLoaded || isMissingAtLoading || loadedQuantity <= 0; + } + + /// Retourne true uniquement si l'équipement doit être classé perdu. + static bool shouldMarkEquipmentAsLost({ + required bool isReturnValidationStep, + required bool isMissingAtReturn, + required bool isLoaded, + required bool isMissingAtLoading, + int? quantityAtLoading, + }) { + if (!isReturnValidationStep || !isMissingAtReturn) { + return false; + } + + return !isEquipmentNotTakenToEvent( + isMissingAtReturn: isMissingAtReturn, + isLoaded: isLoaded, + isMissingAtLoading: isMissingAtLoading, + quantityAtLoading: quantityAtLoading, + ); + } + // === PRÉPARATION === /// Valider un équipement individuel en préparation diff --git a/em2rp/lib/views/event_preparation_page.dart b/em2rp/lib/views/event_preparation_page.dart index a0276f9..7fd467b 100644 --- a/em2rp/lib/views/event_preparation_page.dart +++ b/em2rp/lib/views/event_preparation_page.dart @@ -13,6 +13,7 @@ import 'package:em2rp/services/qr_code_processing_service.dart'; import 'package:em2rp/services/audio_feedback_service.dart'; import 'package:em2rp/services/smart_text_to_speech_service.dart'; import 'package:em2rp/services/equipment_service.dart'; +import 'package:em2rp/services/event_preparation_service.dart'; import 'package:em2rp/views/widgets/equipment/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep; import 'package:em2rp/views/widgets/equipment/container_checklist_item.dart'; import 'package:em2rp/views/widgets/common/qr_code_scanner_dialog.dart'; @@ -1097,6 +1098,10 @@ class _EventPreparationPageState extends State with Single /// Détermine le statut d'un équipement selon l'étape actuelle String _determineEquipmentStatus(EventEquipment eq) { + if (_isNotTakenToEventAtReturn(eq)) { + return 'NOT_TAKEN'; + } + // Vérifier d'abord si l'équipement est perdu (LOST) if (_shouldMarkAsLost(eq)) { return 'LOST'; @@ -1118,14 +1123,31 @@ class _EventPreparationPageState extends State with Single /// Vérifie si un équipement doit être marqué comme LOST bool _shouldMarkAsLost(EventEquipment eq) { - // Seulement aux étapes de retour - if (_currentStep != PreparationStep.return_ && - !(_currentStep == PreparationStep.unloadingReturn && _loadSimultaneously)) { + return EventPreparationService.shouldMarkEquipmentAsLost( + isReturnValidationStep: _isReturnValidationStep, + isMissingAtReturn: eq.isMissingAtReturn, + isLoaded: eq.isLoaded, + isMissingAtLoading: eq.isMissingAtLoading, + quantityAtLoading: eq.quantityAtLoading, + ); + } + + bool _isNotTakenToEventAtReturn(EventEquipment eq) { + if (!_isReturnValidationStep) { return false; } - // Si manquant maintenant mais PAS manquant à la préparation = LOST - return eq.isMissingAtReturn && !eq.isMissingAtPreparation; + return EventPreparationService.isEquipmentNotTakenToEvent( + isMissingAtReturn: eq.isMissingAtReturn, + isLoaded: eq.isLoaded, + isMissingAtLoading: eq.isMissingAtLoading, + quantityAtLoading: eq.quantityAtLoading, + ); + } + + bool get _isReturnValidationStep { + return _currentStep == PreparationStep.return_ || + (_currentStep == PreparationStep.unloadingReturn && _loadSimultaneously); } /// Vérifie si un équipement est manquant à l'étape actuelle diff --git a/em2rp/web/version.json b/em2rp/web/version.json index 349c392..2366431 100644 --- a/em2rp/web/version.json +++ b/em2rp/web/version.json @@ -1,7 +1,7 @@ { - "version": "1.1.19", + "version": "1.1.20", "updateUrl": "https://app.em2events.fr", "forceUpdate": true, - "releaseNotes": "Fix BUG : Problème de cache avec les équipements non affichés dans le dialog de sélection d'équipement. Amélioration de la gestion du cache pour éviter les problèmes d'affichage.", - "timestamp": "2026-03-24T11:14:01.828Z" + "releaseNotes": "Patch bug envoi d'alerte equipement perdu, date dans les alertes, recherche par ID d'équipement.", + "timestamp": "2026-03-30T15:04:34.073Z" } \ No newline at end of file