Compare commits
3 Commits
IA
...
0551f0b9c1
| Author | SHA1 | Date | |
|---|---|---|---|
| 0551f0b9c1 | |||
| cf13b4a986 | |||
| 3f80d9318b |
@@ -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
|
||||
|
||||
@@ -45,3 +45,5 @@ app.*.map.json
|
||||
# Environment configuration with credentials
|
||||
lib/config/env.dev.dart
|
||||
functions/.env
|
||||
.env
|
||||
env.dart
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
# Changelog - EM2RP
|
||||
|
||||
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.
|
||||
|
||||
## 12/03/2026bis
|
||||
Fix BUG : Ajout equipement à un evenement existant, boutons de modification de statut d'un evenement ne fonctionnaient pas. Refonte legere de la page calendrier.
|
||||
|
||||
|
||||
+187
-38
@@ -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
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/// Configuration de la version de l'application
|
||||
class AppVersion {
|
||||
static const String version = '1.1.18';
|
||||
static const String version = '1.1.20';
|
||||
|
||||
/// Retourne la version complète de l'application
|
||||
static String get fullVersion => 'v$version';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -100,7 +100,6 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
|
||||
// Nom
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
@@ -257,7 +256,8 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.scale),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
if (double.tryParse(value) == null) {
|
||||
@@ -279,7 +279,8 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
||||
labelText: 'Longueur (cm)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||
keyboardType:
|
||||
TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
if (double.tryParse(value) == null) {
|
||||
@@ -298,7 +299,8 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
||||
labelText: 'Largeur (cm)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||
keyboardType:
|
||||
TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
if (double.tryParse(value) == null) {
|
||||
@@ -317,7 +319,8 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
||||
labelText: 'Hauteur (cm)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.numberWithOptions(decimal: true),
|
||||
keyboardType:
|
||||
TextInputType.numberWithOptions(decimal: true),
|
||||
validator: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
if (double.tryParse(value) == null) {
|
||||
@@ -452,6 +455,11 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
||||
Future<void> _selectEquipment() async {
|
||||
final equipmentProvider = context.read<EquipmentProvider>();
|
||||
|
||||
// Toujours charger la liste complète pour éviter d'afficher uniquement
|
||||
// la page paginée active d'un autre écran.
|
||||
await equipmentProvider.loadEquipments();
|
||||
if (!mounted) return;
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => _EquipmentSelectorDialog(
|
||||
@@ -460,6 +468,7 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
||||
),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@@ -535,7 +544,8 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
||||
equipmentId: equipmentId,
|
||||
);
|
||||
} catch (e) {
|
||||
DebugLog.error('Erreur lors de l\'ajout de l\'équipement $equipmentId', e);
|
||||
DebugLog.error(
|
||||
'Erreur lors de l\'ajout de l\'équipement $equipmentId', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,7 +583,8 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
||||
});
|
||||
|
||||
// Gérer les équipements ajoutés
|
||||
final addedEquipment = _selectedEquipmentIds.difference(container.equipmentIds.toSet());
|
||||
final addedEquipment =
|
||||
_selectedEquipmentIds.difference(container.equipmentIds.toSet());
|
||||
for (final equipmentId in addedEquipment) {
|
||||
try {
|
||||
await provider.addEquipmentToContainer(
|
||||
@@ -581,12 +592,14 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
||||
equipmentId: equipmentId,
|
||||
);
|
||||
} catch (e) {
|
||||
DebugLog.error('Erreur lors de l\'ajout de l\'équipement $equipmentId', e);
|
||||
DebugLog.error(
|
||||
'Erreur lors de l\'ajout de l\'équipement $equipmentId', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Gérer les équipements retirés
|
||||
final removedEquipment = container.equipmentIds.toSet().difference(_selectedEquipmentIds);
|
||||
final removedEquipment =
|
||||
container.equipmentIds.toSet().difference(_selectedEquipmentIds);
|
||||
for (final equipmentId in removedEquipment) {
|
||||
try {
|
||||
await provider.removeEquipmentFromContainer(
|
||||
@@ -594,7 +607,8 @@ class _ContainerFormPageState extends State<ContainerFormPage> {
|
||||
equipmentId: equipmentId,
|
||||
);
|
||||
} catch (e) {
|
||||
DebugLog.error('Erreur lors du retrait de l\'équipement $equipmentId', e);
|
||||
DebugLog.error(
|
||||
'Erreur lors du retrait de l\'équipement $equipmentId', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,7 +644,8 @@ class _EquipmentSelectorDialog extends StatefulWidget {
|
||||
});
|
||||
|
||||
@override
|
||||
State<_EquipmentSelectorDialog> createState() => _EquipmentSelectorDialogState();
|
||||
State<_EquipmentSelectorDialog> createState() =>
|
||||
_EquipmentSelectorDialogState();
|
||||
}
|
||||
|
||||
class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
||||
@@ -638,12 +653,14 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
||||
EquipmentCategory? _filterCategory;
|
||||
String _searchQuery = '';
|
||||
late Set<String> _tempSelectedIds;
|
||||
late final Future<void> _loadingFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Créer une copie temporaire des IDs sélectionnés
|
||||
_tempSelectedIds = Set<String>.from(widget.selectedIds);
|
||||
_loadingFuture = widget.equipmentProvider.loadEquipments();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -729,7 +746,8 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
||||
},
|
||||
selectedColor: AppColors.rouge,
|
||||
labelStyle: TextStyle(
|
||||
color: _filterCategory == null ? Colors.white : Colors.black,
|
||||
color:
|
||||
_filterCategory == null ? Colors.white : Colors.black,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -746,7 +764,9 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
||||
},
|
||||
selectedColor: AppColors.rouge,
|
||||
labelStyle: TextStyle(
|
||||
color: _filterCategory == category ? Colors.white : Colors.black,
|
||||
color: _filterCategory == category
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -778,8 +798,8 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
||||
|
||||
// Liste des équipements
|
||||
Expanded(
|
||||
child: StreamBuilder<List<EquipmentModel>>(
|
||||
stream: widget.equipmentProvider.equipmentStream,
|
||||
child: FutureBuilder<void>(
|
||||
future: _loadingFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
@@ -789,11 +809,15 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
||||
return Center(child: Text('Erreur: ${snapshot.error}'));
|
||||
}
|
||||
|
||||
var equipment = snapshot.data ?? [];
|
||||
var equipment = List<EquipmentModel>.from(
|
||||
widget.equipmentProvider.allEquipment,
|
||||
);
|
||||
|
||||
// Filtrer par catégorie
|
||||
if (_filterCategory != null) {
|
||||
equipment = equipment.where((e) => e.category == _filterCategory).toList();
|
||||
equipment = equipment
|
||||
.where((e) => e.category == _filterCategory)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Filtrer par recherche
|
||||
@@ -801,8 +825,8 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
|
||||
final query = _searchQuery.toLowerCase();
|
||||
equipment = equipment.where((e) {
|
||||
return e.id.toLowerCase().contains(query) ||
|
||||
(e.brand?.toLowerCase().contains(query) ?? false) ||
|
||||
(e.model?.toLowerCase().contains(query) ?? false);
|
||||
(e.brand?.toLowerCase().contains(query) ?? false) ||
|
||||
(e.model?.toLowerCase().contains(query) ?? false);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<EventPreparationPage> 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<EventPreparationPage> 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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
bool shouldAutoLoadNextPage({
|
||||
required bool hasMoreData,
|
||||
required bool isLoadingMore,
|
||||
required bool hasClients,
|
||||
required double maxScrollExtent,
|
||||
}) {
|
||||
if (!hasMoreData || isLoadingMore) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the list cannot scroll yet, preload the next page to avoid a truncated view.
|
||||
return !hasClients || maxScrollExtent <= 0;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import 'package:em2rp/views/widgets/event/equipment_selection_pagination.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('shouldAutoLoadNextPage', () {
|
||||
test('returns false when there is no more data', () {
|
||||
final result = shouldAutoLoadNextPage(
|
||||
hasMoreData: false,
|
||||
isLoadingMore: false,
|
||||
hasClients: true,
|
||||
maxScrollExtent: 100,
|
||||
);
|
||||
|
||||
expect(result, isFalse);
|
||||
});
|
||||
|
||||
test('returns false while a page is already loading', () {
|
||||
final result = shouldAutoLoadNextPage(
|
||||
hasMoreData: true,
|
||||
isLoadingMore: true,
|
||||
hasClients: true,
|
||||
maxScrollExtent: 0,
|
||||
);
|
||||
|
||||
expect(result, isFalse);
|
||||
});
|
||||
|
||||
test('returns true when list has no scroll client yet', () {
|
||||
final result = shouldAutoLoadNextPage(
|
||||
hasMoreData: true,
|
||||
isLoadingMore: false,
|
||||
hasClients: false,
|
||||
maxScrollExtent: 0,
|
||||
);
|
||||
|
||||
expect(result, isTrue);
|
||||
});
|
||||
|
||||
test('returns true when list is not scrollable yet', () {
|
||||
final result = shouldAutoLoadNextPage(
|
||||
hasMoreData: true,
|
||||
isLoadingMore: false,
|
||||
hasClients: true,
|
||||
maxScrollExtent: 0,
|
||||
);
|
||||
|
||||
expect(result, isTrue);
|
||||
});
|
||||
|
||||
test('returns false when list is scrollable', () {
|
||||
final result = shouldAutoLoadNextPage(
|
||||
hasMoreData: true,
|
||||
isLoadingMore: false,
|
||||
hasClients: true,
|
||||
maxScrollExtent: 250,
|
||||
);
|
||||
|
||||
expect(result, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "1.1.18",
|
||||
"version": "1.1.20",
|
||||
"updateUrl": "https://app.em2events.fr",
|
||||
"forceUpdate": true,
|
||||
"releaseNotes": "Fix BUG : Ajout equipement à un evenement existant, boutons de modification de statut d'un evenement ne fonctionnaient pas. Refonte legere de la page calendrier.",
|
||||
"timestamp": "2026-03-12T20:11:54.548Z"
|
||||
"releaseNotes": "Patch bug envoi d'alerte equipement perdu, date dans les alertes, recherche par ID d'équipement.",
|
||||
"timestamp": "2026-03-30T15:04:34.073Z"
|
||||
}
|
||||
Reference in New Issue
Block a user