feat: Scan et traitement intelligent des QR Codes en préparation d'événement
Cette mise à jour majeure introduit une fonctionnalité de scan et de saisie manuelle de codes QR directement depuis la page de préparation d'un événement. Ce système accélère et fiabilise le processus de validation des équipements et des containers pour chaque étape (préparation, chargement, etc.), tout en ajoutant des retours sonores, haptiques et visuels pour une expérience utilisateur améliorée.
**Fonctionnalités et améliorations principales :**
- **Scan et saisie manuelle en préparation d'événement :**
- Ajout d'un champ de "Saisie manuelle" et d'un bouton "Scanner QR Code" sur la page de préparation (`EventPreparationPage`).
- Le scanner peut fonctionner en mode "multi-scan", permettant de valider plusieurs éléments à la suite sans fermer la caméra.
- Le système gère à la fois les équipements individuels et les containers (qui valident automatiquement tout leur contenu).
- **Logique de traitement intelligente (`QRCodeProcessingService`) :**
- Un nouveau service centralise la logique de traitement des codes.
- Pour les équipements quantitatifs, chaque scan incrémente la quantité jusqu'à atteindre la cible requise pour l'étape en cours.
- Pour les équipements non quantitatifs, le premier scan valide l'élément.
- Les scans multiples d'un élément déjà validé ou dont la quantité est atteinte génèrent une erreur.
- **Ajout dynamique d'équipements :**
- Si un code scanné n'est pas assigné à l'événement, une boîte de dialogue propose de rechercher l'équipement ou le container dans la base de données et de l'ajouter à l'événement en cours.
- **Feedbacks utilisateur :**
- Création d'un `AudioFeedbackService` pour fournir des retours sonores (succès/erreur) et haptiques (vibration) lors de chaque scan.
- Des `Snackbars` claires (vertes pour succès, orange pour erreur) informent l'utilisateur du résultat de chaque action.
- **Optimisation du chargement des données :**
- Nouvel endpoint backend `getEventWithDetails` qui charge un événement et toutes ses dépendances (équipements, containers et leurs enfants) en un seul appel, optimisant drastiquement les temps de chargement des pages de préparation et de modification d'événement.
- Le frontend (`EventPreparationPage`, `EventAssignedEquipmentSection`) utilise ce nouvel endpoint, éliminant les chargements multiples et fiabilisant l'affichage des données.
**Refactorisation et corrections :**
- **Structure du code :**
- La logique de traitement des codes est extraite dans le `QRCodeProcessingService`.
- Création de widgets dédiés (`CodeNotFoundDialog`, `AddEquipmentToEventDialog`) pour gérer les nouveaux flux utilisateurs.
- **Fiabilisation de l'état :**
- Mise à jour optimiste de l'UI lors du changement de statut d'un événement (`EventStatusButton`) pour une meilleure réactivité.
- Correction d'un bug dans la suppression d'un container d'un événement, qui pouvait retirer des équipements partagés avec d'autres containers.
- Correction d'un bug lors de l'ajout d'un container à un événement, qui n'ajoutait pas automatiquement ses équipements enfants.
- **Optimisations des performances UI :**
- Amélioration de la fluidité du défilement infini sur la page de gestion des équipements grâce à `RepaintBoundary` et à une gestion optimisée du chargement.
**Déploiement et version :**
- **Scripts de déploiement :** Ajout d'un script PowerShell (`deploy_hosting.ps1`) et amélioration du script Node.js pour automatiser et fiabiliser les déploiements sur Firebase Hosting.
- **Configuration CORS :** Les en-têtes CORS sont désormais configurés pour `version.json`, assurant le bon fonctionnement du mécanisme de mise à jour de l'application.
- **Version de l'application :** Incrémentée à `1.0.6`.
This commit is contained in:
@@ -32,16 +32,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,1768586208886,5a25871ae727f23c4b7258c34108085b8711aa94f6fcab512e0c3ca00a429a64
|
version.json,1768738172901,f258e76dbf34b4a64999cb6d1d983255ad592c590e53f7c4fe380b2bfef82762
|
||||||
index.html,1768586225248,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
index.html,1768738180374,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
||||||
flutter_service_worker.js,1768586307073,4ea31c373e15f13c2916a12d9d799905af2a79ff7ed0bcceb4334707910c7721
|
flutter_service_worker.js,1768738281912,ad5fcbc95e3f4e31b6c3ae92df0a872c24434ba7ac7448fdd9359f2e3bf7d76c
|
||||||
flutter_bootstrap.js,1768586225225,e95b1b0bd493a475c8eed0e630e413d898f2ceff11cd9b24c6c564bbc2c5f5e9
|
assets/FontManifest.json,1768738277185,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
||||||
assets/FontManifest.json,1768586302952,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
flutter_bootstrap.js,1768738180360,f1963883a54097e939404b503b6a9963408fe0187a18d73adb648f6ef0f81578
|
||||||
assets/AssetManifest.json,1768586302952,1e1501af5844823ef215cf650f4cef4002c0389d88770225ac07576d57dc1067
|
assets/AssetManifest.bin.json,1768738277185,f446eb3de964f3a6f9e76fcc98d79a81b0429e076c9c7bf30cf8edd0263a0b0a
|
||||||
assets/AssetManifest.bin.json,1768586302952,f446eb3de964f3a6f9e76fcc98d79a81b0429e076c9c7bf30cf8edd0263a0b0a
|
assets/AssetManifest.bin,1768738277184,72bbccb69d9a02d3885df0c5e58ebfed29e25a4919e10bf195b59542f4709ca3
|
||||||
assets/AssetManifest.bin,1768586302952,72bbccb69d9a02d3885df0c5e58ebfed29e25a4919e10bf195b59542f4709ca3
|
assets/AssetManifest.json,1768738277185,1e1501af5844823ef215cf650f4cef4002c0389d88770225ac07576d57dc1067
|
||||||
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1768586306083,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
assets/shaders/ink_sparkle.frag,1768738277454,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
||||||
assets/shaders/ink_sparkle.frag,1768586303187,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1768738280959,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
||||||
assets/fonts/MaterialIcons-Regular.otf,1768586306096,33efc485968dd28630ace587c22d6df359c195821b1114aaa85383e4d5394eac
|
assets/fonts/MaterialIcons-Regular.otf,1768738280969,9e7c35e587de73a0aee5675d5aef4c6830478af0aa31ad0da76b84a503906b03
|
||||||
assets/NOTICES,1768586302954,fc20c3c3c998057eb7e58ad2e009c7268bf748bfde685e95130431f4c54bd51c
|
assets/NOTICES,1768738277188,fc20c3c3c998057eb7e58ad2e009c7268bf748bfde685e95130431f4c54bd51c
|
||||||
main.dart.js,1768586301774,9b399ba21ab3247d46cf7dbcd5873aa248636bcd7864a1a0cedf1aae08608f9a
|
main.dart.js,1768738275891,4ef7f90056f38602de6430a68a479a005268f9d83395ad9b444337c214a3710c
|
||||||
|
|||||||
103
em2rp/deploy_hosting.ps1
Normal file
103
em2rp/deploy_hosting.ps1
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# Script de déploiement du hosting Firebase
|
||||||
|
# Ce script construit l'application et la déploie sur Firebase Hosting
|
||||||
|
|
||||||
|
Write-Host "=== Déploiement Firebase Hosting ===" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# 1. Vérifier que nous sommes dans le bon dossier
|
||||||
|
if (!(Test-Path "pubspec.yaml")) {
|
||||||
|
Write-Host "ERREUR: Ce script doit être exécuté depuis la racine du projet Flutter" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Construire l'application Flutter pour le web
|
||||||
|
Write-Host "Étape 1/3: Construction de l'application Flutter pour le web..." -ForegroundColor Yellow
|
||||||
|
flutter build web
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "ERREUR: La construction de l'application a échoué" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "✓ Application construite avec succès" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# 3. Vérifier que version.json existe
|
||||||
|
if (!(Test-Path "build/web/version.json")) {
|
||||||
|
Write-Host "AVERTISSEMENT: version.json n'a pas été copié dans build/web/" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Copier manuellement si nécessaire
|
||||||
|
if (Test-Path "web/version.json") {
|
||||||
|
Write-Host " → Copie de web/version.json vers build/web/..." -ForegroundColor Yellow
|
||||||
|
Copy-Item "web/version.json" "build/web/version.json"
|
||||||
|
Write-Host "✓ Fichier copié" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "ERREUR: web/version.json n'existe pas" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# 4. Afficher la version qui va être déployée
|
||||||
|
$versionContent = Get-Content "build/web/version.json" | ConvertFrom-Json
|
||||||
|
Write-Host "Version à déployer: $($versionContent.version)" -ForegroundColor Cyan
|
||||||
|
Write-Host "Force update: $($versionContent.forceUpdate)" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# 5. Demander confirmation
|
||||||
|
$confirm = Read-Host "Voulez-vous déployer sur Firebase Hosting ? (o/n)"
|
||||||
|
if ($confirm -ne "o" -and $confirm -ne "O") {
|
||||||
|
Write-Host "Déploiement annulé" -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# 6. Déployer sur Firebase Hosting
|
||||||
|
Write-Host "Étape 2/3: Déploiement sur Firebase Hosting..." -ForegroundColor Yellow
|
||||||
|
firebase deploy --only hosting
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "ERREUR: Le déploiement a échoué" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "✓ Déploiement réussi" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# 7. Vérifier que version.json est accessible
|
||||||
|
Write-Host "Étape 3/3: Vérification de l'accès à version.json..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Invoke-WebRequest -Uri "https://app.em2events.fr/version.json" -Method GET -UseBasicParsing
|
||||||
|
|
||||||
|
if ($response.StatusCode -eq 200) {
|
||||||
|
Write-Host "✓ version.json est accessible" -ForegroundColor Green
|
||||||
|
|
||||||
|
# Vérifier les en-têtes CORS
|
||||||
|
if ($response.Headers["Access-Control-Allow-Origin"]) {
|
||||||
|
Write-Host "✓ En-têtes CORS configurés correctement" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠ ATTENTION: En-têtes CORS non détectés" -ForegroundColor Yellow
|
||||||
|
Write-Host " Les en-têtes peuvent prendre quelques minutes pour se propager" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Afficher la version déployée
|
||||||
|
$deployedVersion = ($response.Content | ConvertFrom-Json).version
|
||||||
|
Write-Host "Version déployée: $deployedVersion" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠ Code de statut: $($response.StatusCode)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "⚠ Impossible de vérifier l'accès à version.json" -ForegroundColor Yellow
|
||||||
|
Write-Host " Erreur: $($_.Exception.Message)" -ForegroundColor Yellow
|
||||||
|
Write-Host " Le fichier peut prendre quelques minutes pour être accessible" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== Déploiement terminé ===" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Les utilisateurs recevront une notification de mise à jour au prochain chargement de l'application." -ForegroundColor Green
|
||||||
|
Write-Host "URL de l'application: https://app.em2events.fr" -ForegroundColor Cyan
|
||||||
@@ -42,6 +42,25 @@
|
|||||||
"**/.*",
|
"**/.*",
|
||||||
"**/node_modules/**"
|
"**/node_modules/**"
|
||||||
],
|
],
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"source": "version.json",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Access-Control-Allow-Origin",
|
||||||
|
"value": "*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Access-Control-Allow-Methods",
|
||||||
|
"value": "GET, OPTIONS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "Cache-Control",
|
||||||
|
"value": "no-cache, no-store, must-revalidate"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"rewrites": [
|
"rewrites": [
|
||||||
{
|
{
|
||||||
"source": "**",
|
"source": "**",
|
||||||
|
|||||||
@@ -1725,6 +1725,160 @@ exports.getEvents = onRequest(httpOptions, withCors(async (req, res) => {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère un événement avec tous les détails (équipements complets + containers avec enfants)
|
||||||
|
* Optimisé pour la page de préparation et l'affichage détaillé
|
||||||
|
*/
|
||||||
|
exports.getEventWithDetails = onRequest(httpOptions, withCors(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const decodedToken = await auth.authenticateUser(req);
|
||||||
|
const { eventId } = req.body.data || {};
|
||||||
|
|
||||||
|
if (!eventId) {
|
||||||
|
res.status(400).json({ error: 'eventId is required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer l'événement
|
||||||
|
const eventDoc = await db.collection('events').doc(eventId).get();
|
||||||
|
|
||||||
|
if (!eventDoc.exists) {
|
||||||
|
res.status(404).json({ error: 'Event not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventData = eventDoc.data();
|
||||||
|
|
||||||
|
// Vérifier les permissions
|
||||||
|
const canViewAll = await auth.hasPermission(decodedToken.uid, 'view_all_events');
|
||||||
|
if (!canViewAll) {
|
||||||
|
// Vérifier si l'utilisateur est dans la workforce
|
||||||
|
const userRef = db.collection('users').doc(decodedToken.uid);
|
||||||
|
const isInWorkforce = eventData.workforce && eventData.workforce.some(ref =>
|
||||||
|
(ref.id && ref.id === decodedToken.uid) ||
|
||||||
|
(typeof ref === 'string' && ref === `users/${decodedToken.uid}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isInWorkforce) {
|
||||||
|
res.status(403).json({ error: 'Forbidden: Not assigned to this event' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[getEventWithDetails] Loading details for event ${eventId}`);
|
||||||
|
|
||||||
|
// Collecter tous les IDs d'équipements et de containers
|
||||||
|
const equipmentIds = new Set();
|
||||||
|
const containerIds = new Set();
|
||||||
|
|
||||||
|
if (eventData.assignedEquipment && Array.isArray(eventData.assignedEquipment)) {
|
||||||
|
eventData.assignedEquipment.forEach(eq => {
|
||||||
|
if (eq.equipmentId) {
|
||||||
|
equipmentIds.add(eq.equipmentId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventData.assignedContainers && Array.isArray(eventData.assignedContainers)) {
|
||||||
|
eventData.assignedContainers.forEach(id => containerIds.add(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[getEventWithDetails] Loading ${equipmentIds.size} equipments and ${containerIds.size} containers`);
|
||||||
|
|
||||||
|
// Charger tous les équipements en parallèle
|
||||||
|
const equipmentPromises = Array.from(equipmentIds).map(id =>
|
||||||
|
db.collection('equipments').doc(id).get()
|
||||||
|
);
|
||||||
|
const equipmentDocs = await Promise.all(equipmentPromises);
|
||||||
|
|
||||||
|
const equipmentMap = {};
|
||||||
|
for (const doc of equipmentDocs) {
|
||||||
|
if (doc.exists) {
|
||||||
|
let data = { id: doc.id, ...doc.data() };
|
||||||
|
data = helpers.serializeTimestamps(data);
|
||||||
|
data = helpers.serializeReferences(data);
|
||||||
|
equipmentMap[doc.id] = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charger tous les containers en parallèle
|
||||||
|
const containerPromises = Array.from(containerIds).map(id =>
|
||||||
|
db.collection('containers').doc(id).get()
|
||||||
|
);
|
||||||
|
const containerDocs = await Promise.all(containerPromises);
|
||||||
|
|
||||||
|
// Collecter les IDs des équipements enfants des containers
|
||||||
|
const childEquipmentIds = new Set();
|
||||||
|
for (const doc of containerDocs) {
|
||||||
|
if (doc.exists) {
|
||||||
|
const containerData = doc.data();
|
||||||
|
if (containerData.equipmentIds && Array.isArray(containerData.equipmentIds)) {
|
||||||
|
containerData.equipmentIds.forEach(id => childEquipmentIds.add(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[getEventWithDetails] Loading ${childEquipmentIds.size} child equipments from containers`);
|
||||||
|
|
||||||
|
// Charger les équipements enfants des containers
|
||||||
|
const childEquipmentPromises = Array.from(childEquipmentIds).map(id =>
|
||||||
|
db.collection('equipments').doc(id).get()
|
||||||
|
);
|
||||||
|
const childEquipmentDocs = await Promise.all(childEquipmentPromises);
|
||||||
|
|
||||||
|
// Ajouter les enfants au map d'équipements
|
||||||
|
for (const doc of childEquipmentDocs) {
|
||||||
|
if (doc.exists && !equipmentMap[doc.id]) {
|
||||||
|
let data = { id: doc.id, ...doc.data() };
|
||||||
|
data = helpers.serializeTimestamps(data);
|
||||||
|
data = helpers.serializeReferences(data);
|
||||||
|
equipmentMap[doc.id] = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construire les containers avec leurs enfants complets
|
||||||
|
const containerMap = {};
|
||||||
|
for (const doc of containerDocs) {
|
||||||
|
if (doc.exists) {
|
||||||
|
let containerData = { id: doc.id, ...doc.data() };
|
||||||
|
containerData = helpers.serializeTimestamps(containerData);
|
||||||
|
containerData = helpers.serializeReferences(containerData);
|
||||||
|
|
||||||
|
// Ajouter les équipements enfants complets
|
||||||
|
if (containerData.equipmentIds && Array.isArray(containerData.equipmentIds)) {
|
||||||
|
containerData.children = containerData.equipmentIds
|
||||||
|
.map(id => equipmentMap[id])
|
||||||
|
.filter(eq => eq !== undefined);
|
||||||
|
} else {
|
||||||
|
containerData.children = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
containerMap[doc.id] = containerData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construire la réponse finale
|
||||||
|
const event = {
|
||||||
|
id: eventDoc.id,
|
||||||
|
...helpers.serializeTimestamps(eventData),
|
||||||
|
workforce: eventData.workforce ? eventData.workforce.map(ref =>
|
||||||
|
(ref.id || (typeof ref === 'string' ? ref.split('/')[1] : null))
|
||||||
|
).filter(uid => uid !== null) : [],
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(`[getEventWithDetails] Returning event with ${Object.keys(equipmentMap).length} equipments and ${Object.keys(containerMap).length} containers`);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
event,
|
||||||
|
equipments: equipmentMap,
|
||||||
|
containers: containerMap,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error getting event with details:", error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// MAINTENANCES - Read with permissions
|
// MAINTENANCES - Read with permissions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const EMAIL_CONFIG = {
|
|||||||
},
|
},
|
||||||
replyTo: 'contact@em2events.fr',
|
replyTo: 'contact@em2events.fr',
|
||||||
// URL de l'application pour les liens
|
// URL de l'application pour les liens
|
||||||
appUrl: process.env.APP_URL || 'https://em2rp-951dc.web.app',
|
appUrl: process.env.APP_URL || 'https://app.em2events.fr',
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -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.0.4';
|
static const String version = '1.0.6';
|
||||||
|
|
||||||
/// 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';
|
||||||
|
|||||||
@@ -91,7 +91,20 @@ class EventFormController extends ChangeNotifier {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
if (existingEvent != null) {
|
if (existingEvent != null) {
|
||||||
_populateFromEvent(existingEvent);
|
// 🔧 FIX: Recharger l'événement avec tous les détails (équipements + containers avec enfants)
|
||||||
|
try {
|
||||||
|
final dataService = DataService(FirebaseFunctionsApiService());
|
||||||
|
final result = await dataService.getEventWithDetails(existingEvent.id);
|
||||||
|
final eventData = result['event'] as Map<String, dynamic>;
|
||||||
|
|
||||||
|
// Reconstruire l'événement avec les données complètes
|
||||||
|
final completeEvent = EventModel.fromMap(eventData, eventData['id'] as String);
|
||||||
|
_populateFromEvent(completeEvent);
|
||||||
|
} catch (e) {
|
||||||
|
// Si erreur, utiliser l'événement existant (fallback)
|
||||||
|
print('[EventFormController] Error loading event with details, using existing: $e');
|
||||||
|
_populateFromEvent(existingEvent);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
_selectedStatus = EventStatus.waitingForApproval;
|
_selectedStatus = EventStatus.waitingForApproval;
|
||||||
|
|
||||||
|
|||||||
63
em2rp/lib/models/qr_code_process_result.dart
Normal file
63
em2rp/lib/models/qr_code_process_result.dart
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/// Résultat du traitement d'un code QR ou saisi manuellement
|
||||||
|
class QRCodeProcessResult {
|
||||||
|
/// Indique si le traitement a réussi
|
||||||
|
final bool success;
|
||||||
|
|
||||||
|
/// Message descriptif du résultat
|
||||||
|
final String? message;
|
||||||
|
|
||||||
|
/// Liste des IDs d'équipements affectés par le traitement
|
||||||
|
final List<String> affectedEquipmentIds;
|
||||||
|
|
||||||
|
/// Mises à jour des états de validation (équipements cochés)
|
||||||
|
final Map<String, bool>? updatedValidationState;
|
||||||
|
|
||||||
|
/// Mises à jour des quantités actuelles
|
||||||
|
final Map<String, int>? updatedQuantities;
|
||||||
|
|
||||||
|
/// Indique si le code n'a pas été trouvé dans l'événement actuel
|
||||||
|
/// (utilisé pour proposer de l'ajouter depuis la BDD)
|
||||||
|
final bool codeNotFoundInEvent;
|
||||||
|
|
||||||
|
const QRCodeProcessResult({
|
||||||
|
required this.success,
|
||||||
|
this.message,
|
||||||
|
this.affectedEquipmentIds = const [],
|
||||||
|
this.updatedValidationState,
|
||||||
|
this.updatedQuantities,
|
||||||
|
this.codeNotFoundInEvent = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Crée un résultat de succès
|
||||||
|
factory QRCodeProcessResult.success({
|
||||||
|
required String message,
|
||||||
|
required List<String> affectedEquipmentIds,
|
||||||
|
Map<String, bool>? updatedValidationState,
|
||||||
|
Map<String, int>? updatedQuantities,
|
||||||
|
}) {
|
||||||
|
return QRCodeProcessResult(
|
||||||
|
success: true,
|
||||||
|
message: message,
|
||||||
|
affectedEquipmentIds: affectedEquipmentIds,
|
||||||
|
updatedValidationState: updatedValidationState,
|
||||||
|
updatedQuantities: updatedQuantities,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crée un résultat d'erreur
|
||||||
|
factory QRCodeProcessResult.error(String message) {
|
||||||
|
return QRCodeProcessResult(
|
||||||
|
success: false,
|
||||||
|
message: message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crée un résultat indiquant que le code n'est pas dans l'événement
|
||||||
|
factory QRCodeProcessResult.notFoundInEvent(String code) {
|
||||||
|
return QRCodeProcessResult(
|
||||||
|
success: false,
|
||||||
|
message: 'Code $code non trouvé dans cet événement',
|
||||||
|
codeNotFoundInEvent: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
em2rp/lib/services/audio_feedback_service.dart
Normal file
46
em2rp/lib/services/audio_feedback_service.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
|
|
||||||
|
/// Service pour émettre des feedbacks sonores lors des interactions
|
||||||
|
class AudioFeedbackService {
|
||||||
|
/// Jouer un son de succès (clic système)
|
||||||
|
static Future<void> playSuccessBeep() async {
|
||||||
|
try {
|
||||||
|
await SystemSound.play(SystemSoundType.click);
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[AudioFeedbackService] Error playing success beep', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Jouer un son d'erreur (alerte système)
|
||||||
|
static Future<void> playErrorBeep() async {
|
||||||
|
try {
|
||||||
|
// Note: SystemSoundType.alert n'existe pas sur toutes les plateformes
|
||||||
|
// On utilise click pour l'instant, peut être amélioré avec audioplayers
|
||||||
|
await SystemSound.play(SystemSoundType.click);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
await SystemSound.play(SystemSoundType.click);
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[AudioFeedbackService] Error playing error beep', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Jouer une vibration haptique (si disponible)
|
||||||
|
static Future<void> playHapticFeedback() async {
|
||||||
|
try {
|
||||||
|
await HapticFeedback.mediumImpact();
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[AudioFeedbackService] Error playing haptic feedback', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Jouer un feedback complet (son + vibration)
|
||||||
|
static Future<void> playFullFeedback({bool isSuccess = true}) async {
|
||||||
|
await playHapticFeedback();
|
||||||
|
if (isSuccess) {
|
||||||
|
await playSuccessBeep();
|
||||||
|
} else {
|
||||||
|
await playErrorBeep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,7 +88,8 @@ class DataService {
|
|||||||
/// Met à jour un événement
|
/// Met à jour un événement
|
||||||
Future<void> updateEvent(String eventId, Map<String, dynamic> data) async {
|
Future<void> updateEvent(String eventId, Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
final requestData = {'eventId': eventId, 'data': data};
|
// Correction : fusionner eventId et les champs de data à la racine
|
||||||
|
final requestData = {'eventId': eventId, ...data};
|
||||||
await _apiService.call('updateEvent', requestData);
|
await _apiService.call('updateEvent', requestData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Erreur lors de la mise à jour de l\'événement: $e');
|
throw Exception('Erreur lors de la mise à jour de l\'événement: $e');
|
||||||
@@ -248,6 +249,35 @@ class DataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Récupère un événement avec tous les détails (équipements complets + containers avec enfants)
|
||||||
|
Future<Map<String, dynamic>> getEventWithDetails(String eventId) async {
|
||||||
|
try {
|
||||||
|
print('[DataService] Getting event with details: $eventId');
|
||||||
|
final result = await _apiService.call('getEventWithDetails', {
|
||||||
|
'eventId': eventId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final event = result['event'] as Map<String, dynamic>?;
|
||||||
|
final equipments = result['equipments'] as Map<String, dynamic>? ?? {};
|
||||||
|
final containers = result['containers'] as Map<String, dynamic>? ?? {};
|
||||||
|
|
||||||
|
if (event == null) {
|
||||||
|
throw Exception('Event not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
print('[DataService] Event loaded with ${equipments.length} equipments and ${containers.length} containers');
|
||||||
|
|
||||||
|
return {
|
||||||
|
'event': event,
|
||||||
|
'equipments': equipments,
|
||||||
|
'containers': containers,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
print('[DataService] Error getting event with details: $e');
|
||||||
|
throw Exception('Erreur lors de la récupération de l\'événement avec détails: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Récupère tous les équipements (avec masquage des prix selon permissions)
|
/// Récupère tous les équipements (avec masquage des prix selon permissions)
|
||||||
Future<List<Map<String, dynamic>>> getEquipments() async {
|
Future<List<Map<String, dynamic>>> getEquipments() async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:cloud_functions/cloud_functions.dart';
|
import 'package:cloud_functions/cloud_functions.dart';
|
||||||
import 'package:em2rp/models/alert_model.dart';
|
import 'package:em2rp/models/alert_model.dart';
|
||||||
import 'package:em2rp/models/user_model.dart';
|
|
||||||
import 'package:em2rp/utils/debug_log.dart';
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
import 'package:em2rp/services/equipment_status_calculator.dart';
|
|
||||||
import 'package:em2rp/services/api_service.dart';
|
|
||||||
|
|
||||||
/// Service étendu pour gérer les 4 étapes : Préparation, Chargement, Déchargement, Retour
|
|
||||||
class EventPreparationServiceExtended {
|
|
||||||
final ApiService _apiService = apiService;
|
|
||||||
|
|
||||||
|
|
||||||
// === CHARGEMENT (LOADING) ===
|
|
||||||
|
|
||||||
/// Valider un équipement individuel pour le chargement
|
|
||||||
Future<void> validateEquipmentLoading(String eventId, String equipmentId) async {
|
|
||||||
try {
|
|
||||||
await _apiService.call('validateEquipmentLoading', {
|
|
||||||
'eventId': eventId,
|
|
||||||
'equipmentId': equipmentId,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
print('Error validating equipment loading: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Valider tous les équipements pour le chargement
|
|
||||||
Future<void> validateAllLoading(String eventId) async {
|
|
||||||
try {
|
|
||||||
await _apiService.call('validateAllLoading', {
|
|
||||||
'eventId': eventId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Invalider le cache des statuts d'équipement
|
|
||||||
EquipmentStatusCalculator.invalidateGlobalCache();
|
|
||||||
} catch (e) {
|
|
||||||
print('Error validating all loading: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === DÉCHARGEMENT (UNLOADING) ===
|
|
||||||
|
|
||||||
/// Valider un équipement individuel pour le déchargement
|
|
||||||
Future<void> validateEquipmentUnloading(String eventId, String equipmentId) async {
|
|
||||||
try {
|
|
||||||
await _apiService.call('validateEquipmentUnloading', {
|
|
||||||
'eventId': eventId,
|
|
||||||
'equipmentId': equipmentId,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
print('Error validating equipment unloading: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Valider tous les équipements pour le déchargement
|
|
||||||
Future<void> validateAllUnloading(String eventId) async {
|
|
||||||
try {
|
|
||||||
await _apiService.call('validateAllUnloading', {
|
|
||||||
'eventId': eventId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Invalider le cache des statuts d'équipement
|
|
||||||
EquipmentStatusCalculator.invalidateGlobalCache();
|
|
||||||
} catch (e) {
|
|
||||||
print('Error validating all unloading: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === PRÉPARATION + CHARGEMENT ===
|
|
||||||
|
|
||||||
/// Valider préparation ET chargement en même temps
|
|
||||||
Future<void> validateAllPreparationAndLoading(String eventId) async {
|
|
||||||
try {
|
|
||||||
// Note: On pourrait créer une fonction cloud dédiée pour ça,
|
|
||||||
// mais pour l'instant on appelle les deux séquentiellement
|
|
||||||
await _apiService.call('validateAllPreparation', {'eventId': eventId});
|
|
||||||
await _apiService.call('validateAllLoading', {'eventId': eventId});
|
|
||||||
|
|
||||||
// Invalider le cache
|
|
||||||
EquipmentStatusCalculator.invalidateGlobalCache();
|
|
||||||
} catch (e) {
|
|
||||||
print('Error validating all preparation and loading: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === DÉCHARGEMENT + RETOUR ===
|
|
||||||
|
|
||||||
/// Valider déchargement ET retour en même temps
|
|
||||||
Future<void> validateAllUnloadingAndReturn(
|
|
||||||
String eventId,
|
|
||||||
Map<String, int>? returnedQuantities,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
// Note: On pourrait créer une fonction cloud dédiée pour ça,
|
|
||||||
// mais pour l'instant on appelle les deux séquentiellement
|
|
||||||
await _apiService.call('validateAllUnloading', {'eventId': eventId});
|
|
||||||
await _apiService.call('validateAllReturn', {
|
|
||||||
'eventId': eventId,
|
|
||||||
if (returnedQuantities != null) 'returnedQuantities': returnedQuantities,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Invalider le cache
|
|
||||||
EquipmentStatusCalculator.invalidateGlobalCache();
|
|
||||||
} catch (e) {
|
|
||||||
print('Error validating all unloading and return: $e');
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:em2rp/config/app_version.dart';
|
import 'package:em2rp/config/app_version.dart';
|
||||||
import 'package:em2rp/models/event_model.dart';
|
import 'package:em2rp/models/event_model.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:em2rp/utils/debug_log.dart';
|
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
class IcsExportService {
|
class IcsExportService {
|
||||||
|
|||||||
240
em2rp/lib/services/qr_code_processing_service.dart
Normal file
240
em2rp/lib/services/qr_code_processing_service.dart
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import 'package:em2rp/models/event_model.dart';
|
||||||
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
|
import 'package:em2rp/models/container_model.dart';
|
||||||
|
import 'package:em2rp/models/qr_code_process_result.dart';
|
||||||
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
|
|
||||||
|
/// Service pour traiter les codes QR scannés ou saisis manuellement
|
||||||
|
/// pendant la préparation d'un événement
|
||||||
|
class QRCodeProcessingService {
|
||||||
|
/// Traiter un code (équipement ou container)
|
||||||
|
Future<QRCodeProcessResult> processCode({
|
||||||
|
required String code,
|
||||||
|
required EventModel event,
|
||||||
|
required dynamic step, // Changed to dynamic to accept any PreparationStep enum
|
||||||
|
required Map<String, EquipmentModel> equipmentCache,
|
||||||
|
required Map<String, ContainerModel> containerCache,
|
||||||
|
required Map<String, bool> validationState,
|
||||||
|
required Map<String, int> currentQuantities,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
DebugLog.info('[QRCodeProcessingService] Processing code: $code');
|
||||||
|
|
||||||
|
// Identifier le type selon le préfixe
|
||||||
|
final isContainer = code.startsWith('BOX_');
|
||||||
|
|
||||||
|
if (isContainer) {
|
||||||
|
return await _processContainer(
|
||||||
|
code: code,
|
||||||
|
event: event,
|
||||||
|
step: step,
|
||||||
|
equipmentCache: equipmentCache,
|
||||||
|
containerCache: containerCache,
|
||||||
|
validationState: validationState,
|
||||||
|
currentQuantities: currentQuantities,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return await _processEquipment(
|
||||||
|
code: code,
|
||||||
|
event: event,
|
||||||
|
step: step,
|
||||||
|
equipmentCache: equipmentCache,
|
||||||
|
validationState: validationState,
|
||||||
|
currentQuantities: currentQuantities,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[QRCodeProcessingService] Error processing code', e);
|
||||||
|
return QRCodeProcessResult.error('Erreur lors du traitement du code: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Traiter un code d'équipement
|
||||||
|
Future<QRCodeProcessResult> _processEquipment({
|
||||||
|
required String code,
|
||||||
|
required EventModel event,
|
||||||
|
required dynamic step,
|
||||||
|
required Map<String, EquipmentModel> equipmentCache,
|
||||||
|
required Map<String, bool> validationState,
|
||||||
|
required Map<String, int> currentQuantities,
|
||||||
|
}) async {
|
||||||
|
// Chercher l'équipement dans les équipements assignés
|
||||||
|
final eventEquipment = event.assignedEquipment
|
||||||
|
.cast<EventEquipment?>()
|
||||||
|
.firstWhere(
|
||||||
|
(eq) => eq?.equipmentId == code,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eventEquipment == null) {
|
||||||
|
DebugLog.info('[QRCodeProcessingService] Equipment $code not found in event');
|
||||||
|
return QRCodeProcessResult.notFoundInEvent(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
final equipment = equipmentCache[code];
|
||||||
|
final equipmentName = equipment?.name ?? 'Équipement inconnu';
|
||||||
|
|
||||||
|
// Vérifier si l'équipement a des quantités
|
||||||
|
if (equipment?.hasQuantity ?? false) {
|
||||||
|
return _processQuantitativeEquipment(
|
||||||
|
code: code,
|
||||||
|
equipmentName: equipmentName,
|
||||||
|
eventEquipment: eventEquipment,
|
||||||
|
step: step,
|
||||||
|
validationState: validationState,
|
||||||
|
currentQuantities: currentQuantities,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return _processNonQuantitativeEquipment(
|
||||||
|
code: code,
|
||||||
|
equipmentName: equipmentName,
|
||||||
|
validationState: validationState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Traiter un équipement quantitatif (incrémenter la quantité)
|
||||||
|
QRCodeProcessResult _processQuantitativeEquipment({
|
||||||
|
required String code,
|
||||||
|
required String equipmentName,
|
||||||
|
required EventEquipment eventEquipment,
|
||||||
|
required dynamic step,
|
||||||
|
required Map<String, bool> validationState,
|
||||||
|
required Map<String, int> currentQuantities,
|
||||||
|
}) {
|
||||||
|
final currentQty = currentQuantities[code] ?? 0;
|
||||||
|
final targetQty = _getTargetQuantity(eventEquipment, step);
|
||||||
|
|
||||||
|
// Vérifier si on a déjà atteint la quantité cible
|
||||||
|
if (currentQty >= targetQty) {
|
||||||
|
return QRCodeProcessResult.error(
|
||||||
|
'Quantité cible déjà atteinte pour $equipmentName ($currentQty/$targetQty)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Incrémenter la quantité
|
||||||
|
final newQty = currentQty + 1;
|
||||||
|
final shouldCheck = newQty >= targetQty;
|
||||||
|
|
||||||
|
return QRCodeProcessResult.success(
|
||||||
|
message: '$equipmentName : $newQty/$targetQty${shouldCheck ? " ✓" : ""}',
|
||||||
|
affectedEquipmentIds: [code],
|
||||||
|
updatedQuantities: {code: newQty},
|
||||||
|
updatedValidationState: shouldCheck ? {code: true} : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Traiter un équipement non quantitatif (cocher)
|
||||||
|
QRCodeProcessResult _processNonQuantitativeEquipment({
|
||||||
|
required String code,
|
||||||
|
required String equipmentName,
|
||||||
|
required Map<String, bool> validationState,
|
||||||
|
}) {
|
||||||
|
// Vérifier si déjà coché
|
||||||
|
if (validationState[code] == true) {
|
||||||
|
return QRCodeProcessResult.error('$equipmentName est déjà coché');
|
||||||
|
}
|
||||||
|
|
||||||
|
return QRCodeProcessResult.success(
|
||||||
|
message: '$equipmentName a été coché ✓',
|
||||||
|
affectedEquipmentIds: [code],
|
||||||
|
updatedValidationState: {code: true},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Traiter un code de container (cocher tous les enfants)
|
||||||
|
Future<QRCodeProcessResult> _processContainer({
|
||||||
|
required String code,
|
||||||
|
required EventModel event,
|
||||||
|
required dynamic step,
|
||||||
|
required Map<String, EquipmentModel> equipmentCache,
|
||||||
|
required Map<String, ContainerModel> containerCache,
|
||||||
|
required Map<String, bool> validationState,
|
||||||
|
required Map<String, int> currentQuantities,
|
||||||
|
}) async {
|
||||||
|
// Vérifier que le container est assigné à l'événement
|
||||||
|
if (!event.assignedContainers.contains(code)) {
|
||||||
|
DebugLog.info('[QRCodeProcessingService] Container $code not found in event');
|
||||||
|
return QRCodeProcessResult.notFoundInEvent(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
final container = containerCache[code];
|
||||||
|
if (container == null) {
|
||||||
|
return QRCodeProcessResult.error('Container introuvable dans le cache');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traiter tous les équipements enfants
|
||||||
|
final updatedValidation = <String, bool>{};
|
||||||
|
final updatedQuantities = <String, int>{};
|
||||||
|
int processedCount = 0;
|
||||||
|
|
||||||
|
for (final childId in container.equipmentIds) {
|
||||||
|
final childEventEq = event.assignedEquipment
|
||||||
|
.cast<EventEquipment?>()
|
||||||
|
.firstWhere(
|
||||||
|
(eq) => eq?.equipmentId == childId,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (childEventEq == null) continue;
|
||||||
|
|
||||||
|
final childEquipment = equipmentCache[childId];
|
||||||
|
|
||||||
|
// Si quantitatif, mettre la quantité actuelle = quantité cible
|
||||||
|
if (childEquipment?.hasQuantity ?? false) {
|
||||||
|
final targetQty = _getTargetQuantity(childEventEq, step);
|
||||||
|
updatedQuantities[childId] = targetQty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cocher l'enfant
|
||||||
|
updatedValidation[childId] = true;
|
||||||
|
processedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedCount == 0) {
|
||||||
|
return QRCodeProcessResult.error(
|
||||||
|
'Aucun équipement trouvé dans le container ${container.name}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return QRCodeProcessResult.success(
|
||||||
|
message: 'Container ${container.name} : $processedCount équipement(s) validé(s) ✓',
|
||||||
|
affectedEquipmentIds: updatedValidation.keys.toList(),
|
||||||
|
updatedValidationState: updatedValidation,
|
||||||
|
updatedQuantities: updatedQuantities.isNotEmpty ? updatedQuantities : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtenir la quantité requise selon l'étape
|
||||||
|
/// Logique: chaque étape utilise la quantité actuelle de l'étape N-1
|
||||||
|
int _getTargetQuantity(EventEquipment eventEquipment, dynamic step) {
|
||||||
|
// Convertir l'enum en string pour comparer
|
||||||
|
final stepString = step.toString().split('.').last;
|
||||||
|
|
||||||
|
switch (stepString) {
|
||||||
|
case 'preparation':
|
||||||
|
// Étape 1 : Quantité définie à la création de l'événement
|
||||||
|
return eventEquipment.quantity;
|
||||||
|
|
||||||
|
case 'loadingOutbound':
|
||||||
|
// Étape 2 : Quantité validée à l'étape 1 (préparation)
|
||||||
|
return eventEquipment.quantityAtPreparation ?? eventEquipment.quantity;
|
||||||
|
|
||||||
|
case 'unloadingReturn':
|
||||||
|
// Étape 3 : Quantité validée à l'étape 2 (chargement)
|
||||||
|
return eventEquipment.quantityAtLoading ??
|
||||||
|
eventEquipment.quantityAtPreparation ??
|
||||||
|
eventEquipment.quantity;
|
||||||
|
|
||||||
|
case 'return_':
|
||||||
|
// Étape 4 : Quantité validée à l'étape 3 (déchargement)
|
||||||
|
return eventEquipment.quantityAtUnloading ??
|
||||||
|
eventEquipment.quantityAtLoading ??
|
||||||
|
eventEquipment.quantityAtPreparation ??
|
||||||
|
eventEquipment.quantity;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return eventEquipment.quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,8 +104,9 @@ class CalendarUtils {
|
|||||||
|
|
||||||
static List<EventModel> getEventsForDay(
|
static List<EventModel> getEventsForDay(
|
||||||
DateTime day, List<EventModel> events) {
|
DateTime day, List<EventModel> events) {
|
||||||
final dayStart = DateTime(day.year, day.month, day.day, 0, 0);
|
final nextDay = day.add(const Duration(days: 1));
|
||||||
final dayEnd = DateTime(day.year, day.month, day.day, 23, 59, 59);
|
final dayStart = DateTime(day.year, day.month, day.day, 2, 0);
|
||||||
|
final dayEnd = DateTime(nextDay.year, nextDay.month, nextDay.day, 2, 59, 59);
|
||||||
|
|
||||||
return events.where((event) {
|
return events.where((event) {
|
||||||
return !(event.endDateTime.isBefore(dayStart) ||
|
return !(event.endDateTime.isBefore(dayStart) ||
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import 'package:em2rp/views/widgets/common/qr_code_scanner_dialog.dart';
|
|||||||
import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart';
|
import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart';
|
||||||
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
||||||
import 'package:em2rp/views/widgets/management/management_card.dart';
|
import 'package:em2rp/views/widgets/management/management_card.dart';
|
||||||
import 'package:em2rp/views/widgets/management/management_list.dart';
|
|
||||||
import 'package:em2rp/utils/debug_log.dart';
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
import 'package:em2rp/views/widgets/common/search_actions_bar.dart';
|
import 'package:em2rp/views/widgets/common/search_actions_bar.dart';
|
||||||
import 'package:em2rp/views/widgets/notification_badge.dart';
|
import 'package:em2rp/views/widgets/notification_badge.dart';
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart';
|
|||||||
import 'package:em2rp/views/widgets/equipment/equipment_status_badge.dart';
|
import 'package:em2rp/views/widgets/equipment/equipment_status_badge.dart';
|
||||||
import 'package:em2rp/utils/debug_log.dart';
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
||||||
import 'package:em2rp/views/widgets/management/management_list.dart';
|
|
||||||
import 'package:em2rp/views/widgets/common/search_actions_bar.dart';
|
import 'package:em2rp/views/widgets/common/search_actions_bar.dart';
|
||||||
import 'package:em2rp/views/widgets/notification_badge.dart';
|
import 'package:em2rp/views/widgets/notification_badge.dart';
|
||||||
|
|
||||||
@@ -58,7 +57,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onScroll() {
|
void _onScroll() {
|
||||||
// Éviter les appels multiples
|
// Éviter les appels multiples avec un flag simple (sans setState)
|
||||||
if (_isLoadingMore) return;
|
if (_isLoadingMore) return;
|
||||||
|
|
||||||
final provider = context.read<EquipmentProvider>();
|
final provider = context.read<EquipmentProvider>();
|
||||||
@@ -70,16 +69,13 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
|
|
||||||
// Vérifier qu'on peut charger plus
|
// Vérifier qu'on peut charger plus
|
||||||
if (provider.hasMore && !provider.isLoadingMore) {
|
if (provider.hasMore && !provider.isLoadingMore) {
|
||||||
setState(() => _isLoadingMore = true);
|
// ✅ Pas de setState ici pour éviter les rebuilds pendant le scroll
|
||||||
|
_isLoadingMore = true;
|
||||||
|
|
||||||
provider.loadNextPage().then((_) {
|
provider.loadNextPage().then((_) {
|
||||||
if (mounted) {
|
_isLoadingMore = false;
|
||||||
setState(() => _isLoadingMore = false);
|
|
||||||
}
|
|
||||||
}).catchError((error) {
|
}).catchError((error) {
|
||||||
if (mounted) {
|
_isLoadingMore = false;
|
||||||
setState(() => _isLoadingMore = false);
|
|
||||||
}
|
|
||||||
DebugLog.error('[EquipmentManagementPage] Error loading next page', error);
|
DebugLog.error('[EquipmentManagementPage] Error loading next page', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -502,15 +498,18 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
|
// ✅ Ajouter une estimation de la hauteur pour améliorer le scroll
|
||||||
|
// Note : À ajuster selon la hauteur réelle de vos cartes
|
||||||
|
// itemExtent: 140, // Décommentez si toutes les cartes ont la même hauteur
|
||||||
|
// ✅ Augmenter le cache pour un scroll plus fluide
|
||||||
|
cacheExtent: 500, // Précharger 500px en plus
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
// Dernier élément = indicateur de chargement
|
// Dernier élément = indicateur de chargement
|
||||||
if (index == equipments.length) {
|
if (index == equipments.length) {
|
||||||
return Center(
|
return const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(16.0),
|
||||||
child: provider.isLoadingMore
|
child: CircularProgressIndicator(),
|
||||||
? const CircularProgressIndicator()
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -525,78 +524,81 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
Widget _buildEquipmentCard(EquipmentModel equipment) {
|
Widget _buildEquipmentCard(EquipmentModel equipment) {
|
||||||
final isSelected = isItemSelected(equipment.id);
|
final isSelected = isItemSelected(equipment.id);
|
||||||
|
|
||||||
return Card(
|
// ✅ RepaintBoundary pour isoler le repaint de chaque carte
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
return RepaintBoundary(
|
||||||
color: isSelectionMode && isSelected
|
key: ValueKey(equipment.id),
|
||||||
? AppColors.rouge.withValues(alpha: 0.1)
|
child: Card(
|
||||||
: null,
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
child: ListTile(
|
color: isSelectionMode && isSelected
|
||||||
leading: isSelectionMode
|
? AppColors.rouge.withValues(alpha: 0.1)
|
||||||
? Checkbox(
|
: null,
|
||||||
value: isSelected,
|
child: ListTile(
|
||||||
onChanged: (value) => toggleItemSelection(equipment.id),
|
leading: isSelectionMode
|
||||||
activeColor: AppColors.rouge,
|
? Checkbox(
|
||||||
)
|
value: isSelected,
|
||||||
: CircleAvatar(
|
onChanged: (value) => toggleItemSelection(equipment.id),
|
||||||
backgroundColor: equipment.category.color.withValues(alpha: 0.2),
|
activeColor: AppColors.rouge,
|
||||||
child: equipment.category.getIcon(
|
)
|
||||||
size: 20,
|
: CircleAvatar(
|
||||||
color: equipment.category.color,
|
backgroundColor: equipment.category.color.withValues(alpha: 0.2),
|
||||||
),
|
child: equipment.category.getIcon(
|
||||||
),
|
size: 20,
|
||||||
title: Row(
|
color: equipment.category.color,
|
||||||
children: [
|
),
|
||||||
Expanded(
|
),
|
||||||
child: Text(
|
title: Row(
|
||||||
equipment.id,
|
children: [
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
Expanded(
|
||||||
),
|
child: Text(
|
||||||
),
|
equipment.id,
|
||||||
// Afficher le badge de statut calculé dynamiquement
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
if (equipment.category != EquipmentCategory.consumable &&
|
|
||||||
equipment.category != EquipmentCategory.cable)
|
|
||||||
EquipmentStatusBadge(equipment: equipment),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'${equipment.brand ?? ''} ${equipment.model ?? ''}'
|
|
||||||
.trim()
|
|
||||||
.isNotEmpty
|
|
||||||
? '${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim()
|
|
||||||
: 'Marque/Modèle non défini',
|
|
||||||
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
|
||||||
),
|
|
||||||
// Afficher la sous-catégorie si elle existe
|
|
||||||
if (equipment.subCategory != null && equipment.subCategory!.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
'📁 ${equipment.subCategory}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[500],
|
|
||||||
fontSize: 12,
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Afficher le badge de statut calculé dynamiquement
|
||||||
|
if (equipment.category != EquipmentCategory.consumable &&
|
||||||
|
equipment.category != EquipmentCategory.cable)
|
||||||
|
EquipmentStatusBadge(equipment: equipment),
|
||||||
],
|
],
|
||||||
// Afficher la quantité disponible pour les consommables/câbles
|
),
|
||||||
if (equipment.category == EquipmentCategory.consumable ||
|
subtitle: Column(
|
||||||
equipment.category == EquipmentCategory.cable) ...[
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
_buildQuantityDisplay(equipment),
|
Text(
|
||||||
|
'${equipment.brand ?? ''} ${equipment.model ?? ''}'
|
||||||
|
.trim()
|
||||||
|
.isNotEmpty
|
||||||
|
? '${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim()
|
||||||
|
: 'Marque/Modèle non défini',
|
||||||
|
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
||||||
|
),
|
||||||
|
// Afficher la sous-catégorie si elle existe
|
||||||
|
if (equipment.subCategory != null && equipment.subCategory!.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
'📁 ${equipment.subCategory}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[500],
|
||||||
|
fontSize: 12,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
// Afficher la quantité disponible pour les consommables/câbles
|
||||||
|
if (equipment.category == EquipmentCategory.consumable ||
|
||||||
|
equipment.category == EquipmentCategory.cable) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
_buildQuantityDisplay(equipment),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
trailing: isSelectionMode
|
||||||
trailing: isSelectionMode
|
? null
|
||||||
? null
|
: Row(
|
||||||
: Row(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
// Bouton Restock (uniquement pour consommables/câbles avec permission)
|
||||||
// Bouton Restock (uniquement pour consommables/câbles avec permission)
|
if (equipment.category == EquipmentCategory.consumable ||
|
||||||
if (equipment.category == EquipmentCategory.consumable ||
|
|
||||||
equipment.category == EquipmentCategory.cable)
|
equipment.category == EquipmentCategory.cable)
|
||||||
PermissionGate(
|
PermissionGate(
|
||||||
requiredPermissions: const ['manage_equipment'],
|
requiredPermissions: const ['manage_equipment'],
|
||||||
@@ -640,6 +642,7 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
? () => toggleItemSelection(equipment.id)
|
? () => toggleItemSelection(equipment.id)
|
||||||
: () => _viewEquipmentDetails(equipment),
|
: () => _viewEquipmentDetails(equipment),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:cloud_functions/cloud_functions.dart';
|
import 'package:cloud_functions/cloud_functions.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:em2rp/models/event_model.dart';
|
import 'package:em2rp/models/event_model.dart';
|
||||||
import 'package:em2rp/models/equipment_model.dart';
|
import 'package:em2rp/models/equipment_model.dart';
|
||||||
import 'package:em2rp/models/container_model.dart';
|
import 'package:em2rp/models/container_model.dart';
|
||||||
@@ -10,8 +11,14 @@ import 'package:em2rp/providers/event_provider.dart';
|
|||||||
import 'package:em2rp/providers/local_user_provider.dart';
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
import 'package:em2rp/services/data_service.dart';
|
import 'package:em2rp/services/data_service.dart';
|
||||||
import 'package:em2rp/services/api_service.dart';
|
import 'package:em2rp/services/api_service.dart';
|
||||||
|
import 'package:em2rp/services/qr_code_processing_service.dart';
|
||||||
|
import 'package:em2rp/services/audio_feedback_service.dart';
|
||||||
|
import 'package:em2rp/services/equipment_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/event_preparation/code_not_found_dialog.dart';
|
||||||
|
import 'package:em2rp/views/widgets/event_preparation/add_equipment_to_event_dialog.dart';
|
||||||
import 'package:em2rp/utils/debug_log.dart';
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
import 'package:em2rp/views/widgets/equipment/missing_equipment_dialog.dart';
|
import 'package:em2rp/views/widgets/equipment/missing_equipment_dialog.dart';
|
||||||
import 'package:em2rp/utils/colors.dart';
|
import 'package:em2rp/utils/colors.dart';
|
||||||
@@ -40,6 +47,7 @@ class EventPreparationPage extends StatefulWidget {
|
|||||||
class _EventPreparationPageState extends State<EventPreparationPage> with SingleTickerProviderStateMixin {
|
class _EventPreparationPageState extends State<EventPreparationPage> with SingleTickerProviderStateMixin {
|
||||||
late AnimationController _animationController;
|
late AnimationController _animationController;
|
||||||
late final DataService _dataService;
|
late final DataService _dataService;
|
||||||
|
late final QRCodeProcessingService _qrCodeService;
|
||||||
|
|
||||||
Map<String, EquipmentModel> _equipmentCache = {};
|
Map<String, EquipmentModel> _equipmentCache = {};
|
||||||
Map<String, ContainerModel> _containerCache = {};
|
Map<String, ContainerModel> _containerCache = {};
|
||||||
@@ -48,8 +56,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
// État local des validations (non sauvegardé jusqu'à la validation finale)
|
// État local des validations (non sauvegardé jusqu'à la validation finale)
|
||||||
Map<String, bool> _localValidationState = {};
|
Map<String, bool> _localValidationState = {};
|
||||||
|
|
||||||
|
// Gestion des quantités par étape
|
||||||
// NOUVEAU : Gestion des quantités par étape
|
|
||||||
Map<String, int> _quantitiesAtPreparation = {};
|
Map<String, int> _quantitiesAtPreparation = {};
|
||||||
Map<String, int> _quantitiesAtLoading = {};
|
Map<String, int> _quantitiesAtLoading = {};
|
||||||
Map<String, int> _quantitiesAtUnloading = {};
|
Map<String, int> _quantitiesAtUnloading = {};
|
||||||
@@ -63,6 +70,10 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
// Stockage de l'événement actuel
|
// Stockage de l'événement actuel
|
||||||
late EventModel _currentEvent;
|
late EventModel _currentEvent;
|
||||||
|
|
||||||
|
// 🆕 Pour la saisie manuelle de codes
|
||||||
|
final TextEditingController _manualCodeController = TextEditingController();
|
||||||
|
final FocusNode _manualCodeFocusNode = FocusNode();
|
||||||
|
|
||||||
// Détermine l'étape actuelle selon le statut de l'événement
|
// Détermine l'étape actuelle selon le statut de l'événement
|
||||||
PreparationStep get _currentStep {
|
PreparationStep get _currentStep {
|
||||||
final prep = _currentEvent.preparationStatus ?? PreparationStatus.notStarted;
|
final prep = _currentEvent.preparationStatus ?? PreparationStatus.notStarted;
|
||||||
@@ -100,6 +111,7 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
super.initState();
|
super.initState();
|
||||||
_currentEvent = widget.initialEvent;
|
_currentEvent = widget.initialEvent;
|
||||||
_dataService = DataService(FirebaseFunctionsApiService());
|
_dataService = DataService(FirebaseFunctionsApiService());
|
||||||
|
_qrCodeService = QRCodeProcessingService();
|
||||||
_animationController = AnimationController(
|
_animationController = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: const Duration(milliseconds: 500),
|
duration: const Duration(milliseconds: 500),
|
||||||
@@ -140,6 +152,8 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_animationController.dispose();
|
_animationController.dispose();
|
||||||
|
_manualCodeController.dispose();
|
||||||
|
_manualCodeFocusNode.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,20 +161,46 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final equipmentProvider = context.read<EquipmentProvider>();
|
// 🔧 FIX: Utiliser getEventWithDetails pour charger toutes les données d'un coup
|
||||||
final containerProvider = context.read<ContainerProvider>();
|
DebugLog.info('[EventPreparationPage] Loading event with details: ${_currentEvent.id}');
|
||||||
|
|
||||||
// S'assurer que les équipements sont chargés
|
final result = await _dataService.getEventWithDetails(_currentEvent.id);
|
||||||
await equipmentProvider.ensureLoaded();
|
final equipmentsMap = result['equipments'] as Map<String, dynamic>;
|
||||||
await containerProvider.ensureLoaded();
|
final containersMap = result['containers'] as Map<String, dynamic>;
|
||||||
|
|
||||||
final equipment = await equipmentProvider.equipmentStream.first;
|
DebugLog.info('[EventPreparationPage] Loaded ${equipmentsMap.length} equipments and ${containersMap.length} containers with details');
|
||||||
final containers = await containerProvider.containersStream.first;
|
|
||||||
|
|
||||||
|
// Remplir les caches
|
||||||
|
_equipmentCache.clear();
|
||||||
|
_containerCache.clear();
|
||||||
|
|
||||||
|
// Remplir le cache d'équipements
|
||||||
|
equipmentsMap.forEach((id, data) {
|
||||||
|
try {
|
||||||
|
final equipment = EquipmentModel.fromMap(data as Map<String, dynamic>, id);
|
||||||
|
_equipmentCache[id] = equipment;
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[EventPreparationPage] Error parsing equipment $id', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remplir le cache de containers
|
||||||
|
containersMap.forEach((id, data) {
|
||||||
|
try {
|
||||||
|
final container = ContainerModel.fromMap(data as Map<String, dynamic>, id);
|
||||||
|
_containerCache[id] = container;
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[EventPreparationPage] Error parsing container $id', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialiser les états de validation et quantités pour chaque équipement assigné
|
||||||
for (var eq in _currentEvent.assignedEquipment) {
|
for (var eq in _currentEvent.assignedEquipment) {
|
||||||
final equipmentItem = equipment.firstWhere(
|
final equipmentItem = _equipmentCache[eq.equipmentId];
|
||||||
(e) => e.id == eq.equipmentId,
|
|
||||||
orElse: () => EquipmentModel(
|
// S'assurer que l'équipement est dans le cache (même si inconnu)
|
||||||
|
if (equipmentItem == null) {
|
||||||
|
_equipmentCache[eq.equipmentId] = EquipmentModel(
|
||||||
id: eq.equipmentId,
|
id: eq.equipmentId,
|
||||||
name: 'Équipement inconnu',
|
name: 'Équipement inconnu',
|
||||||
category: EquipmentCategory.other,
|
category: EquipmentCategory.other,
|
||||||
@@ -168,9 +208,8 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
maintenanceIds: [],
|
maintenanceIds: [],
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
_equipmentCache[eq.equipmentId] = equipmentItem;
|
|
||||||
|
|
||||||
// Initialiser l'état local de validation depuis l'événement
|
// Initialiser l'état local de validation depuis l'événement
|
||||||
switch (_currentStep) {
|
switch (_currentStep) {
|
||||||
@@ -190,15 +229,15 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
|
|
||||||
if ((_currentStep == PreparationStep.return_ ||
|
if ((_currentStep == PreparationStep.return_ ||
|
||||||
_currentStep == PreparationStep.unloadingReturn) &&
|
_currentStep == PreparationStep.unloadingReturn) &&
|
||||||
equipmentItem.hasQuantity) {
|
(equipmentItem?.hasQuantity ?? false)) {
|
||||||
_returnedQuantities[eq.equipmentId] = eq.quantityAtReturn ?? eq.quantity;
|
_returnedQuantities[eq.equipmentId] = eq.quantityAtReturn ?? eq.quantity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// S'assurer que les containers assignés sont dans le cache (même si inconnus)
|
||||||
for (var containerId in _currentEvent.assignedContainers) {
|
for (var containerId in _currentEvent.assignedContainers) {
|
||||||
final container = containers.firstWhere(
|
if (!_containerCache.containsKey(containerId)) {
|
||||||
(c) => c.id == containerId,
|
_containerCache[containerId] = ContainerModel(
|
||||||
orElse: () => ContainerModel(
|
|
||||||
id: containerId,
|
id: containerId,
|
||||||
name: 'Conteneur inconnu',
|
name: 'Conteneur inconnu',
|
||||||
type: ContainerType.flightCase,
|
type: ContainerType.flightCase,
|
||||||
@@ -206,9 +245,8 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
equipmentIds: [],
|
equipmentIds: [],
|
||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
_containerCache[containerId] = container;
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLog.error('[EventPreparationPage] Error', e);
|
DebugLog.error('[EventPreparationPage] Error', e);
|
||||||
@@ -564,6 +602,311 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// 🆕 NOUVELLES MÉTHODES POUR LE SCAN QR ET LA SAISIE MANUELLE
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
/// Ouvrir le scanner QR en mode multi-scan
|
||||||
|
Future<void> _openQRScanner() async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => QRCodeScannerDialog(
|
||||||
|
multiScanMode: true,
|
||||||
|
onCodeScanned: _handleScannedCode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Traiter un code (scanné ou saisi manuellement)
|
||||||
|
Future<void> _handleScannedCode(String code) async {
|
||||||
|
final result = await _qrCodeService.processCode(
|
||||||
|
code: code.trim(),
|
||||||
|
event: _currentEvent,
|
||||||
|
step: _currentStep,
|
||||||
|
equipmentCache: _equipmentCache,
|
||||||
|
containerCache: _containerCache,
|
||||||
|
validationState: _localValidationState,
|
||||||
|
currentQuantities: _getCurrentQuantitiesMap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// ✅ Succès : mettre à jour l'état
|
||||||
|
setState(() {
|
||||||
|
if (result.updatedValidationState != null) {
|
||||||
|
_localValidationState.addAll(result.updatedValidationState!);
|
||||||
|
}
|
||||||
|
if (result.updatedQuantities != null) {
|
||||||
|
_updateQuantitiesMap(result.updatedQuantities!);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔊 Jouer le feedback sonore et haptique
|
||||||
|
await AudioFeedbackService.playFullFeedback(isSuccess: true);
|
||||||
|
|
||||||
|
// Feedback visuel
|
||||||
|
_showSuccessFeedback(result.message ?? 'Code traité avec succès');
|
||||||
|
|
||||||
|
} else if (result.codeNotFoundInEvent) {
|
||||||
|
// 🔍 Code non trouvé dans l'événement → proposer de l'ajouter
|
||||||
|
await _handleCodeNotFoundInEvent(code.trim());
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// ❌ Erreur (ex: quantité déjà atteinte, déjà coché)
|
||||||
|
await AudioFeedbackService.playFullFeedback(isSuccess: false);
|
||||||
|
_showErrorFeedback(result.message ?? 'Erreur lors du traitement');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gérer un code non trouvé dans l'événement
|
||||||
|
Future<void> _handleCodeNotFoundInEvent(String code) async {
|
||||||
|
// Afficher le dialog de confirmation
|
||||||
|
final shouldSearch = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => CodeNotFoundDialog(scannedCode: code),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldSearch != true) return;
|
||||||
|
|
||||||
|
// Afficher le dialog de chargement
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (context) => const AddEquipmentToEventDialog(
|
||||||
|
state: AddEquipmentState.loading,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Identifier le type selon le préfixe
|
||||||
|
final isContainer = code.startsWith('BOX_');
|
||||||
|
|
||||||
|
if (isContainer) {
|
||||||
|
await _addContainerToEvent(code);
|
||||||
|
} else {
|
||||||
|
await _addEquipmentToEvent(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔊 Bip de succès
|
||||||
|
await AudioFeedbackService.playFullFeedback(isSuccess: true);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[EventPreparationPage] Error adding item to event', e);
|
||||||
|
|
||||||
|
// Fermer le dialog de chargement et afficher l'erreur
|
||||||
|
if (mounted) Navigator.of(context).pop();
|
||||||
|
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AddEquipmentToEventDialog(
|
||||||
|
state: AddEquipmentState.error,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 🔊 Bip d'erreur
|
||||||
|
await AudioFeedbackService.playFullFeedback(isSuccess: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ajouter un équipement à l'événement
|
||||||
|
Future<void> _addEquipmentToEvent(String equipmentId) async {
|
||||||
|
// Rechercher l'équipement dans la base de données
|
||||||
|
final equipmentProvider = context.read<EquipmentProvider>();
|
||||||
|
await equipmentProvider.ensureLoaded();
|
||||||
|
|
||||||
|
// Chercher d'abord dans le cache
|
||||||
|
EquipmentModel? equipment = equipmentProvider.allEquipment
|
||||||
|
.cast<EquipmentModel?>()
|
||||||
|
.firstWhere(
|
||||||
|
(eq) => eq?.id == equipmentId,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Si pas dans le cache, charger depuis Firestore
|
||||||
|
if (equipment == null) {
|
||||||
|
final equipmentService = EquipmentService();
|
||||||
|
equipment = await equipmentService.getEquipmentById(equipmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equipment == null) {
|
||||||
|
throw Exception('Équipement non trouvé dans la base de données');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter l'équipement à l'événement
|
||||||
|
final newEventEquipment = EventEquipment(
|
||||||
|
equipmentId: equipmentId,
|
||||||
|
quantity: 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedEquipment = List<EventEquipment>.from(_currentEvent.assignedEquipment)
|
||||||
|
..add(newEventEquipment);
|
||||||
|
|
||||||
|
await _dataService.updateEvent(_currentEvent.id, {
|
||||||
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mettre à jour l'état local
|
||||||
|
setState(() {
|
||||||
|
_currentEvent = _currentEvent.copyWith(
|
||||||
|
assignedEquipment: updatedEquipment,
|
||||||
|
);
|
||||||
|
_equipmentCache[equipmentId] = equipment!;
|
||||||
|
_localValidationState[equipmentId] = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fermer le dialog de chargement et afficher le succès
|
||||||
|
if (mounted) Navigator.of(context).pop();
|
||||||
|
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AddEquipmentToEventDialog(
|
||||||
|
state: AddEquipmentState.success,
|
||||||
|
itemName: equipment!.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ajouter un container à l'événement
|
||||||
|
Future<void> _addContainerToEvent(String containerId) async {
|
||||||
|
// Rechercher le container dans la base de données
|
||||||
|
final containerProvider = context.read<ContainerProvider>();
|
||||||
|
await containerProvider.ensureLoaded();
|
||||||
|
|
||||||
|
final container = await containerProvider.getContainerById(containerId);
|
||||||
|
|
||||||
|
if (container == null) {
|
||||||
|
throw Exception('Container non trouvé dans la base de données');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter le container à l'événement
|
||||||
|
final updatedContainers = List<String>.from(_currentEvent.assignedContainers)
|
||||||
|
..add(containerId);
|
||||||
|
|
||||||
|
await _dataService.updateEvent(_currentEvent.id, {
|
||||||
|
'assignedContainers': updatedContainers,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mettre à jour l'état local
|
||||||
|
setState(() {
|
||||||
|
_currentEvent = _currentEvent.copyWith(
|
||||||
|
assignedContainers: updatedContainers,
|
||||||
|
);
|
||||||
|
_containerCache[containerId] = container;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fermer le dialog de chargement et afficher le succès
|
||||||
|
if (mounted) Navigator.of(context).pop();
|
||||||
|
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AddEquipmentToEventDialog(
|
||||||
|
state: AddEquipmentState.success,
|
||||||
|
itemName: 'Container ${container.name}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Traiter la saisie manuelle d'un code
|
||||||
|
Future<void> _handleManualCodeEntry(String code) async {
|
||||||
|
if (code.trim().isEmpty) return;
|
||||||
|
|
||||||
|
await _handleScannedCode(code.trim());
|
||||||
|
|
||||||
|
// Effacer le champ après traitement
|
||||||
|
_manualCodeController.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtenir les quantités actuelles selon l'étape
|
||||||
|
Map<String, int> _getCurrentQuantitiesMap() {
|
||||||
|
switch (_currentStep) {
|
||||||
|
case PreparationStep.preparation:
|
||||||
|
return _quantitiesAtPreparation;
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
return _quantitiesAtLoading;
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
return _quantitiesAtUnloading;
|
||||||
|
case PreparationStep.return_:
|
||||||
|
return _quantitiesAtReturn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mettre à jour les quantités selon l'étape
|
||||||
|
void _updateQuantitiesMap(Map<String, int> quantities) {
|
||||||
|
switch (_currentStep) {
|
||||||
|
case PreparationStep.preparation:
|
||||||
|
_quantitiesAtPreparation.addAll(quantities);
|
||||||
|
break;
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
_quantitiesAtLoading.addAll(quantities);
|
||||||
|
break;
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
_quantitiesAtUnloading.addAll(quantities);
|
||||||
|
break;
|
||||||
|
case PreparationStep.return_:
|
||||||
|
_quantitiesAtReturn.addAll(quantities);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtenir la quantité requise selon l'étape (nouvelle logique)
|
||||||
|
int _getTargetQuantity(EventEquipment eventEquipment) {
|
||||||
|
switch (_currentStep) {
|
||||||
|
case PreparationStep.preparation:
|
||||||
|
return eventEquipment.quantity; // Quantité initiale
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
return eventEquipment.quantityAtPreparation ?? eventEquipment.quantity;
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
return eventEquipment.quantityAtLoading ??
|
||||||
|
eventEquipment.quantityAtPreparation ??
|
||||||
|
eventEquipment.quantity;
|
||||||
|
case PreparationStep.return_:
|
||||||
|
return eventEquipment.quantityAtUnloading ??
|
||||||
|
eventEquipment.quantityAtLoading ??
|
||||||
|
eventEquipment.quantityAtPreparation ??
|
||||||
|
eventEquipment.quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Afficher un message de succès
|
||||||
|
void _showSuccessFeedback(String message) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.check_circle, color: Colors.white),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: Text(message)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Afficher un message d'erreur
|
||||||
|
void _showErrorFeedback(String message) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error, color: Colors.white),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: Text(message)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// FIN DES NOUVELLES MÉTHODES
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
Future<void> _confirm() async {
|
Future<void> _confirm() async {
|
||||||
// Vérifier s'il y a des équipements manquants (non cochés localement)
|
// Vérifier s'il y a des équipements manquants (non cochés localement)
|
||||||
final missingEquipmentIds = _currentEvent.assignedEquipment
|
final missingEquipmentIds = _currentEvent.assignedEquipment
|
||||||
@@ -842,6 +1185,50 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// 🆕 Champ de saisie manuelle de code
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _manualCodeController,
|
||||||
|
focusNode: _manualCodeFocusNode,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Saisie manuelle d\'un code',
|
||||||
|
hintText: 'Entrez un ID d\'équipement ou container',
|
||||||
|
prefixIcon: const Icon(Icons.keyboard, color: AppColors.bleuFonce),
|
||||||
|
suffixIcon: _manualCodeController.text.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
_manualCodeController.clear();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
borderSide: const BorderSide(color: AppColors.bleuFonce, width: 2),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
),
|
||||||
|
onSubmitted: _handleManualCodeEntry,
|
||||||
|
onChanged: (value) => setState(() {}),
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
),
|
||||||
|
|
||||||
|
// 🆕 Bouton Scanner QR Code
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _openQRScanner,
|
||||||
|
icon: const Icon(Icons.qr_code_scanner),
|
||||||
|
label: const Text('Scanner QR Code'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blue[700],
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: allValidated ? null : _validateAllAndConfirm,
|
onPressed: allValidated ? null : _validateAllAndConfirm,
|
||||||
|
|||||||
@@ -23,39 +23,40 @@ class EventStatusButton extends StatefulWidget {
|
|||||||
|
|
||||||
class _EventStatusButtonState extends State<EventStatusButton> {
|
class _EventStatusButtonState extends State<EventStatusButton> {
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
|
EventStatus? _optimisticStatus;
|
||||||
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
||||||
|
|
||||||
Future<void> _changeStatus(EventStatus newStatus) async {
|
Future<void> _changeStatus(EventStatus newStatus) async {
|
||||||
if (widget.event.status == newStatus) return;
|
if ((widget.event.status == newStatus) || _loading) return;
|
||||||
setState(() => _loading = true);
|
setState(() {
|
||||||
|
_loading = true;
|
||||||
|
_optimisticStatus = newStatus;
|
||||||
|
});
|
||||||
|
final oldStatus = widget.event.status;
|
||||||
try {
|
try {
|
||||||
// Mettre à jour via l'API
|
|
||||||
await _dataService.updateEvent(widget.event.id, {
|
await _dataService.updateEvent(widget.event.id, {
|
||||||
'status': eventStatusToString(newStatus),
|
'status': eventStatusToString(newStatus),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Récupérer l'événement mis à jour via l'API
|
|
||||||
final result = await _dataService.getEvents();
|
final result = await _dataService.getEvents();
|
||||||
final eventsList = result['events'] as List<dynamic>;
|
final eventsList = result['events'] as List<dynamic>;
|
||||||
final eventData = eventsList.firstWhere(
|
final eventData = eventsList.firstWhere(
|
||||||
(e) => e['id'] == widget.event.id,
|
(e) => e['id'] == widget.event.id,
|
||||||
orElse: () => <String, dynamic>{},
|
orElse: () => <String, dynamic>{},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (eventData.isNotEmpty) {
|
if (eventData.isNotEmpty) {
|
||||||
final updatedEvent = EventModel.fromMap(eventData, widget.event.id);
|
final updatedEvent = EventModel.fromMap(eventData, widget.event.id);
|
||||||
|
|
||||||
widget.onSelectEvent(
|
widget.onSelectEvent(
|
||||||
updatedEvent,
|
updatedEvent,
|
||||||
widget.selectedDate ?? updatedEvent.startDateTime,
|
widget.selectedDate ?? updatedEvent.startDateTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
await Provider.of<EventProvider>(context, listen: false)
|
await Provider.of<EventProvider>(context, listen: false)
|
||||||
.updateEvent(updatedEvent);
|
.updateEvent(updatedEvent);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_optimisticStatus = oldStatus;
|
||||||
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Erreur lors du changement de statut: $e')),
|
SnackBar(content: Text('Erreur lors du changement de statut: $e')),
|
||||||
);
|
);
|
||||||
@@ -69,11 +70,22 @@ class _EventStatusButtonState extends State<EventStatusButton> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final status = widget.event.status;
|
final status = _optimisticStatus ?? widget.event.status;
|
||||||
String texte;
|
String texte;
|
||||||
Color couleurFond;
|
Color couleurFond;
|
||||||
List<Widget> enfants = [];
|
List<Widget> enfants = [];
|
||||||
|
|
||||||
|
if (_loading) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: const SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case EventStatus.waitingForApproval:
|
case EventStatus.waitingForApproval:
|
||||||
texte = "En Attente";
|
texte = "En Attente";
|
||||||
|
|||||||
@@ -4,7 +4,17 @@ import 'package:em2rp/utils/colors.dart';
|
|||||||
|
|
||||||
/// Dialog pour scanner un QR code et récupérer l'ID
|
/// Dialog pour scanner un QR code et récupérer l'ID
|
||||||
class QRCodeScannerDialog extends StatefulWidget {
|
class QRCodeScannerDialog extends StatefulWidget {
|
||||||
const QRCodeScannerDialog({super.key});
|
/// Callback appelé quand un code est scanné (mode multi-scan)
|
||||||
|
final Function(String code)? onCodeScanned;
|
||||||
|
|
||||||
|
/// Active le mode scan continu (ne ferme pas automatiquement)
|
||||||
|
final bool multiScanMode;
|
||||||
|
|
||||||
|
const QRCodeScannerDialog({
|
||||||
|
super.key,
|
||||||
|
this.onCodeScanned,
|
||||||
|
this.multiScanMode = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<QRCodeScannerDialog> createState() => _QRCodeScannerDialogState();
|
State<QRCodeScannerDialog> createState() => _QRCodeScannerDialogState();
|
||||||
@@ -45,12 +55,27 @@ class _QRCodeScannerDialogState extends State<QRCodeScannerDialog> {
|
|||||||
_scannedCode = code;
|
_scannedCode = code;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Retourner le code après un court délai pour montrer le feedback visuel
|
if (widget.multiScanMode && widget.onCodeScanned != null) {
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
// Mode multi-scan : appeler le callback et rester ouvert
|
||||||
if (mounted) {
|
widget.onCodeScanned!(code);
|
||||||
Navigator.of(context).pop(code);
|
|
||||||
}
|
// Réinitialiser après un délai pour permettre un nouveau scan
|
||||||
});
|
Future.delayed(const Duration(milliseconds: 800), () {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isProcessing = false;
|
||||||
|
_scannedCode = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Mode simple : retourner le code et fermer
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop(code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import 'package:em2rp/models/equipment_model.dart';
|
|||||||
import 'package:em2rp/models/container_model.dart';
|
import 'package:em2rp/models/container_model.dart';
|
||||||
import 'package:em2rp/providers/equipment_provider.dart';
|
import 'package:em2rp/providers/equipment_provider.dart';
|
||||||
import 'package:em2rp/providers/container_provider.dart';
|
import 'package:em2rp/providers/container_provider.dart';
|
||||||
|
import 'package:em2rp/services/data_service.dart';
|
||||||
|
import 'package:em2rp/services/api_service.dart';
|
||||||
import 'package:em2rp/utils/colors.dart';
|
import 'package:em2rp/utils/colors.dart';
|
||||||
import 'package:em2rp/views/widgets/event/equipment_selection_dialog.dart';
|
import 'package:em2rp/views/widgets/event/equipment_selection_dialog.dart';
|
||||||
import 'package:em2rp/views/widgets/event/equipment_conflict_dialog.dart';
|
import 'package:em2rp/views/widgets/event/equipment_conflict_dialog.dart';
|
||||||
@@ -37,6 +39,7 @@ class EventAssignedEquipmentSection extends StatefulWidget {
|
|||||||
class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSection> {
|
class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSection> {
|
||||||
bool get _canAddMaterial => widget.startDate != null && widget.endDate != null;
|
bool get _canAddMaterial => widget.startDate != null && widget.endDate != null;
|
||||||
final EventAvailabilityService _availabilityService = EventAvailabilityService();
|
final EventAvailabilityService _availabilityService = EventAvailabilityService();
|
||||||
|
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
||||||
Map<String, EquipmentModel> _equipmentCache = {};
|
Map<String, EquipmentModel> _equipmentCache = {};
|
||||||
Map<String, ContainerModel> _containerCache = {};
|
Map<String, ContainerModel> _containerCache = {};
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
@@ -64,52 +67,100 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
final equipmentProvider = context.read<EquipmentProvider>();
|
final equipmentProvider = context.read<EquipmentProvider>();
|
||||||
final containerProvider = context.read<ContainerProvider>();
|
final containerProvider = context.read<ContainerProvider>();
|
||||||
|
|
||||||
// Extraire les IDs des équipements assignés
|
// 🔧 FIX: Si on a un eventId, utiliser getEventWithDetails pour charger les données complètes
|
||||||
final equipmentIds = widget.assignedEquipment
|
if (widget.eventId != null && widget.eventId!.isNotEmpty) {
|
||||||
.map((eq) => eq.equipmentId)
|
DebugLog.info('[EventAssignedEquipmentSection] Loading event with details: ${widget.eventId}');
|
||||||
.toList();
|
|
||||||
|
|
||||||
// Charger UNIQUEMENT les équipements nécessaires (optimisé)
|
final result = await _dataService.getEventWithDetails(widget.eventId!);
|
||||||
final equipment = await equipmentProvider.getEquipmentsByIds(equipmentIds);
|
final equipmentsMap = result['equipments'] as Map<String, dynamic>;
|
||||||
|
final containersMap = result['containers'] as Map<String, dynamic>;
|
||||||
|
|
||||||
// Charger UNIQUEMENT les conteneurs nécessaires (optimisé)
|
DebugLog.info('[EventAssignedEquipmentSection] Loaded ${equipmentsMap.length} equipments and ${containersMap.length} containers with details');
|
||||||
final containers = await containerProvider.getContainersByIds(widget.assignedContainers);
|
|
||||||
|
|
||||||
// Créer le cache des équipements
|
// Construire les caches à partir des données reçues
|
||||||
for (var eq in widget.assignedEquipment) {
|
_equipmentCache.clear();
|
||||||
final equipmentItem = equipment.firstWhere(
|
_containerCache.clear();
|
||||||
(e) => e.id == eq.equipmentId,
|
|
||||||
orElse: () => EquipmentModel(
|
|
||||||
id: eq.equipmentId,
|
|
||||||
name: 'Équipement inconnu',
|
|
||||||
category: EquipmentCategory.other,
|
|
||||||
status: EquipmentStatus.available,
|
|
||||||
maintenanceIds: [],
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
updatedAt: DateTime.now(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_equipmentCache[eq.equipmentId] = equipmentItem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Créer le cache des conteneurs
|
// Remplir le cache d'équipements
|
||||||
for (var containerId in widget.assignedContainers) {
|
equipmentsMap.forEach((id, data) {
|
||||||
final container = containers.firstWhere(
|
try {
|
||||||
(c) => c.id == containerId,
|
_equipmentCache[id] = EquipmentModel.fromMap(data as Map<String, dynamic>, id);
|
||||||
orElse: () => ContainerModel(
|
} catch (e) {
|
||||||
id: containerId,
|
DebugLog.error('[EventAssignedEquipmentSection] Error parsing equipment $id', e);
|
||||||
name: 'Conteneur inconnu',
|
}
|
||||||
type: ContainerType.flightCase,
|
});
|
||||||
status: EquipmentStatus.available,
|
|
||||||
equipmentIds: [],
|
// Remplir le cache de containers
|
||||||
updatedAt: DateTime.now(),
|
containersMap.forEach((id, data) {
|
||||||
createdAt: DateTime.now(),
|
try {
|
||||||
),
|
_containerCache[id] = ContainerModel.fromMap(data as Map<String, dynamic>, id);
|
||||||
);
|
} catch (e) {
|
||||||
_containerCache[containerId] = container;
|
DebugLog.error('[EventAssignedEquipmentSection] Error parsing container $id', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
DebugLog.info('[EventAssignedEquipmentSection] Caches populated: ${_equipmentCache.length} equipments, ${_containerCache.length} containers');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Mode création d'événement : charger via les providers
|
||||||
|
DebugLog.info('[EventAssignedEquipmentSection] Loading via providers (creation mode)');
|
||||||
|
|
||||||
|
// Extraire les IDs des équipements assignés
|
||||||
|
final equipmentIds = widget.assignedEquipment
|
||||||
|
.map((eq) => eq.equipmentId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// Charger les conteneurs
|
||||||
|
final containers = await containerProvider.getContainersByIds(widget.assignedContainers);
|
||||||
|
|
||||||
|
// Extraire les IDs des équipements enfants des containers
|
||||||
|
final childEquipmentIds = <String>[];
|
||||||
|
for (var container in containers) {
|
||||||
|
childEquipmentIds.addAll(container.equipmentIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combiner les IDs des équipements assignés + enfants des containers
|
||||||
|
final allEquipmentIds = <String>{...equipmentIds, ...childEquipmentIds}.toList();
|
||||||
|
|
||||||
|
// Charger TOUS les équipements nécessaires
|
||||||
|
final equipment = await equipmentProvider.getEquipmentsByIds(allEquipmentIds);
|
||||||
|
|
||||||
|
// Créer le cache des équipements
|
||||||
|
for (var eq in widget.assignedEquipment) {
|
||||||
|
final equipmentItem = equipment.firstWhere(
|
||||||
|
(e) => e.id == eq.equipmentId,
|
||||||
|
orElse: () => EquipmentModel(
|
||||||
|
id: eq.equipmentId,
|
||||||
|
name: 'Équipement inconnu',
|
||||||
|
category: EquipmentCategory.other,
|
||||||
|
status: EquipmentStatus.available,
|
||||||
|
maintenanceIds: [],
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_equipmentCache[eq.equipmentId] = equipmentItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer le cache des conteneurs
|
||||||
|
for (var containerId in widget.assignedContainers) {
|
||||||
|
final container = containers.firstWhere(
|
||||||
|
(c) => c.id == containerId,
|
||||||
|
orElse: () => ContainerModel(
|
||||||
|
id: containerId,
|
||||||
|
name: 'Conteneur inconnu',
|
||||||
|
type: ContainerType.flightCase,
|
||||||
|
status: EquipmentStatus.available,
|
||||||
|
equipmentIds: [],
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
_containerCache[containerId] = container;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Erreur silencieuse - le cache restera vide
|
DebugLog.error('[EventAssignedEquipmentSection] Error loading equipment and containers', e);
|
||||||
} finally {
|
} finally {
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
@@ -156,6 +207,26 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
|
|
||||||
DebugLog.info('[EventAssignedEquipmentSection] Found ${newEquipment.length} equipment(s) and ${newContainers.length} container(s)');
|
DebugLog.info('[EventAssignedEquipmentSection] Found ${newEquipment.length} equipment(s) and ${newContainers.length} container(s)');
|
||||||
|
|
||||||
|
// 🔧 FIX: Pour chaque container sélectionné, ajouter aussi ses équipements enfants
|
||||||
|
if (newContainers.isNotEmpty) {
|
||||||
|
final containerProvider = context.read<ContainerProvider>();
|
||||||
|
final containers = await containerProvider.getContainersByIds(newContainers);
|
||||||
|
|
||||||
|
for (var container in containers) {
|
||||||
|
for (var childEquipmentId in container.equipmentIds) {
|
||||||
|
// Vérifier si l'équipement enfant n'est pas déjà dans la liste
|
||||||
|
final existsInNew = newEquipment.any((eq) => eq.equipmentId == childEquipmentId);
|
||||||
|
if (!existsInNew) {
|
||||||
|
newEquipment.add(EventEquipment(
|
||||||
|
equipmentId: childEquipmentId,
|
||||||
|
quantity: 1,
|
||||||
|
));
|
||||||
|
DebugLog.info('[EventAssignedEquipmentSection] Adding child equipment $childEquipmentId from container ${container.id}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ✅ Pas de vérification de conflits : déjà fait dans le pop-up
|
// ✅ Pas de vérification de conflits : déjà fait dans le pop-up
|
||||||
// On enregistre directement la sélection
|
// On enregistre directement la sélection
|
||||||
|
|
||||||
@@ -217,25 +288,47 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
.where((id) => id != containerId)
|
.where((id) => id != containerId)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Retirer les équipements enfants de la liste des équipements assignés
|
// 🔧 FIX: Ne supprimer les équipements enfants QUE s'ils ne sont pas dans un autre container
|
||||||
final updatedEquipment = widget.assignedEquipment.where((eq) {
|
final updatedEquipment = <EventEquipment>[];
|
||||||
if (container != null) {
|
|
||||||
// Garder uniquement les équipements qui ne sont PAS dans ce conteneur
|
|
||||||
return !container.equipmentIds.contains(eq.equipmentId);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
|
if (container != null) {
|
||||||
|
// Collecter les IDs d'équipements dans les autres containers
|
||||||
|
final Set<String> equipmentIdsInOtherContainers = {};
|
||||||
|
for (var otherContainerId in updatedContainers) {
|
||||||
|
final otherContainer = _containerCache[otherContainerId];
|
||||||
|
if (otherContainer != null) {
|
||||||
|
equipmentIdsInOtherContainers.addAll(otherContainer.equipmentIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Garder les équipements qui :
|
||||||
|
// 1. Ne sont PAS dans le container supprimé OU
|
||||||
|
// 2. Sont dans le container supprimé MAIS aussi dans un autre container
|
||||||
|
for (var eq in widget.assignedEquipment) {
|
||||||
|
final isInRemovedContainer = container.equipmentIds.contains(eq.equipmentId);
|
||||||
|
final isInOtherContainer = equipmentIdsInOtherContainers.contains(eq.equipmentId);
|
||||||
|
|
||||||
|
if (!isInRemovedContainer || isInOtherContainer) {
|
||||||
|
updatedEquipment.add(eq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Si le container n'est pas dans le cache, garder tous les équipements
|
||||||
|
updatedEquipment.addAll(widget.assignedEquipment);
|
||||||
|
}
|
||||||
|
|
||||||
// Notifier le changement avec les deux listes mises à jour
|
// Notifier le changement avec les deux listes mises à jour
|
||||||
widget.onChanged(updatedEquipment, updatedContainers);
|
widget.onChanged(updatedEquipment, updatedContainers);
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_containerCache.remove(containerId);
|
_containerCache.remove(containerId);
|
||||||
// Retirer aussi les équipements enfants du cache
|
// Nettoyer le cache uniquement pour les équipements effectivement supprimés
|
||||||
if (container != null) {
|
if (container != null) {
|
||||||
|
final remainingEquipmentIds = updatedEquipment.map((eq) => eq.equipmentId).toSet();
|
||||||
for (var equipmentId in container.equipmentIds) {
|
for (var equipmentId in container.equipmentIds) {
|
||||||
_equipmentCache.remove(equipmentId);
|
if (!remainingEquipmentIds.contains(equipmentId)) {
|
||||||
|
_equipmentCache.remove(equipmentId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -444,79 +537,69 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
|||||||
onPressed: () => _removeContainer(container.id),
|
onPressed: () => _removeContainer(container.id),
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
// Afficher les équipements enfants (par composition)
|
// 🔧 FIX: Utiliser directement le cache local au lieu du provider stream
|
||||||
Consumer<EquipmentProvider>(
|
Builder(
|
||||||
builder: (context, provider, child) {
|
builder: (context) {
|
||||||
return StreamBuilder<List<EquipmentModel>>(
|
// Récupérer les équipements enfants depuis le cache local
|
||||||
stream: provider.equipmentStream,
|
final childEquipments = container.equipmentIds
|
||||||
builder: (context, snapshot) {
|
.map((id) => _equipmentCache[id])
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
.where((eq) => eq != null)
|
||||||
return const Padding(
|
.cast<EquipmentModel>()
|
||||||
padding: EdgeInsets.all(16),
|
.toList();
|
||||||
child: Center(child: CircularProgressIndicator()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final allEquipment = snapshot.data ?? [];
|
if (childEquipments.isEmpty) {
|
||||||
final childEquipments = allEquipment
|
return Padding(
|
||||||
.where((eq) => container.equipmentIds.contains(eq.id))
|
padding: const EdgeInsets.all(16),
|
||||||
.toList();
|
child: Text(
|
||||||
|
'Aucun équipement dans ce conteneur (${container.equipmentIds.length} attendu(s))',
|
||||||
|
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (childEquipments.isEmpty) {
|
return Padding(
|
||||||
return const Padding(
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
padding: EdgeInsets.all(16),
|
child: Column(
|
||||||
child: Text(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
'Aucun équipement dans ce conteneur',
|
children: [
|
||||||
style: TextStyle(color: Colors.grey),
|
Text(
|
||||||
|
'Contenu (${childEquipments.length} équipement(s))',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Contenu (${childEquipments.length} équipement(s))',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
...childEquipments.map((eq) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 4),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Icon(
|
|
||||||
Icons.subdirectory_arrow_right,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
eq.category.getIcon(size: 16, color: eq.category.color),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
eq.id,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: Colors.grey.shade700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
const SizedBox(height: 8),
|
||||||
},
|
...childEquipments.map((eq) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Icon(
|
||||||
|
Icons.subdirectory_arrow_right,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
eq.category.getIcon(size: 16, color: eq.category.color),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
eq.id,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
|
||||||
|
/// États possibles lors de l'ajout d'un équipement
|
||||||
|
enum AddEquipmentState {
|
||||||
|
loading,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dialog pour afficher le résultat de l'ajout d'un équipement/container
|
||||||
|
class AddEquipmentToEventDialog extends StatelessWidget {
|
||||||
|
final AddEquipmentState state;
|
||||||
|
final String? itemName;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
const AddEquipmentToEventDialog({
|
||||||
|
super.key,
|
||||||
|
required this.state,
|
||||||
|
this.itemName,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dialog(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_buildIcon(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildMessage(),
|
||||||
|
if (state != AddEquipmentState.loading) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.rouge,
|
||||||
|
),
|
||||||
|
child: const Text('OK'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildIcon() {
|
||||||
|
switch (state) {
|
||||||
|
case AddEquipmentState.loading:
|
||||||
|
return const SizedBox(
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: AppColors.rouge,
|
||||||
|
strokeWidth: 4,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case AddEquipmentState.success:
|
||||||
|
return const Icon(
|
||||||
|
Icons.check_circle,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.green,
|
||||||
|
);
|
||||||
|
case AddEquipmentState.error:
|
||||||
|
return const Icon(
|
||||||
|
Icons.error,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.red,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMessage() {
|
||||||
|
switch (state) {
|
||||||
|
case AddEquipmentState.loading:
|
||||||
|
return const Text(
|
||||||
|
'Recherche en cours...',
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
);
|
||||||
|
case AddEquipmentState.success:
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Ajouté avec succès !',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
if (itemName != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
itemName!,
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
case AddEquipmentState.error:
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Non trouvé',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
if (errorMessage != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
errorMessage!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
|
||||||
|
/// Dialog affiché quand un code scanné n'est pas trouvé dans l'événement
|
||||||
|
class CodeNotFoundDialog extends StatelessWidget {
|
||||||
|
final String scannedCode;
|
||||||
|
|
||||||
|
const CodeNotFoundDialog({
|
||||||
|
super.key,
|
||||||
|
required this.scannedCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.warning_amber_rounded,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.orange,
|
||||||
|
),
|
||||||
|
title: const Text('Code non reconnu'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Le code scanné n\'est pas assigné à cet événement :',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[200],
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
scannedCode,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
'Voulez-vous le rechercher dans la base de données et l\'ajouter à l\'événement ?',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Non'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: AppColors.rouge),
|
||||||
|
child: const Text('Oui, rechercher'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,6 +58,7 @@ dependencies:
|
|||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
timeago: ^3.6.1
|
timeago: ^3.6.1
|
||||||
|
audioplayers: ^6.1.0
|
||||||
|
|
||||||
path: any
|
path: any
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|||||||
@@ -5,18 +5,23 @@
|
|||||||
* - Bascule en mode PRODUCTION
|
* - Bascule en mode PRODUCTION
|
||||||
* - Incrémente la version
|
* - Incrémente la version
|
||||||
* - Build l'application Flutter pour le web
|
* - Build l'application Flutter pour le web
|
||||||
* - Déploie sur Firebase Hosting
|
* - Vérifie que version.json est bien présent
|
||||||
|
* - Déploie sur Firebase Hosting (avec en-têtes CORS pour version.json)
|
||||||
|
* - Vérifie que version.json est accessible avec CORS
|
||||||
* - Rebascule en mode DÉVELOPPEMENT
|
* - Rebascule en mode DÉVELOPPEMENT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
const { incrementVersion } = require('./increment_version');
|
const { incrementVersion } = require('./increment_version');
|
||||||
const { setProductionMode, setDevelopmentMode } = require('./toggle_env');
|
const { setProductionMode, setDevelopmentMode } = require('./toggle_env');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
console.log('🚀 Démarrage du déploiement Firebase Hosting...\n');
|
console.log('🚀 Démarrage du déploiement Firebase Hosting...\n');
|
||||||
|
|
||||||
// Étape 0: Basculer en mode production
|
// Étape 0: Basculer en mode production
|
||||||
console.log('🔒 Étape 0/4: Basculement en mode PRODUCTION');
|
console.log('🔒 Étape 0/5: Basculement en mode PRODUCTION');
|
||||||
if (!setProductionMode()) {
|
if (!setProductionMode()) {
|
||||||
console.error('❌ Impossible de basculer en mode production');
|
console.error('❌ Impossible de basculer en mode production');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -24,12 +29,12 @@ if (!setProductionMode()) {
|
|||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Étape 1: Incrémenter la version
|
// Étape 1: Incrémenter la version
|
||||||
console.log('📝 Étape 1/4: Incrémentation de la version');
|
console.log('📝 Étape 1/5: Incrémentation de la version');
|
||||||
const newVersion = incrementVersion();
|
const newVersion = incrementVersion();
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Étape 2: Build Flutter pour le web
|
// Étape 2: Build Flutter pour le web
|
||||||
console.log('🔨 Étape 2/4: Build Flutter Web');
|
console.log('🔨 Étape 2/5: Build Flutter Web');
|
||||||
try {
|
try {
|
||||||
execSync('flutter build web --release', {
|
execSync('flutter build web --release', {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
@@ -43,9 +48,42 @@ try {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Étape 3: Déploiement Firebase
|
// Étape 2.5: Vérifier que version.json est bien présent dans build/web
|
||||||
console.log('🌐 Étape 3/4: Déploiement sur Firebase Hosting');
|
console.log('🔍 Étape 2.5/5: Vérification de version.json');
|
||||||
|
const versionJsonPath = path.join(process.cwd(), 'build', 'web', 'version.json');
|
||||||
|
if (!fs.existsSync(versionJsonPath)) {
|
||||||
|
console.warn('⚠️ version.json n\'a pas été copié dans build/web/');
|
||||||
|
|
||||||
|
// Copier manuellement depuis web/version.json
|
||||||
|
const sourceVersionJsonPath = path.join(process.cwd(), 'web', 'version.json');
|
||||||
|
if (fs.existsSync(sourceVersionJsonPath)) {
|
||||||
|
console.log(' → Copie de web/version.json vers build/web/...');
|
||||||
|
fs.copyFileSync(sourceVersionJsonPath, versionJsonPath);
|
||||||
|
console.log('✅ Fichier version.json copié avec succès');
|
||||||
|
} else {
|
||||||
|
console.error('❌ Impossible de trouver web/version.json');
|
||||||
|
setDevelopmentMode();
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('✅ version.json est présent dans build/web/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Afficher la version qui va être déployée
|
||||||
try {
|
try {
|
||||||
|
const versionContent = JSON.parse(fs.readFileSync(versionJsonPath, 'utf8'));
|
||||||
|
console.log(` 📦 Version: ${versionContent.version}`);
|
||||||
|
console.log(` 🔒 Force update: ${versionContent.forceUpdate}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Impossible de lire version.json');
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Étape 3: Déploiement Firebase
|
||||||
|
console.log('🌐 Étape 3/5: Déploiement sur Firebase Hosting');
|
||||||
|
console.log(' ℹ️ Les en-têtes CORS pour version.json seront appliqués automatiquement');
|
||||||
|
try {
|
||||||
|
|
||||||
execSync('firebase deploy --only hosting', {
|
execSync('firebase deploy --only hosting', {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
cwd: process.cwd()
|
cwd: process.cwd()
|
||||||
@@ -59,8 +97,48 @@ try {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Étape 4: Rebascule en mode développement
|
// Étape 4: Vérifier que version.json est accessible avec CORS
|
||||||
console.log('\n🔓 Étape 4/4: Retour en mode DÉVELOPPEMENT');
|
console.log('\n🔍 Étape 4/5: Vérification de l\'accès à version.json');
|
||||||
|
setTimeout(() => {
|
||||||
|
https.get('https://app.em2events.fr/version.json', {
|
||||||
|
headers: {
|
||||||
|
'Origin': 'http://localhost'
|
||||||
|
}
|
||||||
|
}, (res) => {
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
console.log('✅ version.json est accessible (statut 200)');
|
||||||
|
|
||||||
|
// Vérifier les en-têtes CORS
|
||||||
|
const corsHeader = res.headers['access-control-allow-origin'];
|
||||||
|
if (corsHeader) {
|
||||||
|
console.log(`✅ En-têtes CORS configurés: ${corsHeader}`);
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ En-têtes CORS non détectés (peuvent prendre quelques minutes pour se propager)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lire et afficher la version déployée
|
||||||
|
let body = '';
|
||||||
|
res.on('data', chunk => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const deployed = JSON.parse(body);
|
||||||
|
console.log(`📦 Version déployée: ${deployed.version}`);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ Statut HTTP: ${res.statusCode}`);
|
||||||
|
}
|
||||||
|
}).on('error', (err) => {
|
||||||
|
console.warn('⚠️ Impossible de vérifier l\'accès à version.json');
|
||||||
|
console.warn(` ${err.message}`);
|
||||||
|
console.warn(' Le fichier peut prendre quelques minutes pour être accessible');
|
||||||
|
});
|
||||||
|
}, 2000); // Attendre 2 secondes pour que le déploiement se propage
|
||||||
|
|
||||||
|
// Étape 5: Rebascule en mode développement
|
||||||
|
console.log('\n🔓 Étape 5/5: Retour en mode DÉVELOPPEMENT');
|
||||||
if (!setDevelopmentMode()) {
|
if (!setDevelopmentMode()) {
|
||||||
console.warn('⚠️ Impossible de rebascule en mode développement');
|
console.warn('⚠️ Impossible de rebascule en mode développement');
|
||||||
console.warn('⚠️ Exécutez manuellement: npm run env:dev');
|
console.warn('⚠️ Exécutez manuellement: npm run env:dev');
|
||||||
@@ -69,3 +147,4 @@ if (!setDevelopmentMode()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n✨ Processus de déploiement terminé!');
|
console.log('\n✨ Processus de déploiement terminé!');
|
||||||
|
console.log('📝 Les utilisateurs recevront une notification de mise à jour au prochain chargement.');
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.4",
|
"version": "1.0.6",
|
||||||
"updateUrl": "https://app.em2events.fr",
|
"updateUrl": "https://app.em2events.fr",
|
||||||
"forceUpdate": true,
|
"forceUpdate": true,
|
||||||
"releaseNotes": "Cette version apporte des outils majeurs pour faciliter la gestion de votre parc et de vos événements :\r\n\r\n* **Scanner QR Code :** Retrouvez instantanément la fiche d'un équipement ou d'un conteneur en scannant son code directement depuis l'application. La génération des codes a également été rendue plus fluide.\r\n* **Centre de Notifications & Alertes :** Ne ratez plus rien ! Un nouveau système d'alertes (dans l'app et par email) vous prévient des maintenances, équipements manquants ou conflits. Vous pouvez configurer vos préférences d'envoi.",
|
"releaseNotes": "Cette version apporte des outils majeurs pour faciliter la gestion de votre parc et de vos événements :\r\n\r\n* **Scanner QR Code :** Retrouvez instantanément la fiche d'un équipement ou d'un conteneur en scannant son code directement depuis l'application. La génération des codes a également été rendue plus fluide.\r\n* **Centre de Notifications & Alertes :** Ne ratez plus rien ! Un nouveau système d'alertes (dans l'app et par email) vous prévient des maintenances, équipements manquants ou conflits. Vous pouvez configurer vos préférences d'envoi.",
|
||||||
"timestamp": "2026-01-16T17:56:48.878Z"
|
"timestamp": "2026-01-18T12:09:32.899Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user