feat: Mise à jour à la version 1.1.20 et amélioration de la recherche d'équipements
- 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.
This commit is contained in:
@@ -34,16 +34,16 @@ assets/assets/images/tshirt-incrust.webp,1737393735487,af7cb34adfca19c0b41c8eb63
|
|||||||
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
|
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
|
||||||
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
||||||
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
||||||
version.json,1773324020831,d5cd7334d7c3a990dbff0821b9aaab39129803e306b0d96599b8adc6d4f433a6
|
version.json,1774883074073,049c47e9089dc5497475a6cf7733e11235bc9cfa30d458cc9a8eae761214c2b8
|
||||||
index.html,1773324025840,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
flutter_service_worker.js,1774883173949,00cc791f6cc0d2beb4b16cc382b049268125aa6a7c5b73cd4bc89a003fc70f3a
|
||||||
flutter_service_worker.js,1773324116910,8582e401e070055f59183c207cf7a7e6a9219a50f5089a24a77d91d3ff77dcbc
|
index.html,1774883102020,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
||||||
flutter_bootstrap.js,1773324025827,74eaa66055c715df232ee96fc4114d5473f67717278fb4effa38d8b1b362e303
|
flutter_bootstrap.js,1774883102005,80bbca812eb76632e250fe5c6b726db647443cbabc7f90010618e6a6f445d222
|
||||||
assets/FontManifest.json,1773324113335,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
assets/FontManifest.json,1774883170660,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
||||||
assets/AssetManifest.json,1773324113335,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6
|
assets/AssetManifest.bin,1774883170657,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907
|
||||||
assets/AssetManifest.bin.json,1773324113335,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53
|
assets/AssetManifest.bin.json,1774883170660,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53
|
||||||
assets/AssetManifest.bin,1773324113335,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907
|
assets/shaders/ink_sparkle.frag,1774883170848,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
||||||
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1773324115847,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1774883173201,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
||||||
assets/shaders/ink_sparkle.frag,1773324113551,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
assets/AssetManifest.json,1774883170657,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6
|
||||||
assets/fonts/MaterialIcons-Regular.otf,1773324115852,d1409c3c8050990bdc63a413539d600245a27c9794a053c211299cc86d4f6a5c
|
assets/fonts/MaterialIcons-Regular.otf,1774883173207,d1409c3c8050990bdc63a413539d600245a27c9794a053c211299cc86d4f6a5c
|
||||||
assets/NOTICES,1773324113339,1d9a08da58db7959b9607f0f1f342f96243af76dc608ed659614d586ec58cd79
|
assets/NOTICES,1774883170660,1d9a08da58db7959b9607f0f1f342f96243af76dc608ed659614d586ec58cd79
|
||||||
main.dart.js,1773324112059,bfc66ab7e817db63dee4b996af3dea0629c4c4e87ba91070c15b133ab5104848
|
main.dart.js,1774883168025,bc4bc60206728a982496fe5977f48e690fe8abdfd1167a9226de18fe0052cdcf
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
## 30/03/2026
|
||||||
|
Patch bug envoi d'alerte equipement perdu, date dans les alertes, recherche par ID d'équipement.
|
||||||
|
|
||||||
## 24/03/2026
|
## 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.
|
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.
|
||||||
|
|
||||||
|
|||||||
+185
-36
@@ -3825,18 +3825,97 @@ exports.getEquipmentsPaginated = onRequest(httpOptions, withCors(async (req, res
|
|||||||
// Convertir en majuscules pour correspondre au format Firestore
|
// Convertir en majuscules pour correspondre au format Firestore
|
||||||
const category = params.category ? params.category.toUpperCase() : null;
|
const category = params.category ? params.category.toUpperCase() : null;
|
||||||
const status = params.status ? params.status.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 sortBy = params.sortBy || 'id';
|
||||||
const sortOrder = params.sortOrder === 'desc' ? 'desc' : 'asc';
|
const sortOrder = params.sortOrder === 'desc' ? 'desc' : 'asc';
|
||||||
|
|
||||||
logger.info(`[getEquipmentsPaginated] Params: limit=${limit}, startAfter=${startAfterId}, category=${category}, status=${status}, search=${searchQuery}`);
|
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
|
// Construire la requête Firestore
|
||||||
let query = db.collection('equipments');
|
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
|
// Appliquer les filtres
|
||||||
if (category) {
|
if (category) {
|
||||||
query = query.where('category', '==', category);
|
query = query.where('category', '==', category);
|
||||||
@@ -3861,20 +3940,10 @@ exports.getEquipmentsPaginated = onRequest(httpOptions, withCors(async (req, res
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limiter les résultats
|
const timestampFields = ['purchaseDate', 'nextMaintenanceDate', 'lastMaintenanceDate', 'createdAt', 'updatedAt'];
|
||||||
query = query.limit(queryLimit + 1);
|
|
||||||
|
|
||||||
const snapshot = await query.get();
|
const mapEquipmentDoc = (doc) => {
|
||||||
|
const data = {...(doc.data() || {})};
|
||||||
// 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();
|
|
||||||
|
|
||||||
// Masquer les prix si l'utilisateur n'a pas manage_equipment
|
// Masquer les prix si l'utilisateur n'a pas manage_equipment
|
||||||
if (!canManage) {
|
if (!canManage) {
|
||||||
@@ -3882,32 +3951,50 @@ exports.getEquipmentsPaginated = onRequest(httpOptions, withCors(async (req, res
|
|||||||
delete data.rentalPrice;
|
delete data.rentalPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const legacyId = typeof data.id === 'string' ? data.id : '';
|
||||||
id: doc.id,
|
|
||||||
...helpers.serializeTimestamps(data, ['purchaseDate', 'nextMaintenanceDate', 'lastMaintenanceDate', 'createdAt', 'updatedAt'])
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filtrage textuel côté serveur
|
return {
|
||||||
if (searchQuery) {
|
...helpers.serializeTimestamps(data, timestampFields),
|
||||||
equipments = equipments.filter(eq => {
|
id: doc.id,
|
||||||
|
_legacyId: legacyId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const matchesSearchQuery = (equipment) => {
|
||||||
const searchableText = [
|
const searchableText = [
|
||||||
eq.name || '',
|
equipment.name || '',
|
||||||
eq.id || '',
|
equipment.id || '',
|
||||||
eq.model || '',
|
equipment._legacyId || '',
|
||||||
eq.brand || '',
|
equipment.model || '',
|
||||||
eq.subCategory || ''
|
equipment.brand || '',
|
||||||
|
equipment.subCategory || ''
|
||||||
].join(' ').toLowerCase();
|
].join(' ').toLowerCase();
|
||||||
return searchableText.includes(searchQuery);
|
|
||||||
});
|
if (searchableText.includes(searchQuery)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pour la limite finale après filtrage textuel
|
if (!compactSearchQuery) {
|
||||||
const limitedEquipments = equipments.slice(0, limit);
|
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;
|
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] Firestore returned ${rawDocCount} docs, hasMore=${hasMoreDocs}`);
|
||||||
logger.info(`[getEquipmentsPaginated] Returning ${limitedEquipments.length} equipments (filtered from ${equipments.length}), hasMore=${hasMoreDocs}`);
|
logger.info(`[getEquipmentsPaginated] Returning ${limitedEquipments.length} equipments, hasMore=${hasMoreDocs}`);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
equipments: limitedEquipments,
|
equipments: limitedEquipments,
|
||||||
@@ -3915,6 +4002,68 @@ exports.getEquipmentsPaginated = onRequest(httpOptions, withCors(async (req, res
|
|||||||
lastVisible,
|
lastVisible,
|
||||||
total: limitedEquipments.length
|
total: limitedEquipments.length
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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: hasMoreMatches,
|
||||||
|
lastVisible,
|
||||||
|
total: limitedEquipments.length
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error fetching paginated equipments:", error);
|
logger.error("Error fetching paginated equipments:", error);
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ exports.processEquipmentValidation = onCall({
|
|||||||
for (const equipment of equipmentList) {
|
for (const equipment of equipmentList) {
|
||||||
const {equipmentId, status, quantity, expectedQuantity} = equipment;
|
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
|
// Cas 1: Équipement PERDU
|
||||||
if (status === 'LOST') {
|
if (status === 'LOST') {
|
||||||
const alertData = await createAlertInFirestore({
|
const alertData = await createAlertInFirestore({
|
||||||
@@ -91,7 +96,9 @@ exports.processEquipmentValidation = onCall({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cas 3: Quantité incorrecte
|
// 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({
|
const alertData = await createAlertInFirestore({
|
||||||
type: 'QUANTITY_MISMATCH',
|
type: 'QUANTITY_MISMATCH',
|
||||||
severity: 'INFO',
|
severity: 'INFO',
|
||||||
@@ -409,10 +416,48 @@ async function sendAlertEmails(alert, userIds) {
|
|||||||
* Formate la date d'un événement
|
* Formate la date d'un événement
|
||||||
*/
|
*/
|
||||||
function formatEventDate(event) {
|
function formatEventDate(event) {
|
||||||
if (event.startDate) {
|
const rawDate =
|
||||||
const date = event.startDate.toDate ? event.startDate.toDate() : new Date(event.startDate);
|
event?.StartDateTime ||
|
||||||
return date.toLocaleDateString('fr-FR', {day: 'numeric', month: 'numeric', year: 'numeric'});
|
event?.startDateTime ||
|
||||||
}
|
event?.startDate ||
|
||||||
return 'Date inconnue';
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/// Configuration de la version de l'application
|
/// Configuration de la version de l'application
|
||||||
class AppVersion {
|
class AppVersion {
|
||||||
static const String version = '1.1.19';
|
static const String version = '1.1.20';
|
||||||
|
|
||||||
/// 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';
|
||||||
|
|||||||
@@ -4,6 +4,44 @@ import 'package:em2rp/services/api_service.dart';
|
|||||||
class EventPreparationService {
|
class EventPreparationService {
|
||||||
final ApiService _apiService = apiService;
|
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 ===
|
// === PRÉPARATION ===
|
||||||
|
|
||||||
/// Valider un équipement individuel en préparation
|
/// Valider un équipement individuel en préparation
|
||||||
|
|||||||
@@ -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/audio_feedback_service.dart';
|
||||||
import 'package:em2rp/services/smart_text_to_speech_service.dart';
|
import 'package:em2rp/services/smart_text_to_speech_service.dart';
|
||||||
import 'package:em2rp/services/equipment_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/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep;
|
||||||
import 'package:em2rp/views/widgets/equipment/container_checklist_item.dart';
|
import 'package:em2rp/views/widgets/equipment/container_checklist_item.dart';
|
||||||
import 'package:em2rp/views/widgets/common/qr_code_scanner_dialog.dart';
|
import 'package:em2rp/views/widgets/common/qr_code_scanner_dialog.dart';
|
||||||
@@ -1097,6 +1098,10 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
|
|
||||||
/// Détermine le statut d'un équipement selon l'étape actuelle
|
/// Détermine le statut d'un équipement selon l'étape actuelle
|
||||||
String _determineEquipmentStatus(EventEquipment eq) {
|
String _determineEquipmentStatus(EventEquipment eq) {
|
||||||
|
if (_isNotTakenToEventAtReturn(eq)) {
|
||||||
|
return 'NOT_TAKEN';
|
||||||
|
}
|
||||||
|
|
||||||
// Vérifier d'abord si l'équipement est perdu (LOST)
|
// Vérifier d'abord si l'équipement est perdu (LOST)
|
||||||
if (_shouldMarkAsLost(eq)) {
|
if (_shouldMarkAsLost(eq)) {
|
||||||
return 'LOST';
|
return 'LOST';
|
||||||
@@ -1118,14 +1123,31 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
|
|
||||||
/// Vérifie si un équipement doit être marqué comme LOST
|
/// Vérifie si un équipement doit être marqué comme LOST
|
||||||
bool _shouldMarkAsLost(EventEquipment eq) {
|
bool _shouldMarkAsLost(EventEquipment eq) {
|
||||||
// Seulement aux étapes de retour
|
return EventPreparationService.shouldMarkEquipmentAsLost(
|
||||||
if (_currentStep != PreparationStep.return_ &&
|
isReturnValidationStep: _isReturnValidationStep,
|
||||||
!(_currentStep == PreparationStep.unloadingReturn && _loadSimultaneously)) {
|
isMissingAtReturn: eq.isMissingAtReturn,
|
||||||
|
isLoaded: eq.isLoaded,
|
||||||
|
isMissingAtLoading: eq.isMissingAtLoading,
|
||||||
|
quantityAtLoading: eq.quantityAtLoading,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isNotTakenToEventAtReturn(EventEquipment eq) {
|
||||||
|
if (!_isReturnValidationStep) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si manquant maintenant mais PAS manquant à la préparation = LOST
|
return EventPreparationService.isEquipmentNotTakenToEvent(
|
||||||
return eq.isMissingAtReturn && !eq.isMissingAtPreparation;
|
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
|
/// Vérifie si un équipement est manquant à l'étape actuelle
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.1.19",
|
"version": "1.1.20",
|
||||||
"updateUrl": "https://app.em2events.fr",
|
"updateUrl": "https://app.em2events.fr",
|
||||||
"forceUpdate": true,
|
"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.",
|
"releaseNotes": "Patch bug envoi d'alerte equipement perdu, date dans les alertes, recherche par ID d'équipement.",
|
||||||
"timestamp": "2026-03-24T11:14:01.828Z"
|
"timestamp": "2026-03-30T15:04:34.073Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user