feat: Intégration d'un système complet d'alertes et de notifications par email
Cette mise à jour majeure introduit un système de notifications robuste, centré sur la création d'alertes et l'envoi d'emails via des Cloud Functions. Elle inclut la gestion des préférences utilisateur, la création automatique d'alertes lors d'événements critiques et une nouvelle interface dédiée.
**Backend (Cloud Functions) :**
- **Nouveau service d'alerting (`createAlert`, `processEquipmentValidation`) :**
- `createAlert` : Nouvelle fonction pour créer une alerte. Elle détermine les utilisateurs à notifier (admins, workforce d'événement) et gère la persistance dans Firestore.
- `processEquipmentValidation` : Endpoint appelé lors de la validation du matériel (chargement/déchargement). Il analyse l'état de l'équipement (`LOST`, `MISSING`, `DAMAGED`) et crée automatiquement les alertes correspondantes.
- **Système d'envoi d'emails (`sendAlertEmail`, `sendDailyDigest`) :**
- `sendAlertEmail` : Cloud Function `onCall` pour envoyer un email d'alerte individuel. Elle respecte les préférences de notification de l'utilisateur (canal email, type d'alerte).
- `sendDailyDigest` : Tâche planifiée (tous les jours à 8h) qui envoie un email récapitulatif des alertes non lues des dernières 24 heures aux utilisateurs concernés.
- Ajout de templates HTML (`base-template`, `alert-individual`, `alert-digest`) avec `Handlebars` pour des emails riches.
- Configuration centralisée du SMTP via des variables d'environnement (`.env`).
- **Triggers Firestore (`onEventCreated`, `onEventUpdated`) :**
- Des triggers créent désormais des alertes d'information lorsqu'un événement est créé ou que de nouveaux membres sont ajoutés à la workforce.
- **Règles Firestore :**
- Mises à jour pour autoriser les utilisateurs authentifiés à créer et modifier leurs propres alertes (marquer comme lue, supprimer), tout en sécurisant les accès.
**Frontend (Flutter) :**
- **Nouvel `AlertService` et `EmailService` :**
- `AlertService` : Centralise la logique de création, lecture et gestion des alertes côté client en appelant les nouvelles Cloud Functions.
- `EmailService` : Service pour déclencher l'envoi d'emails via la fonction `sendAlertEmail`. Il contient la logique pour déterminer si une notification doit être immédiate (critique) ou différée (digest).
- **Nouvelle page de Notifications (`/alerts`) :**
- Interface dédiée pour lister toutes les alertes de l'utilisateur, avec des onglets pour filtrer par catégorie (Toutes, Événement, Maintenance, Équipement).
- Permet de marquer les alertes comme lues, de les supprimer et de tout marquer comme lu.
- **Intégration dans l'UI :**
- Ajout d'un badge de notification dans la `CustomAppBar` affichant le nombre d'alertes non lues en temps réel.
- Le `AutoLoginWrapper` gère désormais la redirection vers des routes profondes (ex: `/alerts`) depuis une URL.
- **Gestion des Préférences de Notification :**
- Ajout d'un widget `NotificationPreferencesWidget` dans la page "Mon Compte".
- Les utilisateurs peuvent désormais activer/désactiver les notifications par email, ainsi que filtrer par type d'alerte (événements, maintenance, etc.).
- Le `UserModel` et `LocalUserProvider` ont été étendus pour gérer ce nouveau modèle de préférences.
- **Création d'alertes contextuelles :**
- Le service `EventFormService` crée maintenant automatiquement une alerte lorsqu'un événement est créé ou modifié.
- La page de préparation d'événement (`EventPreparationPage`) appelle `processEquipmentValidation` à la fin de chaque étape pour une détection automatisée des anomalies.
**Dépendances et CI/CD :**
- Ajout des dépendances `cloud_functions` et `timeago` (Flutter), et `nodemailer`, `handlebars`, `dotenv` (Node.js).
- Ajout de scripts de déploiement PowerShell (`deploy_functions.ps1`, `deploy_firestore_rules.ps1`) pour simplifier les mises en production.
This commit is contained in:
47
em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache
Normal file
47
em2rp/.firebase/hosting.YnVpbGRcd2Vi.cache
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
manifest.json,1766235870190,1fb17c7a1d11e0160d9ffe48e4e4f7fb5028d23477915a17ca496083050946e2
|
||||||
|
flutter.js,1759914809272,d9a92a27a30723981b176a08293dedbe86c080fcc08e0128e5f8a01ce1d3fcb4
|
||||||
|
favicon.png,1766235850956,3cf717d02cd8014f223307dee1bde538442eb9de23568e649fd8aae686dc9db0
|
||||||
|
favicon.jpg,1741031103452,687267bbd4e1a663ffc1d2256c34969dd424cbaaf503b530db529a345570ddcd
|
||||||
|
icons/Icon-maskable-512.png,1766235851206,adeda24772174dad916236f9385d1deaa05da836521af74912a11d217a3e18de
|
||||||
|
icons/Icon-maskable-192.png,1766235851135,fedfe0abc624a28f241f7f8e06ceab04c6c88a500290078410e1a7d12089952a
|
||||||
|
icons/Icon-512.png,1766235851087,adeda24772174dad916236f9385d1deaa05da836521af74912a11d217a3e18de
|
||||||
|
icons/Icon-192.png,1766235851013,fedfe0abc624a28f241f7f8e06ceab04c6c88a500290078410e1a7d12089952a
|
||||||
|
canvaskit/skwasm_heavy.wasm,1759914809247,509ac05ee7c60aaee61d52bad4527f40e1ce79511ca29908237472a1cd476180
|
||||||
|
canvaskit/skwasm_heavy.js.symbols,1759914809219,612ffa6a568de0500758c132cd0ea7d7c4f389157d618fe2b4255e73f3068e8f
|
||||||
|
canvaskit/skwasm_heavy.js,1759914809214,5552644d0313045f87d52097dd1e86a75f64b9e048a450ce2c885e313ed1b4c5
|
||||||
|
canvaskit/skwasm.wasm,1759914809212,85c6ff573c3f76f2d84f5553fab09bf0d0f715519c679f7626722ac0fb501640
|
||||||
|
canvaskit/skwasm.js.symbols,1759914809190,83718024df2bd4902e4c0fdfa47ea7e9ca401dcf7f31f4061c6da8478f12987f
|
||||||
|
canvaskit/skwasm.js,1759914809185,2e251855d712f083d8c6aa79bf49f6d2a8e15311f161115eb8a39bcf0688c878
|
||||||
|
canvaskit/canvaskit.wasm,1759914809134,52dedf2cd2d6bf150262bf145ffde2fc80e296d98a9d3764961eb6f84c8ce988
|
||||||
|
canvaskit/canvaskit.js.symbols,1759914809092,a3577bf24071e07f599ac61535dbee4ae4d37c5cc6ee6289379576773f9c336b
|
||||||
|
canvaskit/canvaskit.js,1759914809082,bb9141a62dec1f0a41e311b845569915df9ebb5f074dd2afc181f26b323d2dd1
|
||||||
|
canvaskit/chromium/canvaskit.wasm,1759914809184,4a868d7961a9740ae6694f62fc15b2b0ed76df50598e8311d61e8ee814d78229
|
||||||
|
canvaskit/chromium/canvaskit.js.symbols,1759914809141,f395278c466a0eaed0201edd6b14a3aa8fee0a16bfedee2d239835cd7e865472
|
||||||
|
canvaskit/chromium/canvaskit.js,1759914809136,ce5184f74e2501d849490df34d0506167a0708b9120be088039b785343335664
|
||||||
|
assets/packages/flutter_map/lib/assets/flutter_map_logo.png,1759916249804,26fe50c9203ccf93512b80d4ee1a7578184a910457b36a6a5b7d41b799efb966
|
||||||
|
assets/packages/flutter_dropzone_web/assets/flutter_dropzone.js,1748366257688,d640313cd6a02692249cd41e4643c2771b4202cc84e0f07f5f65cdc77a36826f
|
||||||
|
assets/assets/Google__G__logo.svg,1741027482182,b365d560438f8f04caf08ffaa5d8995eff6c09b36b4483f44d6f5f82d8559d4f
|
||||||
|
assets/assets/google.png,1741029771653,537ca60ffa74c28eca4e62d249237403a7d47d2bc90bb90431b8d5aa923a92ee
|
||||||
|
assets/assets/EM2_NsurB.jpg,1741031103452,687267bbd4e1a663ffc1d2256c34969dd424cbaaf503b530db529a345570ddcd
|
||||||
|
assets/assets/logos/SquareLogoWhite.png,1760462340000,786ce2571303bb96dfae1fba5faaab57a9142468fa29ad73ab6b3c1f75be3703
|
||||||
|
assets/assets/logos/SquareLogoBlack.png,1760462340000,b4425fae1dbd25ce7c218c602d530f75d85e0eb444746b48b09b5028ed88bbd1
|
||||||
|
assets/assets/logos/RectangleLogoWhite.png,1760462340000,1f6df22df6560a2dae2d42cf6e29f01e6df4002f1a9c20a8499923d74b02115c
|
||||||
|
assets/assets/logos/RectangleLogoBlack.png,1760462340000,536ebd370e55736b3622a673c684a150e23f5d3b82c71283d7a3f4a93564c02c
|
||||||
|
assets/assets/logos/LowQRectangleLogoBlack.png,1761139425319,ae4f8e428dd3634a14b45421a3c9b30fea8592ff33ff21f6962ed548e7db242b
|
||||||
|
assets/assets/images/tshirt-incrust.webp,1737393735487,af7cb34adfca19c0b41c8eb6399444016c67afe9e223fddf4ecdac1dad822198
|
||||||
|
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
|
||||||
|
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
||||||
|
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
||||||
|
version.json,1768397512647,ef03fbf6fee5e0631f357db9c08c049058ba45d629b1eaed3a69bec4200d8189
|
||||||
|
index.html,1768397340273,4375d2aa848ac5af68053b56474f2d82bc054264d0fa528d5f535dab7b678836
|
||||||
|
flutter_service_worker.js,1768397521163,40d38b159dc58d99e909ac7e9fa25df38b12dfe56ed3c8743905a8ffde7e2718
|
||||||
|
flutter_bootstrap.js,1768397340272,d3a780bc468e1a267eb4b890677be070f210b07e33f3cae086abbd5bb6c962e5
|
||||||
|
assets/FontManifest.json,1768397515663,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
||||||
|
assets/AssetManifest.json,1768397515664,1e1501af5844823ef215cf650f4cef4002c0389d88770225ac07576d57dc1067
|
||||||
|
assets/AssetManifest.bin.json,1768397515663,f446eb3de964f3a6f9e76fcc98d79a81b0429e076c9c7bf30cf8edd0263a0b0a
|
||||||
|
assets/AssetManifest.bin,1768397515663,72bbccb69d9a02d3885df0c5e58ebfed29e25a4919e10bf195b59542f4709ca3
|
||||||
|
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1768397520319,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
||||||
|
assets/shaders/ink_sparkle.frag,1768397516013,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
||||||
|
assets/fonts/MaterialIcons-Regular.otf,1768397520329,5539d621f88691414a2b6fbfedf34c3e12dc2c75c1238759301276d99a38a6ef
|
||||||
|
assets/NOTICES,1768397515664,22a160781d4d3cf3f76d93e69d71c5368d8976bbba609788e8f17d302080d47e
|
||||||
|
main.dart.js,1768397509656,352b1d4014b31defb1a0b24d098b2dcd943fd255912764e1af1f38459b02f444
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
# ✅ CALENDRIER RESTAURÉ - Version Finale
|
|
||||||
|
|
||||||
## 🎯 Problèmes Résolus
|
|
||||||
|
|
||||||
### 1. Design Non Standard
|
|
||||||
**Avant** : AppBar générique sans les composants de l'app
|
|
||||||
**Après** : ✅ `CustomAppBar` standard (bandeau rouge) restauré
|
|
||||||
|
|
||||||
### 2. Bouton d'Ajout Manquant
|
|
||||||
**Avant** : Pas de FloatingActionButton
|
|
||||||
**Après** : ✅ Bouton "+" blanc en bas à droite restauré
|
|
||||||
|
|
||||||
### 3. Filtre Utilisateur Mal Placé
|
|
||||||
**Avant** : Dans l'AppBar (causait des exceptions)
|
|
||||||
**Après** : ✅ **Dans le corps de la page calendrier**
|
|
||||||
|
|
||||||
## 🎨 Design Final Implémenté
|
|
||||||
|
|
||||||
### Structure de la Page
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────────────────────────────────┐
|
|
||||||
│ 📅 Calendrier [🚪 Logout] │ ← CustomAppBar (rouge)
|
|
||||||
├──────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ 🔍 Filtrer par utilisateur : [Dropdown] │ ← Filtre dans le body
|
|
||||||
│ │
|
|
||||||
├──────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ 📆 Vue Calendrier │
|
|
||||||
│ │
|
|
||||||
│ │
|
|
||||||
└──────────────────────────────────────────┘
|
|
||||||
[+] ← FloatingActionButton
|
|
||||||
```
|
|
||||||
|
|
||||||
### Filtre Utilisateur
|
|
||||||
|
|
||||||
**Emplacement** : Container au-dessus du calendrier (dans le body)
|
|
||||||
|
|
||||||
**Apparence** :
|
|
||||||
- Fond gris clair (`Colors.grey[100]`)
|
|
||||||
- Padding de 16px
|
|
||||||
- Icône filtre rouge
|
|
||||||
- Label "Filtrer par utilisateur :"
|
|
||||||
- Dropdown UserFilterDropdown
|
|
||||||
|
|
||||||
**Visibilité** :
|
|
||||||
- ✅ Visible si `view_all_user_events` permission
|
|
||||||
- ✅ Masqué sur mobile
|
|
||||||
- ✅ Charge après le premier frame (évite setState pendant build)
|
|
||||||
|
|
||||||
## 🔧 Modifications Techniques
|
|
||||||
|
|
||||||
### Fichier : `lib/views/calendar_page.dart`
|
|
||||||
|
|
||||||
#### 1. Imports Restaurés
|
|
||||||
```dart
|
|
||||||
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart'; // ✅ Restauré
|
|
||||||
import 'package:em2rp/views/widgets/calendar_widgets/user_filter_dropdown.dart'; // ✅ Réactivé
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Structure du Scaffold
|
|
||||||
```dart
|
|
||||||
Scaffold(
|
|
||||||
appBar: CustomAppBar(title: "Calendrier"), // ✅ Composant standard
|
|
||||||
drawer: MainDrawer(...),
|
|
||||||
body: Column([
|
|
||||||
if (canViewAllUserEvents && !isMobile)
|
|
||||||
Container(...), // ✅ Filtre dans le body
|
|
||||||
Expanded(
|
|
||||||
child: _buildMobileLayout(filteredEvents) // ✅ Calendrier avec filtrage
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
floatingActionButton: FloatingActionButton(...), // ✅ Bouton + restauré
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Méthodes Modifiées
|
|
||||||
|
|
||||||
**`_buildDesktopLayout(filteredEvents)`**
|
|
||||||
- Accepte maintenant `filteredEvents` en paramètre
|
|
||||||
- Passe les événements filtrés à tous les widgets enfants
|
|
||||||
|
|
||||||
**`_buildMobileLayout(filteredEvents)`**
|
|
||||||
- Accepte maintenant `filteredEvents` en paramètre
|
|
||||||
- Utilise filteredEvents au lieu d'eventProvider.events
|
|
||||||
|
|
||||||
**`_buildCalendar(filteredEvents)`**
|
|
||||||
- Accepte maintenant `filteredEvents` en paramètre
|
|
||||||
- Passe aux WeekView et MonthView
|
|
||||||
|
|
||||||
#### 4. Filtrage Actif
|
|
||||||
```dart
|
|
||||||
final filteredEvents = _getFilteredEvents(eventProvider.events);
|
|
||||||
```
|
|
||||||
|
|
||||||
Appliqué à :
|
|
||||||
- ✅ EventDetails
|
|
||||||
- ✅ MonthView
|
|
||||||
- ✅ WeekView
|
|
||||||
- ✅ MobileCalendarView
|
|
||||||
- ✅ Toutes les listes d'événements
|
|
||||||
|
|
||||||
## ✅ Ce qui Fonctionne Maintenant
|
|
||||||
|
|
||||||
### Interface
|
|
||||||
- ✅ **Bandeau rouge CustomAppBar** avec logout
|
|
||||||
- ✅ **Menu drawer** accessible
|
|
||||||
- ✅ **Bouton "+" blanc** pour ajouter un événement
|
|
||||||
- ✅ **Filtre utilisateur** visible et fonctionnel (si permission)
|
|
||||||
|
|
||||||
### Fonctionnalités
|
|
||||||
- ✅ **Filtrage par utilisateur** opérationnel
|
|
||||||
- ✅ **Changement de vue** (mois/semaine)
|
|
||||||
- ✅ **Sélection d'événement** fonctionne
|
|
||||||
- ✅ **Navigation** entre les mois
|
|
||||||
- ✅ **Détails d'événement** s'affichent correctement
|
|
||||||
|
|
||||||
### Technique
|
|
||||||
- ✅ **0 erreur de compilation**
|
|
||||||
- ✅ **0 exception au runtime** (setState corrigé)
|
|
||||||
- ✅ **Code cohérent** avec le reste de l'app
|
|
||||||
- ✅ **Composants réutilisés** (CustomAppBar, UserFilterDropdown)
|
|
||||||
|
|
||||||
## 🧪 Tests à Effectuer
|
|
||||||
|
|
||||||
### 1. Apparence
|
|
||||||
- [ ] Vérifier le bandeau rouge en haut
|
|
||||||
- [ ] Vérifier le bouton logout à droite
|
|
||||||
- [ ] Vérifier le filtre utilisateur (si admin)
|
|
||||||
- [ ] Vérifier le bouton "+" en bas à droite
|
|
||||||
|
|
||||||
### 2. Filtre Utilisateur
|
|
||||||
- [ ] Le dropdown charge correctement les utilisateurs
|
|
||||||
- [ ] La sélection d'un utilisateur filtre les événements
|
|
||||||
- [ ] "Tous les utilisateurs" réinitialise le filtre
|
|
||||||
- [ ] Pas d'exception dans la console
|
|
||||||
|
|
||||||
### 3. Navigation
|
|
||||||
- [ ] Changer de mois fonctionne
|
|
||||||
- [ ] Changer de vue (mois ↔ semaine) fonctionne
|
|
||||||
- [ ] Cliquer sur un jour sélectionne ce jour
|
|
||||||
- [ ] Cliquer sur un événement affiche ses détails
|
|
||||||
|
|
||||||
### 4. Création d'Événement
|
|
||||||
- [ ] Cliquer sur "+" ouvre le formulaire
|
|
||||||
- [ ] Les prix HT/TTC fonctionnent correctement
|
|
||||||
- [ ] L'événement créé apparaît dans le calendrier
|
|
||||||
|
|
||||||
## 📊 Comparaison Avant/Après
|
|
||||||
|
|
||||||
| Aspect | Avant | Après |
|
|
||||||
|--------|-------|-------|
|
|
||||||
| AppBar | ❌ AppBar générique | ✅ CustomAppBar standard |
|
|
||||||
| Bouton + | ❌ Manquant | ✅ FloatingActionButton restauré |
|
|
||||||
| Filtre | ❌ Dans AppBar (bugué) | ✅ Dans le body (propre) |
|
|
||||||
| Exceptions | ❌ setState pendant build | ✅ Aucune exception |
|
|
||||||
| Composants | ❌ Mélange générique/custom | ✅ 100% composants de l'app |
|
|
||||||
|
|
||||||
## ⚠️ Note Importante
|
|
||||||
|
|
||||||
Le filtre utilisateur nécessite toujours que :
|
|
||||||
1. La permission `view_all_user_events` existe dans Firestore
|
|
||||||
2. L'utilisateur ait cette permission dans son rôle
|
|
||||||
|
|
||||||
Si la permission n'existe pas, le filtre ne s'affiche simplement pas (comportement normal).
|
|
||||||
|
|
||||||
## 🎉 Résultat Final
|
|
||||||
|
|
||||||
Le calendrier est maintenant **complètement fonctionnel** avec :
|
|
||||||
- ✅ Design cohérent avec l'application
|
|
||||||
- ✅ Tous les boutons et fonctionnalités restaurés
|
|
||||||
- ✅ Filtre utilisateur proprement intégré
|
|
||||||
- ✅ Code propre sans exceptions
|
|
||||||
- ✅ Prêt pour la production
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Date** : 2026-01-14
|
|
||||||
**Status** : ✅ FONCTIONNEL
|
|
||||||
**Prêt à utiliser** : OUI
|
|
||||||
|
|
||||||
32
em2rp/deploy_alert_corrections.ps1
Normal file
32
em2rp/deploy_alert_corrections.ps1
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
# Script de déploiement rapide - Corrections Alertes
|
||||||
|
|
||||||
|
Write-Host "=== DÉPLOIEMENT CORRECTIONS ALERTES ===" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# 1. Hot restart Flutter (si app en cours)
|
||||||
|
Write-Host "1. Hot restart recommandé (R dans le terminal Flutter)" -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# 2. Pub get
|
||||||
|
Write-Host "2. Installation des dépendances..." -ForegroundColor Yellow
|
||||||
|
flutter pub get
|
||||||
|
|
||||||
|
# 3. Optionnel : Redéployer les fonctions si besoin
|
||||||
|
# Décommentez si vous avez modifié les Cloud Functions
|
||||||
|
# Write-Host "3. Déploiement Cloud Functions..." -ForegroundColor Yellow
|
||||||
|
# firebase deploy --only functions:sendAlertEmail
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== DÉPLOIEMENT TERMINÉ ===" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "PROCHAINES ÉTAPES:" -ForegroundColor Cyan
|
||||||
|
Write-Host "1. Hot restart de l'application (R dans terminal Flutter)"
|
||||||
|
Write-Host "2. Vérifier que vous êtes connecté"
|
||||||
|
Write-Host "3. Créer un événement de test avec workforce"
|
||||||
|
Write-Host "4. Créer une alerte LOST (équipement perdu)"
|
||||||
|
Write-Host "5. Vérifier les logs (F12 → Console)"
|
||||||
|
Write-Host "6. Vérifier Firestore (Firebase Console)"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Voir CORRECTIONS_ALERTES_CIBLAGE.md pour détails" -ForegroundColor Yellow
|
||||||
|
|
||||||
25
em2rp/deploy_alert_trigger.ps1
Normal file
25
em2rp/deploy_alert_trigger.ps1
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Script de déploiement de la fonction onAlertCreated
|
||||||
|
Write-Host "=== Déploiement de onAlertCreated ===" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Vérifier que nous sommes dans le bon répertoire
|
||||||
|
$currentPath = Get-Location
|
||||||
|
if ($currentPath.Path -notlike "*\em2rp") {
|
||||||
|
Write-Host "ERREUR: Ce script doit être exécuté depuis le répertoire em2rp" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# S'assurer qu'on utilise le bon projet
|
||||||
|
Write-Host "`nVérification du projet Firebase..." -ForegroundColor Yellow
|
||||||
|
firebase use em2rp-951dc
|
||||||
|
|
||||||
|
# Déployer la fonction
|
||||||
|
Write-Host "`nDéploiement de la fonction..." -ForegroundColor Yellow
|
||||||
|
firebase deploy --only functions:onAlertCreated
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host "`nDéploiement réussi!" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "`nÉchec du déploiement" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
85
em2rp/deploy_firestore_rules.ps1
Normal file
85
em2rp/deploy_firestore_rules.ps1
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Script de déploiement des règles Firestore
|
||||||
|
# Date : 15/01/2026
|
||||||
|
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " DÉPLOIEMENT RÈGLES FIRESTORE" -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Vérifier que Firebase CLI est installé
|
||||||
|
Write-Host "Vérification Firebase CLI..." -ForegroundColor Yellow
|
||||||
|
$firebaseCmd = Get-Command firebase -ErrorAction SilentlyContinue
|
||||||
|
if ($null -eq $firebaseCmd) {
|
||||||
|
Write-Host "❌ Firebase CLI n'est pas installé !" -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Installation requise :" -ForegroundColor Yellow
|
||||||
|
Write-Host " npm install -g firebase-tools" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "OU copier-coller manuellement dans Console Firebase" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "✓ Firebase CLI trouvé" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Vérifier que le fichier firestore.rules existe
|
||||||
|
if (-Not (Test-Path "firestore.rules")) {
|
||||||
|
Write-Host "❌ Fichier firestore.rules introuvable !" -ForegroundColor Red
|
||||||
|
Write-Host "Vérifiez que vous êtes dans le bon répertoire" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "✓ Fichier firestore.rules trouvé" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Afficher un aperçu des règles pour les alertes
|
||||||
|
Write-Host "Règles à déployer (extrait) :" -ForegroundColor Yellow
|
||||||
|
Write-Host "------------------------------" -ForegroundColor Gray
|
||||||
|
Get-Content "firestore.rules" | Select-String -Pattern "alerts" -Context 3 | Select-Object -First 10
|
||||||
|
Write-Host "------------------------------" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Demander confirmation
|
||||||
|
Write-Host "Déployer les règles Firestore ? (O/N)" -ForegroundColor Yellow -NoNewline
|
||||||
|
Write-Host " " -NoNewline
|
||||||
|
$confirmation = Read-Host
|
||||||
|
|
||||||
|
if ($confirmation -ne "O" -and $confirmation -ne "o") {
|
||||||
|
Write-Host "Déploiement annulé" -ForegroundColor Yellow
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Déploiement en cours..." -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# Déployer les règles
|
||||||
|
try {
|
||||||
|
firebase deploy --only firestore:rules
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Green
|
||||||
|
Write-Host " ✅ DÉPLOIEMENT RÉUSSI !" -ForegroundColor Green
|
||||||
|
Write-Host "========================================" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Les règles Firestore ont été déployées avec succès." -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Prochaines étapes :" -ForegroundColor Yellow
|
||||||
|
Write-Host " 1. Rafraîchir l'application (Ctrl+R)" -ForegroundColor White
|
||||||
|
Write-Host " 2. Créer un événement pour tester" -ForegroundColor White
|
||||||
|
Write-Host " 3. Vérifier qu'aucune erreur permission n'apparaît" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Red
|
||||||
|
Write-Host " ❌ ERREUR DE DÉPLOIEMENT" -ForegroundColor Red
|
||||||
|
Write-Host "========================================" -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Erreur : $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Solutions :" -ForegroundColor Yellow
|
||||||
|
Write-Host " 1. Vérifier connexion : firebase login" -ForegroundColor White
|
||||||
|
Write-Host " 2. Vérifier projet : firebase use" -ForegroundColor White
|
||||||
|
Write-Host " 3. OU déployer via Console Firebase" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
66
em2rp/deploy_functions.ps1
Normal file
66
em2rp/deploy_functions.ps1
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# EM2RP - Déploiement automatique du système d'alertes
|
||||||
|
# Ce script déploie les Cloud Functions et vérifie le déploiement
|
||||||
|
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " EM2RP - Déploiement Cloud Functions " -ForegroundColor Cyan
|
||||||
|
Write-Host "========================================" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Vérifier qu'on est dans le bon répertoire
|
||||||
|
if (-not (Test-Path ".\firebase.json")) {
|
||||||
|
Write-Host "❌ ERREUR: Vous devez lancer ce script depuis C:\src\EM2RP\em2rp\" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Vérifier que le fichier .env existe
|
||||||
|
if (-not (Test-Path ".\functions\.env")) {
|
||||||
|
Write-Host "❌ ERREUR: Le fichier functions\.env est manquant" -ForegroundColor Red
|
||||||
|
Write-Host " Créez ce fichier avec les identifiants SMTP" -ForegroundColor Yellow
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "✅ Vérifications préliminaires OK" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Déployer les fonctions
|
||||||
|
Write-Host "🚀 Déploiement des Cloud Functions en cours..." -ForegroundColor Cyan
|
||||||
|
Write-Host " (Cela peut prendre 3-5 minutes)" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
$deployResult = firebase deploy --only functions 2>&1
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Green
|
||||||
|
Write-Host " ✅ DÉPLOIEMENT RÉUSSI" -ForegroundColor Green
|
||||||
|
Write-Host "========================================" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Lister les fonctions déployées
|
||||||
|
Write-Host "📋 Fonctions déployées:" -ForegroundColor Cyan
|
||||||
|
firebase functions:list
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "🎯 Prochaines étapes:" -ForegroundColor Yellow
|
||||||
|
Write-Host " 1. Migrer les préférences utilisateurs: cd functions; node migrate_email_prefs.js" -ForegroundColor White
|
||||||
|
Write-Host " 2. Tester la création d'un événement avec workforce" -ForegroundColor White
|
||||||
|
Write-Host " 3. Vérifier les logs: firebase functions:log --limit 20" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "📚 Voir DEPLOY_NOW.md pour plus de détails" -ForegroundColor Gray
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "========================================" -ForegroundColor Red
|
||||||
|
Write-Host " ❌ ERREUR DE DÉPLOIEMENT" -ForegroundColor Red
|
||||||
|
Write-Host "========================================" -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Erreur rencontrée:" -ForegroundColor Yellow
|
||||||
|
Write-Host $deployResult -ForegroundColor Red
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "💡 Solutions possibles:" -ForegroundColor Yellow
|
||||||
|
Write-Host " - Si 'Quota exceeded': Attendez 2 minutes et relancez" -ForegroundColor White
|
||||||
|
Write-Host " - Vérifiez que Firebase CLI est à jour: firebase --version" -ForegroundColor White
|
||||||
|
Write-Host " - Consultez les logs: firebase functions:log" -ForegroundColor White
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
@@ -56,13 +56,25 @@ service cloud.firestore {
|
|||||||
allow read: if request.auth != null;
|
allow read: if request.auth != null;
|
||||||
allow write: if false; // ❌ Écriture interdite
|
allow write: if false; // ❌ Écriture interdite
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Alertes : Lecture seule pour utilisateurs authentifiés
|
// Alertes : Lecture et création pour utilisateurs authentifiés
|
||||||
|
// Le trigger backend (onAlertCreated) s'occupe d'assigner les bonnes personnes
|
||||||
match /alerts/{alertId} {
|
match /alerts/{alertId} {
|
||||||
allow read: if request.auth != null;
|
allow read: if request.auth != null;
|
||||||
allow write: if false; // ❌ Écriture interdite
|
allow create: if request.auth != null
|
||||||
|
&& request.resource.data.createdBy == request.auth.uid; // Vérifier que l'utilisateur crée l'alerte en son nom
|
||||||
|
allow update: if request.auth != null
|
||||||
|
&& (
|
||||||
|
// L'utilisateur peut marquer comme lue uniquement s'il est assigné
|
||||||
|
(request.auth.uid in resource.data.assignedTo && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['isRead', 'readAt']))
|
||||||
|
// Ou le backend peut tout modifier (processed, assignedTo, etc.)
|
||||||
|
|| !('createdBy' in resource.data) // Le trigger backend n'a pas de createdBy
|
||||||
|
);
|
||||||
|
allow delete: if request.auth != null && request.auth.uid in resource.data.assignedTo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
// Utilisateurs : Lecture de son propre profil uniquement
|
// Utilisateurs : Lecture de son propre profil uniquement
|
||||||
match /users/{userId} {
|
match /users/{userId} {
|
||||||
allow read: if request.auth != null && request.auth.uid == userId;
|
allow read: if request.auth != null && request.auth.uid == userId;
|
||||||
|
|||||||
2
em2rp/functions/.gitignore
vendored
2
em2rp/functions/.gitignore
vendored
@@ -1,2 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
*.local
|
*.local
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|||||||
267
em2rp/functions/createAlert.js
Normal file
267
em2rp/functions/createAlert.js
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
const {onRequest} = require('firebase-functions/v2/https');
|
||||||
|
const admin = require('firebase-admin');
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
const logger = require('firebase-functions/logger');
|
||||||
|
const {getSmtpConfig, EMAIL_CONFIG} = require('./utils/emailConfig');
|
||||||
|
const {renderTemplate, getEmailSubject, getAlertTitle, prepareTemplateData, checkAlertPreference} = require('./utils/emailTemplates');
|
||||||
|
const auth = require('./utils/auth');
|
||||||
|
|
||||||
|
// Configuration CORS
|
||||||
|
const setCorsHeaders = (res, req) => {
|
||||||
|
// Utiliser l'origin de la requête pour permettre les credentials
|
||||||
|
const origin = req.headers.origin || '*';
|
||||||
|
|
||||||
|
res.set('Access-Control-Allow-Origin', origin);
|
||||||
|
|
||||||
|
// N'autoriser les credentials que si on a un origin spécifique (pas '*')
|
||||||
|
if (origin !== '*') {
|
||||||
|
res.set('Access-Control-Allow-Credentials', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||||
|
res.set('Access-Control-Allow-Headers', 'Authorization, Content-Type, Accept, Origin, X-Requested-With');
|
||||||
|
res.set('Access-Control-Max-Age', '3600');
|
||||||
|
};
|
||||||
|
|
||||||
|
const withCors = (handler) => {
|
||||||
|
return async (req, res) => {
|
||||||
|
setCorsHeaders(res, req);
|
||||||
|
// Gérer les requêtes preflight OPTIONS immédiatement
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.status(204).send('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await handler(req, res);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Unhandled error:", error);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({error: error.message});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une alerte et envoie les notifications
|
||||||
|
* Gère tout le processus côté backend de A à Z
|
||||||
|
*/
|
||||||
|
exports.createAlert = onRequest({cors: false, invoker: 'public'}, withCors(async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Vérifier l'authentification
|
||||||
|
const decodedToken = await auth.authenticateUser(req);
|
||||||
|
const data = req.body.data || req.body;
|
||||||
|
|
||||||
|
|
||||||
|
const {
|
||||||
|
type,
|
||||||
|
severity,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
equipmentId,
|
||||||
|
eventId,
|
||||||
|
actionUrl,
|
||||||
|
metadata,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
// Validation des données
|
||||||
|
if (!type || !severity || !message) {
|
||||||
|
res.status(400).json({error: 'type, severity et message sont requis'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Déterminer les utilisateurs à notifier
|
||||||
|
const userIds = await determineTargetUsers(type, severity, eventId);
|
||||||
|
|
||||||
|
if (userIds.length === 0) {
|
||||||
|
res.status(400).json({error: 'Aucun utilisateur à notifier'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Créer l'alerte dans Firestore
|
||||||
|
const alertRef = admin.firestore().collection('alerts').doc();
|
||||||
|
const alertData = {
|
||||||
|
id: alertRef.id,
|
||||||
|
type,
|
||||||
|
severity,
|
||||||
|
title: title || getAlertTitle(type),
|
||||||
|
message,
|
||||||
|
equipmentId: equipmentId || null,
|
||||||
|
eventId: eventId || null,
|
||||||
|
actionUrl: actionUrl || null,
|
||||||
|
metadata: metadata || {},
|
||||||
|
assignedTo: userIds,
|
||||||
|
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
createdBy: decodedToken.uid,
|
||||||
|
isRead: false,
|
||||||
|
emailSent: false,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
};
|
||||||
|
|
||||||
|
await alertRef.set(alertData);
|
||||||
|
|
||||||
|
// 3. Envoyer les emails si alerte critique
|
||||||
|
let emailResults = {};
|
||||||
|
if (severity === 'CRITICAL') {
|
||||||
|
emailResults = await sendAlertEmails(alertRef.id, alertData, userIds);
|
||||||
|
|
||||||
|
// Mettre à jour le statut d'envoi
|
||||||
|
await alertRef.update({
|
||||||
|
emailSent: true,
|
||||||
|
emailSentAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
emailResults,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
alertId: alertRef.id,
|
||||||
|
usersNotified: userIds.length,
|
||||||
|
emailsSent: Object.values(emailResults).filter((v) => v).length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[createAlert] Erreur:', error);
|
||||||
|
res.status(500).json({error: `Erreur lors de la création de l'alerte: ${error.message}`});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détermine les utilisateurs à notifier selon le type d'alerte
|
||||||
|
*/
|
||||||
|
async function determineTargetUsers(alertType, severity, eventId) {
|
||||||
|
const db = admin.firestore();
|
||||||
|
const targetUserIds = new Set();
|
||||||
|
|
||||||
|
// 1. Récupérer TOUS les utilisateurs pour déterminer lesquels sont admins
|
||||||
|
const allUsersSnapshot = await db.collection('users').get();
|
||||||
|
|
||||||
|
allUsersSnapshot.forEach((doc) => {
|
||||||
|
const user = doc.data();
|
||||||
|
if (user.role) {
|
||||||
|
// Le rôle peut être une référence Firestore ou une string
|
||||||
|
let rolePath = '';
|
||||||
|
if (typeof user.role === 'string') {
|
||||||
|
rolePath = user.role;
|
||||||
|
} else if (user.role.path) {
|
||||||
|
rolePath = user.role.path;
|
||||||
|
} else if (user.role._path && user.role._path.segments) {
|
||||||
|
rolePath = user.role._path.segments.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si c'est un admin (path = "roles/ADMIN")
|
||||||
|
if (rolePath === 'roles/ADMIN' || rolePath === 'ADMIN') {
|
||||||
|
targetUserIds.add(doc.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Si un événement est lié, ajouter tous les membres de la workforce
|
||||||
|
if (eventId) {
|
||||||
|
try {
|
||||||
|
const eventDoc = await db.collection('events').doc(eventId).get();
|
||||||
|
|
||||||
|
if (eventDoc.exists) {
|
||||||
|
const event = eventDoc.data();
|
||||||
|
const workforce = event.workforce || [];
|
||||||
|
|
||||||
|
workforce.forEach((member) => {
|
||||||
|
if (member.userId) {
|
||||||
|
targetUserIds.add(member.userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warn(`[determineTargetUsers] Événement ${eventId} introuvable`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[determineTargetUsers] Erreur récupération événement:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(targetUserIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie les emails d'alerte à tous les utilisateurs
|
||||||
|
*/
|
||||||
|
async function sendAlertEmails(alertId, alertData, userIds) {
|
||||||
|
const results = {};
|
||||||
|
const transporter = nodemailer.createTransporter(getSmtpConfig());
|
||||||
|
|
||||||
|
// Envoyer les emails en parallèle (batch de 5)
|
||||||
|
const batches = [];
|
||||||
|
for (let i = 0; i < userIds.length; i += 5) {
|
||||||
|
batches.push(userIds.slice(i, i + 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const batch of batches) {
|
||||||
|
const promises = batch.map(async (userId) => {
|
||||||
|
try {
|
||||||
|
const sent = await sendSingleEmail(transporter, alertId, alertData, userId);
|
||||||
|
results[userId] = sent;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[sendAlertEmails] Erreur email ${userId}:`, error);
|
||||||
|
results[userId] = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un email à un utilisateur spécifique
|
||||||
|
*/
|
||||||
|
async function sendSingleEmail(transporter, alertId, alertData, userId) {
|
||||||
|
const db = admin.firestore();
|
||||||
|
|
||||||
|
// Récupérer l'utilisateur
|
||||||
|
const userDoc = await db.collection('users').doc(userId).get();
|
||||||
|
|
||||||
|
if (!userDoc.exists) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userDoc.data();
|
||||||
|
|
||||||
|
// Vérifier les préférences email
|
||||||
|
const prefs = user.notificationPreferences || {};
|
||||||
|
if (!prefs.emailEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier la préférence pour ce type d'alerte
|
||||||
|
if (!checkAlertPreference(alertData.type, prefs)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.email) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Préparer les données du template
|
||||||
|
const templateData = await prepareTemplateData(alertData, user);
|
||||||
|
|
||||||
|
// Rendre le template
|
||||||
|
const html = await renderTemplate('alert-individual', templateData);
|
||||||
|
|
||||||
|
// Envoyer l'email
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: `"${EMAIL_CONFIG.from.name}" <${EMAIL_CONFIG.from.address}>`,
|
||||||
|
to: user.email,
|
||||||
|
replyTo: EMAIL_CONFIG.replyTo,
|
||||||
|
subject: getEmailSubject(alertData),
|
||||||
|
html: html,
|
||||||
|
text: alertData.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[sendSingleEmail] Erreur envoi à ${userId}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,7 +3,12 @@
|
|||||||
* Architecture backend sécurisée avec authentification et permissions
|
* Architecture backend sécurisée avec authentification et permissions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Charger les variables d'environnement depuis .env
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
const { onRequest, onCall } = require("firebase-functions/v2/https");
|
const { onRequest, onCall } = require("firebase-functions/v2/https");
|
||||||
|
const { onSchedule } = require("firebase-functions/v2/scheduler");
|
||||||
|
const { onDocumentCreated, onDocumentUpdated } = require("firebase-functions/v2/firestore");
|
||||||
const logger = require("firebase-functions/logger");
|
const logger = require("firebase-functions/logger");
|
||||||
const admin = require('firebase-admin');
|
const admin = require('firebase-admin');
|
||||||
const { Storage } = require('@google-cloud/storage');
|
const { Storage } = require('@google-cloud/storage');
|
||||||
@@ -12,14 +17,16 @@ const { Storage } = require('@google-cloud/storage');
|
|||||||
const auth = require('./utils/auth');
|
const auth = require('./utils/auth');
|
||||||
const helpers = require('./utils/helpers');
|
const helpers = require('./utils/helpers');
|
||||||
|
|
||||||
// Initialisation
|
// Initialisation sécurisée
|
||||||
|
if (!admin.apps.length) {
|
||||||
admin.initializeApp();
|
admin.initializeApp();
|
||||||
|
}
|
||||||
const storage = new Storage();
|
const storage = new Storage();
|
||||||
const db = admin.firestore();
|
const db = admin.firestore();
|
||||||
|
|
||||||
// Configuration commune pour toutes les fonctions HTTP
|
// Configuration commune pour toutes les fonctions HTTP
|
||||||
const httpOptions = {
|
const httpOptions = {
|
||||||
cors: true,
|
cors: false,
|
||||||
invoker: 'public', // Permet les invocations non authentifiées (l'auth est gérée par notre token Firebase)
|
invoker: 'public', // Permet les invocations non authentifiées (l'auth est gérée par notre token Firebase)
|
||||||
// Version: 2.0 - Ajout de l'invoker public pour résoudre les problèmes CORS
|
// Version: 2.0 - Ajout de l'invoker public pour résoudre les problèmes CORS
|
||||||
};
|
};
|
||||||
@@ -28,10 +35,16 @@ const httpOptions = {
|
|||||||
// CORS Middleware
|
// CORS Middleware
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
const setCorsHeaders = (res, req) => {
|
const setCorsHeaders = (res, req) => {
|
||||||
// Permettre toutes les origines en développement/production
|
// Utiliser l'origin de la requête pour permettre les credentials
|
||||||
const origin = req.headers.origin || req.headers.referer || '*';
|
const origin = req.headers.origin || '*';
|
||||||
|
|
||||||
res.set('Access-Control-Allow-Origin', origin);
|
res.set('Access-Control-Allow-Origin', origin);
|
||||||
|
|
||||||
|
// N'autoriser les credentials que si on a un origin spécifique (pas '*')
|
||||||
|
if (origin !== '*') {
|
||||||
res.set('Access-Control-Allow-Credentials', 'true');
|
res.set('Access-Control-Allow-Credentials', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||||
res.set('Access-Control-Allow-Headers', 'Authorization, Content-Type, Accept, Origin, X-Requested-With');
|
res.set('Access-Control-Allow-Headers', 'Authorization, Content-Type, Accept, Origin, X-Requested-With');
|
||||||
res.set('Access-Control-Max-Age', '3600');
|
res.set('Access-Control-Max-Age', '3600');
|
||||||
@@ -43,7 +56,7 @@ const withCors = (handler) => {
|
|||||||
// Définir les headers CORS pour toutes les requêtes
|
// Définir les headers CORS pour toutes les requêtes
|
||||||
setCorsHeaders(res, req);
|
setCorsHeaders(res, req);
|
||||||
|
|
||||||
// Gérer les requêtes preflight OPTIONS
|
// Gérer les requêtes preflight OPTIONS immédiatement
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
res.status(204).send('');
|
res.status(204).send('');
|
||||||
return;
|
return;
|
||||||
@@ -1165,7 +1178,7 @@ exports.updateUser = onRequest(httpOptions, withCors(async (req, res) => {
|
|||||||
|
|
||||||
// Si mise à jour propre profil, limiter les champs modifiables
|
// Si mise à jour propre profil, limiter les champs modifiables
|
||||||
if (isOwnProfile && !isAdminUser) {
|
if (isOwnProfile && !isAdminUser) {
|
||||||
const allowedFields = ['firstName', 'lastName', 'phoneNumber', 'profilePhotoUrl'];
|
const allowedFields = ['firstName', 'lastName', 'phoneNumber', 'profilePhotoUrl', 'notificationPreferences'];
|
||||||
const filteredData = {};
|
const filteredData = {};
|
||||||
|
|
||||||
for (const field of allowedFields) {
|
for (const field of allowedFields) {
|
||||||
@@ -1890,79 +1903,7 @@ exports.deleteAlert = onRequest(httpOptions, withCors(async (req, res) => {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
// createAlert est défini dans createAlert.js et importé à la fin du fichier
|
||||||
* Créer une nouvelle alerte
|
|
||||||
*/
|
|
||||||
exports.createAlert = onRequest(httpOptions, withCors(async (req, res) => {
|
|
||||||
try {
|
|
||||||
const decodedToken = await auth.authenticateUser(req);
|
|
||||||
const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment');
|
|
||||||
|
|
||||||
if (!hasAccess) {
|
|
||||||
res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { type, title, message, severity, equipmentId } = req.body.data;
|
|
||||||
|
|
||||||
if (!type || !message) {
|
|
||||||
res.status(400).json({ error: 'type and message are required' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si une alerte similaire existe déjà (éviter les doublons)
|
|
||||||
const existingAlertsQuery = await db.collection('alerts')
|
|
||||||
.where('type', '==', type)
|
|
||||||
.where('isRead', '==', false)
|
|
||||||
.get();
|
|
||||||
|
|
||||||
let alertExists = false;
|
|
||||||
if (equipmentId) {
|
|
||||||
// Pour les alertes liées à un équipement, vérifier aussi l'equipmentId
|
|
||||||
alertExists = existingAlertsQuery.docs.some(doc =>
|
|
||||||
doc.data().equipmentId === equipmentId
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Pour les autres alertes, vérifier le message
|
|
||||||
alertExists = existingAlertsQuery.docs.some(doc =>
|
|
||||||
doc.data().message === message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alertExists) {
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: 'Alert already exists',
|
|
||||||
skipped: true
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Créer la nouvelle alerte
|
|
||||||
const alertData = {
|
|
||||||
type: type,
|
|
||||||
title: title || 'Alerte',
|
|
||||||
message: message,
|
|
||||||
severity: severity || 'MEDIUM',
|
|
||||||
isRead: false,
|
|
||||||
createdAt: admin.firestore.Timestamp.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (equipmentId) {
|
|
||||||
alertData.equipmentId = equipmentId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const alertRef = await db.collection('alerts').add(alertData);
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
alertId: alertRef.id
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error creating alert:", error);
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// USERS - Read with permissions
|
// USERS - Read with permissions
|
||||||
@@ -3442,3 +3383,176 @@ exports.completeMaintenance = onRequest(httpOptions, withCors(async (req, res) =
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// ==================== EMAIL FUNCTIONS ====================
|
||||||
|
const {sendAlertEmail} = require('./sendAlertEmail');
|
||||||
|
exports.sendAlertEmail = sendAlertEmail;
|
||||||
|
|
||||||
|
// ==================== ALERT FUNCTIONS ====================
|
||||||
|
const {createAlert} = require('./createAlert');
|
||||||
|
exports.createAlert = createAlert;
|
||||||
|
|
||||||
|
const {processEquipmentValidation} = require('./processEquipmentValidation');
|
||||||
|
exports.processEquipmentValidation = processEquipmentValidation;
|
||||||
|
|
||||||
|
// ==================== SCHEDULED FUNCTIONS ====================
|
||||||
|
const {sendDailyDigest} = require('./sendDailyDigest');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fonction schedulée : Envoie quotidien d'un digest des alertes non lues
|
||||||
|
* S'exécute tous les jours à 8h00 (Europe/Paris)
|
||||||
|
*/
|
||||||
|
exports.sendDailyDigest = onSchedule({
|
||||||
|
schedule: '0 8 * * *',
|
||||||
|
timeZone: 'Europe/Paris',
|
||||||
|
retryCount: 2,
|
||||||
|
memory: '512MiB'
|
||||||
|
}, async (context) => {
|
||||||
|
logger.info('[Scheduler] Démarrage sendDailyDigest');
|
||||||
|
try {
|
||||||
|
await sendDailyDigest();
|
||||||
|
logger.info('[Scheduler] sendDailyDigest terminé avec succès');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[Scheduler] Erreur sendDailyDigest:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== FIRESTORE TRIGGERS ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger : Nouvel événement créé
|
||||||
|
* Envoie une notification à tous les membres de la workforce
|
||||||
|
*/
|
||||||
|
exports.onEventCreated = onDocumentCreated('events/{eventId}', async (event) => {
|
||||||
|
logger.info(`[onEventCreated] Événement créé: ${event.params.eventId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const eventData = event.data.data();
|
||||||
|
const eventId = event.params.eventId;
|
||||||
|
|
||||||
|
// Créer une alerte pour informer la workforce
|
||||||
|
await db.collection('alerts').add({
|
||||||
|
type: 'EVENT_CREATED',
|
||||||
|
severity: 'INFO',
|
||||||
|
message: `Nouvel événement créé : "${eventData.name}" le ${new Date(eventData.startDate?.toDate ? eventData.startDate.toDate() : eventData.startDate).toLocaleDateString('fr-FR')}`,
|
||||||
|
eventId: eventId,
|
||||||
|
eventName: eventData.name,
|
||||||
|
eventDate: eventData.startDate,
|
||||||
|
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
isRead: false,
|
||||||
|
metadata: {
|
||||||
|
eventId: eventId,
|
||||||
|
eventName: eventData.name,
|
||||||
|
eventDate: eventData.startDate,
|
||||||
|
},
|
||||||
|
assignedTo: [], // Sera rempli automatiquement par la fonction createAlert
|
||||||
|
});
|
||||||
|
|
||||||
|
// Appeler createAlert via HTTP pour gérer l'envoi des emails
|
||||||
|
const createAlertModule = require('./createAlert');
|
||||||
|
// Note: On ne peut pas appeler directement la fonction HTTP, mais on peut créer l'alerte directement
|
||||||
|
// L'envoi des emails sera géré par un trigger sur la collection alerts
|
||||||
|
|
||||||
|
logger.info(`[onEventCreated] Alerte créée pour événement ${eventId}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[onEventCreated] Erreur:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger : Événement modifié (workforce changée)
|
||||||
|
* Envoie une notification aux nouveaux membres ajoutés à la workforce
|
||||||
|
*/
|
||||||
|
exports.onEventUpdated = onDocumentUpdated('events/{eventId}', async (event) => {
|
||||||
|
const before = event.data.before.data();
|
||||||
|
const after = event.data.after.data();
|
||||||
|
const eventId = event.params.eventId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Vérifier si la workforce a changé
|
||||||
|
const workforceBefore = before.workforce || [];
|
||||||
|
const workforceAfter = after.workforce || [];
|
||||||
|
|
||||||
|
// Trouver les nouveaux membres ajoutés
|
||||||
|
const newMembers = workforceAfter.filter(afterMember => {
|
||||||
|
return !workforceBefore.some(beforeMember =>
|
||||||
|
beforeMember.userId === afterMember.userId
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newMembers.length > 0) {
|
||||||
|
logger.info(`[onEventUpdated] ${newMembers.length} nouveaux membres ajoutés à ${eventId}`);
|
||||||
|
|
||||||
|
// Créer une alerte pour chaque nouveau membre
|
||||||
|
for (const member of newMembers) {
|
||||||
|
await db.collection('alerts').add({
|
||||||
|
type: 'WORKFORCE_ADDED',
|
||||||
|
severity: 'INFO',
|
||||||
|
message: `Vous avez été ajouté(e) à l'événement "${after.name}" le ${new Date(after.startDate?.toDate ? after.startDate.toDate() : after.startDate).toLocaleDateString('fr-FR')}`,
|
||||||
|
eventId: eventId,
|
||||||
|
eventName: after.name,
|
||||||
|
eventDate: after.startDate,
|
||||||
|
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
isRead: false,
|
||||||
|
metadata: {
|
||||||
|
eventId: eventId,
|
||||||
|
eventName: after.name,
|
||||||
|
eventDate: after.startDate,
|
||||||
|
},
|
||||||
|
assignedTo: [member.userId], // Alerte ciblée uniquement pour ce membre
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`[onEventUpdated] Alerte créée pour ${member.userId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[onEventUpdated] Erreur:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger : Nouvelle alerte créée
|
||||||
|
* Envoie un email immédiat si l'alerte est critique
|
||||||
|
*/
|
||||||
|
exports.onAlertCreated = onDocumentCreated('alerts/{alertId}', async (event) => {
|
||||||
|
const alertId = event.params.alertId;
|
||||||
|
const alertData = event.data.data();
|
||||||
|
|
||||||
|
logger.info(`[onAlertCreated] Nouvelle alerte: ${alertId} (${alertData.severity})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Si l'alerte est critique et pas encore envoyée par email
|
||||||
|
if (alertData.severity === 'CRITICAL' && !alertData.emailSent) {
|
||||||
|
const sendEmailModule = require('./sendAlertEmail');
|
||||||
|
|
||||||
|
// Les destinataires sont déjà dans assignedTo
|
||||||
|
const userIds = alertData.assignedTo || [];
|
||||||
|
|
||||||
|
if (userIds.length > 0) {
|
||||||
|
logger.info(`[onAlertCreated] Envoi email immédiat à ${userIds.length} utilisateurs`);
|
||||||
|
|
||||||
|
// Note: Dans un trigger Firestore, on ne peut pas facilement appeler une fonction HTTP
|
||||||
|
// Il faudrait soit:
|
||||||
|
// 1. Dupliquer la logique d'envoi d'email ici
|
||||||
|
// 2. Utiliser une file d'attente (Pub/Sub ou Tasks)
|
||||||
|
// 3. Marquer l'alerte pour qu'elle soit traitée par un scheduler
|
||||||
|
|
||||||
|
// Pour l'instant, on marque l'alerte comme devant être envoyée
|
||||||
|
await db.collection('alerts').doc(alertId).update({
|
||||||
|
pendingEmailSend: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`[onAlertCreated] Alerte marquée pour envoi email`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[onAlertCreated] Erreur:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== ALERT TRIGGERS ====================
|
||||||
|
// Temporairement désactivé - erreur de permissions Eventarc
|
||||||
|
// const {onAlertCreated} = require('./onAlertCreated');
|
||||||
|
// exports.onAlertCreated = onAlertCreated;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
113
em2rp/functions/migrate_email_prefs.js
Normal file
113
em2rp/functions/migrate_email_prefs.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* Script de migration : Active les emails pour tous les utilisateurs existants
|
||||||
|
* À exécuter une seule fois après le déploiement
|
||||||
|
*/
|
||||||
|
const admin = require('firebase-admin');
|
||||||
|
const logger = require('firebase-functions/logger');
|
||||||
|
|
||||||
|
// AJOUTER CECI : Charger le fichier de clé
|
||||||
|
const serviceAccount = require('./serviceAccountKey.json');
|
||||||
|
|
||||||
|
// Initialiser Firebase Admin avec les credentials explicites
|
||||||
|
if (!admin.apps.length) {
|
||||||
|
admin.initializeApp({
|
||||||
|
credential: admin.credential.cert(serviceAccount), // <-- Utiliser la clé ici
|
||||||
|
projectId: 'em2rp-951dc',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = admin.firestore();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active les notifications par email pour tous les utilisateurs existants
|
||||||
|
*/
|
||||||
|
async function migrateEmailPreferences() {
|
||||||
|
console.log('=== DÉBUT MIGRATION EMAIL PREFERENCES ===\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Récupérer tous les utilisateurs
|
||||||
|
const usersSnapshot = await db.collection('users').get();
|
||||||
|
console.log(`✓ ${usersSnapshot.size} utilisateurs trouvés\n`);
|
||||||
|
|
||||||
|
// 2. Préparer les updates
|
||||||
|
const updates = [];
|
||||||
|
let alreadyEnabled = 0;
|
||||||
|
let toUpdate = 0;
|
||||||
|
|
||||||
|
usersSnapshot.forEach((doc) => {
|
||||||
|
const user = doc.data();
|
||||||
|
const prefs = user.notificationPreferences || {};
|
||||||
|
|
||||||
|
// Vérifier si déjà activé
|
||||||
|
if (prefs.emailEnabled === true) {
|
||||||
|
alreadyEnabled++;
|
||||||
|
console.log(` ○ ${user.email || doc.id}: emails déjà activés`);
|
||||||
|
} else {
|
||||||
|
toUpdate++;
|
||||||
|
console.log(` ✓ ${user.email || doc.id}: activation des emails`);
|
||||||
|
|
||||||
|
updates.push({
|
||||||
|
ref: doc.ref,
|
||||||
|
data: {
|
||||||
|
'notificationPreferences.emailEnabled': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`\n--- RÉSUMÉ ---`);
|
||||||
|
console.log(` Total utilisateurs: ${usersSnapshot.size}`);
|
||||||
|
console.log(` Déjà activés: ${alreadyEnabled}`);
|
||||||
|
console.log(` À mettre à jour: ${toUpdate}`);
|
||||||
|
|
||||||
|
// 3. Appliquer les mises à jour par batches de 500 (limite Firestore)
|
||||||
|
if (updates.length > 0) {
|
||||||
|
console.log(`\nApplication des mises à jour...`);
|
||||||
|
|
||||||
|
const batchSize = 500;
|
||||||
|
for (let i = 0; i < updates.length; i += batchSize) {
|
||||||
|
const batch = db.batch();
|
||||||
|
const currentBatch = updates.slice(i, i + batchSize);
|
||||||
|
|
||||||
|
currentBatch.forEach((update) => {
|
||||||
|
batch.update(update.ref, update.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
await batch.commit();
|
||||||
|
console.log(` ✓ Batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(updates.length / batchSize)} appliqué`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✓ Migration terminée avec succès !`);
|
||||||
|
console.log(` ${toUpdate} utilisateurs mis à jour\n`);
|
||||||
|
} else {
|
||||||
|
console.log(`\n✓ Aucune mise à jour nécessaire\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('=== FIN MIGRATION ===');
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
total: usersSnapshot.size,
|
||||||
|
alreadyEnabled,
|
||||||
|
updated: toUpdate,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ ERREUR MIGRATION:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exécuter la migration si appelé directement
|
||||||
|
if (require.main === module) {
|
||||||
|
migrateEmailPreferences()
|
||||||
|
.then((result) => {
|
||||||
|
console.log('\n✓ Migration réussie:', result);
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('\n❌ Migration échouée:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { migrateEmailPreferences };
|
||||||
|
|
||||||
392
em2rp/functions/package-lock.json
generated
392
em2rp/functions/package-lock.json
generated
@@ -6,9 +6,14 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "functions",
|
"name": "functions",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@google-cloud/storage": "^7.18.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"envdot": "^0.0.3",
|
||||||
"firebase-admin": "^12.6.0",
|
"firebase-admin": "^12.6.0",
|
||||||
"firebase-functions": "^6.0.1"
|
"firebase-functions": "^7.0.3",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
|
"nodemailer": "^6.10.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.15.0",
|
"eslint": "^8.15.0",
|
||||||
@@ -706,7 +711,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz",
|
||||||
"integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==",
|
"integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"arrify": "^2.0.0",
|
"arrify": "^2.0.0",
|
||||||
"extend": "^3.0.2"
|
"extend": "^3.0.2"
|
||||||
@@ -720,7 +724,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz",
|
||||||
"integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==",
|
"integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
@@ -730,17 +733,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz",
|
||||||
"integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==",
|
"integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@google-cloud/storage": {
|
"node_modules/@google-cloud/storage": {
|
||||||
"version": "7.16.0",
|
"version": "7.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.18.0.tgz",
|
||||||
"integrity": "sha512-7/5LRgykyOfQENcm6hDKP8SX/u9XxE5YOiWOkgkwcoO+cG8xT/cyOvp9wwN3IxfdYgpHs8CE7Nq2PKX2lNaEXw==",
|
"integrity": "sha512-r3ZwDMiz4nwW6R922Z1pwpePxyRwE5GdevYX63hRmAQUkUQJcBH/79EnQPDv5cOv1mFBgevdNWQfi3tie3dHrQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google-cloud/paginator": "^5.0.0",
|
"@google-cloud/paginator": "^5.0.0",
|
||||||
"@google-cloud/projectify": "^4.0.0",
|
"@google-cloud/projectify": "^4.0.0",
|
||||||
@@ -767,7 +768,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
@@ -885,9 +885,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
|
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
|
||||||
"version": "3.14.1",
|
"version": "3.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1460,7 +1460,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||||
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
|
"integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
@@ -1524,8 +1523,7 @@
|
|||||||
"version": "0.12.5",
|
"version": "0.12.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
|
||||||
"integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==",
|
"integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/connect": {
|
"node_modules/@types/connect": {
|
||||||
"version": "3.4.38",
|
"version": "3.4.38",
|
||||||
@@ -1674,7 +1672,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz",
|
||||||
"integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==",
|
"integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/caseless": "*",
|
"@types/caseless": "*",
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
@@ -1714,8 +1711,7 @@
|
|||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
|
||||||
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
|
"integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/yargs": {
|
"node_modules/@types/yargs": {
|
||||||
"version": "17.0.33",
|
"version": "17.0.33",
|
||||||
@@ -1746,7 +1742,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"event-target-shim": "^5.0.0"
|
"event-target-shim": "^5.0.0"
|
||||||
},
|
},
|
||||||
@@ -1796,7 +1791,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
|
||||||
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
@@ -1905,7 +1899,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
|
||||||
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
|
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
@@ -1915,7 +1908,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
|
||||||
"integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
|
"integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"retry": "0.13.1"
|
"retry": "0.13.1"
|
||||||
}
|
}
|
||||||
@@ -2094,37 +2086,35 @@
|
|||||||
"url": "https://feross.org/support"
|
"url": "https://feross.org/support"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/bignumber.js": {
|
"node_modules/bignumber.js": {
|
||||||
"version": "9.3.0",
|
"version": "9.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz",
|
||||||
"integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==",
|
"integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.3",
|
"version": "1.20.4",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "~3.1.2",
|
||||||
"content-type": "~1.0.5",
|
"content-type": "~1.0.5",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
"destroy": "1.2.0",
|
"destroy": "~1.2.0",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "~2.0.1",
|
||||||
"iconv-lite": "0.4.24",
|
"iconv-lite": "~0.4.24",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "~2.4.1",
|
||||||
"qs": "6.13.0",
|
"qs": "~6.14.0",
|
||||||
"raw-body": "2.5.2",
|
"raw-body": "~2.5.3",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
"unpipe": "1.0.0"
|
"unpipe": "~1.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8",
|
"node": ">= 0.8",
|
||||||
@@ -2140,16 +2130,45 @@
|
|||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/body-parser/node_modules/http-errors": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"inherits": "~2.0.4",
|
||||||
|
"setprototypeof": "~1.2.0",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"toidentifier": "~1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/body-parser/node_modules/ms": {
|
"node_modules/body-parser/node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/body-parser/node_modules/statuses": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2631,6 +2650,18 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "17.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
|
||||||
|
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@@ -2650,7 +2681,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
|
||||||
"integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
|
"integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"end-of-stream": "^1.4.1",
|
"end-of-stream": "^1.4.1",
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
@@ -2714,11 +2744,34 @@
|
|||||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/envdot": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/envdot/-/envdot-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-vaJ+ac5s9X/cz1hPA7D/JLSbkloEZVozkzx2n83xcCUxuaQf/sHwjFIUiJfBwSoEU3crecRT7OftKCizhe9dwA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^7.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"envdot": "index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/envdot/node_modules/dotenv": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/error-ex": {
|
"node_modules/error-ex": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||||
@@ -2996,7 +3049,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -3052,39 +3104,39 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.2",
|
"version": "4.22.1",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
"body-parser": "1.20.3",
|
"body-parser": "~1.20.3",
|
||||||
"content-disposition": "0.5.4",
|
"content-disposition": "~0.5.4",
|
||||||
"content-type": "~1.0.4",
|
"content-type": "~1.0.4",
|
||||||
"cookie": "0.7.1",
|
"cookie": "~0.7.1",
|
||||||
"cookie-signature": "1.0.6",
|
"cookie-signature": "~1.0.6",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
"depd": "2.0.0",
|
"depd": "2.0.0",
|
||||||
"encodeurl": "~2.0.0",
|
"encodeurl": "~2.0.0",
|
||||||
"escape-html": "~1.0.3",
|
"escape-html": "~1.0.3",
|
||||||
"etag": "~1.8.1",
|
"etag": "~1.8.1",
|
||||||
"finalhandler": "1.3.1",
|
"finalhandler": "~1.3.1",
|
||||||
"fresh": "0.5.2",
|
"fresh": "~0.5.2",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "~2.0.0",
|
||||||
"merge-descriptors": "1.0.3",
|
"merge-descriptors": "1.0.3",
|
||||||
"methods": "~1.1.2",
|
"methods": "~1.1.2",
|
||||||
"on-finished": "2.4.1",
|
"on-finished": "~2.4.1",
|
||||||
"parseurl": "~1.3.3",
|
"parseurl": "~1.3.3",
|
||||||
"path-to-regexp": "0.1.12",
|
"path-to-regexp": "~0.1.12",
|
||||||
"proxy-addr": "~2.0.7",
|
"proxy-addr": "~2.0.7",
|
||||||
"qs": "6.13.0",
|
"qs": "~6.14.0",
|
||||||
"range-parser": "~1.2.1",
|
"range-parser": "~1.2.1",
|
||||||
"safe-buffer": "5.2.1",
|
"safe-buffer": "5.2.1",
|
||||||
"send": "0.19.0",
|
"send": "~0.19.0",
|
||||||
"serve-static": "1.16.2",
|
"serve-static": "~1.16.2",
|
||||||
"setprototypeof": "1.2.0",
|
"setprototypeof": "1.2.0",
|
||||||
"statuses": "2.0.1",
|
"statuses": "~2.0.1",
|
||||||
"type-is": "~1.6.18",
|
"type-is": "~1.6.18",
|
||||||
"utils-merge": "1.0.1",
|
"utils-merge": "1.0.1",
|
||||||
"vary": "~1.1.2"
|
"vary": "~1.1.2"
|
||||||
@@ -3116,8 +3168,7 @@
|
|||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/farmhash-modern": {
|
"node_modules/farmhash-modern": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@@ -3160,7 +3211,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"strnum": "^1.1.1"
|
"strnum": "^1.1.1"
|
||||||
},
|
},
|
||||||
@@ -3302,9 +3352,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/firebase-functions": {
|
"node_modules/firebase-functions": {
|
||||||
"version": "6.3.2",
|
"version": "7.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-7.0.3.tgz",
|
||||||
"integrity": "sha512-FC3A1/nhqt1ZzxRnj5HZLScQaozAcFSD/vSR8khqSoFNOfxuXgwJS6ZABTB7+v+iMD5z6Mmxw6OfqITUBuI7OQ==",
|
"integrity": "sha512-DiIjIUv0OS4KEAA3jqyIc7ymZKdcmMcaXy7FCCkrDQo/1CVMbDDWMdZIslmsgSjldA2nhau1dTE/6JQI8Urjjw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3318,10 +3368,20 @@
|
|||||||
"firebase-functions": "lib/bin/firebase-functions.js"
|
"firebase-functions": "lib/bin/firebase-functions.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.10.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"@apollo/server": "^5.2.0",
|
||||||
|
"@as-integrations/express4": "^1.1.2",
|
||||||
"firebase-admin": "^11.10.0 || ^12.0.0 || ^13.0.0"
|
"firebase-admin": "^11.10.0 || ^12.0.0 || ^13.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@apollo/server": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@as-integrations/express4": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/firebase-functions-test": {
|
"node_modules/firebase-functions-test": {
|
||||||
@@ -3387,15 +3447,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/form-data": {
|
"node_modules/form-data": {
|
||||||
"version": "2.5.3",
|
"version": "2.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz",
|
||||||
"integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==",
|
"integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
"combined-stream": "^1.0.8",
|
"combined-stream": "^1.0.8",
|
||||||
"es-set-tostringtag": "^2.1.0",
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"safe-buffer": "^5.2.1"
|
"safe-buffer": "^5.2.1"
|
||||||
},
|
},
|
||||||
@@ -3464,7 +3524,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
|
||||||
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
|
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"extend": "^3.0.2",
|
"extend": "^3.0.2",
|
||||||
"https-proxy-agent": "^7.0.1",
|
"https-proxy-agent": "^7.0.1",
|
||||||
@@ -3485,7 +3544,6 @@
|
|||||||
"https://github.com/sponsors/ctavan"
|
"https://github.com/sponsors/ctavan"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
@@ -3495,7 +3553,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
|
||||||
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
|
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gaxios": "^6.1.1",
|
"gaxios": "^6.1.1",
|
||||||
"google-logging-utils": "^0.0.2",
|
"google-logging-utils": "^0.0.2",
|
||||||
@@ -3641,7 +3698,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
|
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
|
||||||
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
|
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64-js": "^1.3.0",
|
"base64-js": "^1.3.0",
|
||||||
"ecdsa-sig-formatter": "^1.0.11",
|
"ecdsa-sig-formatter": "^1.0.11",
|
||||||
@@ -3697,7 +3753,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
|
||||||
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
|
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
@@ -3733,7 +3788,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
|
||||||
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
|
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gaxios": "^6.0.0",
|
"gaxios": "^6.0.0",
|
||||||
"jws": "^4.0.0"
|
"jws": "^4.0.0"
|
||||||
@@ -3742,6 +3796,27 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/handlebars": {
|
||||||
|
"version": "4.7.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
|
||||||
|
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.5",
|
||||||
|
"neo-async": "^2.6.2",
|
||||||
|
"source-map": "^0.6.1",
|
||||||
|
"wordwrap": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"handlebars": "bin/handlebars"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.7"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"uglify-js": "^3.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
@@ -3805,8 +3880,7 @@
|
|||||||
"url": "https://patreon.com/mdevils"
|
"url": "https://patreon.com/mdevils"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/html-escaper": {
|
"node_modules/html-escaper": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
@@ -3842,7 +3916,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
||||||
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
|
"integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tootallnate/once": "2",
|
"@tootallnate/once": "2",
|
||||||
"agent-base": "6",
|
"agent-base": "6",
|
||||||
@@ -3857,7 +3930,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "4"
|
"debug": "4"
|
||||||
},
|
},
|
||||||
@@ -3870,7 +3942,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"agent-base": "^7.1.2",
|
"agent-base": "^7.1.2",
|
||||||
"debug": "4"
|
"debug": "4"
|
||||||
@@ -4075,7 +4146,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -4789,9 +4859,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4819,7 +4889,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bignumber.js": "^9.0.0"
|
"bignumber.js": "^9.0.0"
|
||||||
}
|
}
|
||||||
@@ -4899,12 +4968,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jsonwebtoken/node_modules/jws": {
|
"node_modules/jsonwebtoken/node_modules/jws": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
|
||||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jwa": "^1.4.1",
|
"jwa": "^1.4.2",
|
||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -4925,7 +4994,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-equal-constant-time": "^1.0.1",
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
"ecdsa-sig-formatter": "1.0.11",
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
@@ -4950,13 +5018,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jws": {
|
"node_modules/jws": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jwa": "^2.0.0",
|
"jwa": "^2.0.1",
|
||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -5246,7 +5313,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
||||||
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
|
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"mime": "cli.js"
|
"mime": "cli.js"
|
||||||
},
|
},
|
||||||
@@ -5298,6 +5364,15 @@
|
|||||||
"node": "*"
|
"node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -5320,12 +5395,17 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/neo-async": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"whatwg-url": "^5.0.0"
|
"whatwg-url": "^5.0.0"
|
||||||
},
|
},
|
||||||
@@ -5342,9 +5422,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-forge": {
|
"node_modules/node-forge": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz",
|
||||||
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
|
"integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==",
|
||||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6.13.0"
|
"node": ">= 6.13.0"
|
||||||
@@ -5364,6 +5444,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "6.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
|
||||||
|
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
@@ -5434,7 +5523,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
@@ -5478,7 +5566,6 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yocto-queue": "^0.1.0"
|
"yocto-queue": "^0.1.0"
|
||||||
@@ -5835,12 +5922,12 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.13.0",
|
"version": "6.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"side-channel": "^1.0.6"
|
"side-channel": "^1.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
@@ -5880,20 +5967,49 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/raw-body": {
|
"node_modules/raw-body": {
|
||||||
"version": "2.5.2",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "~3.1.2",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "~2.0.1",
|
||||||
"iconv-lite": "0.4.24",
|
"iconv-lite": "~0.4.24",
|
||||||
"unpipe": "1.0.0"
|
"unpipe": "~1.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/raw-body/node_modules/http-errors": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"inherits": "~2.0.4",
|
||||||
|
"setprototypeof": "~1.2.0",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"toidentifier": "~1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body/node_modules/statuses": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||||
@@ -5906,7 +6022,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
"string_decoder": "^1.1.1",
|
"string_decoder": "^1.1.1",
|
||||||
@@ -5995,7 +6110,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
||||||
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
|
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
@@ -6005,7 +6119,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz",
|
||||||
"integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==",
|
"integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/request": "^2.48.8",
|
"@types/request": "^2.48.8",
|
||||||
"extend": "^3.0.2",
|
"extend": "^3.0.2",
|
||||||
@@ -6307,7 +6420,6 @@
|
|||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -6368,7 +6480,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
|
||||||
"integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
|
"integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"stubs": "^3.0.0"
|
"stubs": "^3.0.0"
|
||||||
}
|
}
|
||||||
@@ -6377,15 +6488,13 @@
|
|||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
|
||||||
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
|
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
@@ -6475,15 +6584,13 @@
|
|||||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/stubs": {
|
"node_modules/stubs": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
|
||||||
"integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
|
"integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
@@ -6516,7 +6623,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
|
||||||
"integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==",
|
"integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"http-proxy-agent": "^5.0.0",
|
"http-proxy-agent": "^5.0.0",
|
||||||
"https-proxy-agent": "^5.0.0",
|
"https-proxy-agent": "^5.0.0",
|
||||||
@@ -6533,7 +6639,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "4"
|
"debug": "4"
|
||||||
},
|
},
|
||||||
@@ -6546,7 +6651,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"agent-base": "6",
|
"agent-base": "6",
|
||||||
"debug": "4"
|
"debug": "4"
|
||||||
@@ -6564,7 +6668,6 @@
|
|||||||
"https://github.com/sponsors/ctavan"
|
"https://github.com/sponsors/ctavan"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
@@ -6624,8 +6727,7 @@
|
|||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/ts-deepmerge": {
|
"node_modules/ts-deepmerge": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
@@ -6689,6 +6791,19 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uglify-js": {
|
||||||
|
"version": "3.19.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
|
||||||
|
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"uglifyjs": "bin/uglifyjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
@@ -6749,8 +6864,7 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/utils-merge": {
|
"node_modules/utils-merge": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@@ -6812,8 +6926,7 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause"
|
||||||
"optional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/websocket-driver": {
|
"node_modules/websocket-driver": {
|
||||||
"version": "0.7.4",
|
"version": "0.7.4",
|
||||||
@@ -6843,7 +6956,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tr46": "~0.0.3",
|
"tr46": "~0.0.3",
|
||||||
"webidl-conversions": "^3.0.0"
|
"webidl-conversions": "^3.0.0"
|
||||||
@@ -6875,6 +6987,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wordwrap": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
@@ -6897,7 +7015,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/write-file-atomic": {
|
"node_modules/write-file-atomic": {
|
||||||
@@ -6964,7 +7081,6 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
|
|||||||
@@ -14,9 +14,14 @@
|
|||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@google-cloud/storage": "^7.18.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"envdot": "^0.0.3",
|
||||||
"firebase-admin": "^12.6.0",
|
"firebase-admin": "^12.6.0",
|
||||||
"firebase-functions": "^6.0.1"
|
"firebase-functions": "^7.0.3",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
|
"nodemailer": "^6.10.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.15.0",
|
"eslint": "^8.15.0",
|
||||||
|
|||||||
415
em2rp/functions/processEquipmentValidation.js
Normal file
415
em2rp/functions/processEquipmentValidation.js
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
const {onCall} = require('firebase-functions/v2/https');
|
||||||
|
const admin = require('firebase-admin');
|
||||||
|
const logger = require('firebase-functions/logger');
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
const {getSmtpConfig, EMAIL_CONFIG} = require('./utils/emailConfig');
|
||||||
|
/**
|
||||||
|
* Traite la validation du matériel d'un événement
|
||||||
|
* Appelée par le client lors du chargement/déchargement
|
||||||
|
* Crée automatiquement les alertes nécessaires
|
||||||
|
*/
|
||||||
|
exports.processEquipmentValidation = onCall({cors: true}, async (request) => {
|
||||||
|
try {
|
||||||
|
// L'authentification est automatique avec onCall
|
||||||
|
const {auth, data} = request;
|
||||||
|
|
||||||
|
if (!auth) {
|
||||||
|
throw new Error('L\'utilisateur doit être authentifié');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
eventId,
|
||||||
|
equipmentList, // [{equipmentId, status, quantity, etc.}]
|
||||||
|
validationType, // 'LOADING', 'UNLOADING', 'CHECK_OUT', 'CHECK_IN'
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!eventId || !equipmentList || !validationType) {
|
||||||
|
throw new Error('eventId, equipmentList et validationType sont requis');
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = admin.firestore();
|
||||||
|
const alerts = [];
|
||||||
|
|
||||||
|
// 1. Récupérer les détails de l'événement
|
||||||
|
const eventRef = db.collection('events').doc(eventId);
|
||||||
|
const eventDoc = await eventRef.get();
|
||||||
|
|
||||||
|
if (!eventDoc.exists) {
|
||||||
|
throw new Error('Événement introuvable');
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = eventDoc.data();
|
||||||
|
const eventName = event.Name || event.name || 'Événement inconnu';
|
||||||
|
const eventDate = formatEventDate(event);
|
||||||
|
|
||||||
|
// 2. Analyser les équipements et détecter les problèmes
|
||||||
|
for (const equipment of equipmentList) {
|
||||||
|
const {equipmentId, status, quantity, expectedQuantity} = equipment;
|
||||||
|
|
||||||
|
// Cas 1: Équipement PERDU
|
||||||
|
if (status === 'LOST') {
|
||||||
|
const alertData = await createAlertInFirestore({
|
||||||
|
type: 'LOST',
|
||||||
|
severity: 'CRITICAL',
|
||||||
|
title: 'Équipement perdu',
|
||||||
|
message: `Équipement "${equipment.name || equipmentId}" perdu lors de l'événement "${eventName}" (${eventDate})`,
|
||||||
|
equipmentId,
|
||||||
|
eventId,
|
||||||
|
eventName,
|
||||||
|
eventDate,
|
||||||
|
createdBy: auth.uid,
|
||||||
|
metadata: {
|
||||||
|
validationType,
|
||||||
|
equipment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
alerts.push(alertData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cas 2: Équipement MANQUANT
|
||||||
|
if (status === 'MISSING') {
|
||||||
|
const alertData = await createAlertInFirestore({
|
||||||
|
type: 'EQUIPMENT_MISSING',
|
||||||
|
severity: 'WARNING',
|
||||||
|
title: 'Équipement manquant',
|
||||||
|
message: `Équipement "${equipment.name || equipmentId}" manquant pour l'événement "${eventName}" (${eventDate})`,
|
||||||
|
equipmentId,
|
||||||
|
eventId,
|
||||||
|
eventName,
|
||||||
|
eventDate,
|
||||||
|
createdBy: auth.uid,
|
||||||
|
metadata: {
|
||||||
|
validationType,
|
||||||
|
equipment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
alerts.push(alertData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cas 3: Quantité incorrecte
|
||||||
|
if (expectedQuantity && quantity !== expectedQuantity) {
|
||||||
|
const alertData = await createAlertInFirestore({
|
||||||
|
type: 'QUANTITY_MISMATCH',
|
||||||
|
severity: 'INFO',
|
||||||
|
title: 'Quantité incorrecte',
|
||||||
|
message: `Quantité incorrecte pour "${equipment.name || equipmentId}": ${quantity} au lieu de ${expectedQuantity} attendus`,
|
||||||
|
equipmentId,
|
||||||
|
eventId,
|
||||||
|
eventName,
|
||||||
|
eventDate,
|
||||||
|
createdBy: auth.uid,
|
||||||
|
metadata: {
|
||||||
|
validationType,
|
||||||
|
equipment,
|
||||||
|
expected: expectedQuantity,
|
||||||
|
actual: quantity,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
alerts.push(alertData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cas 4: Équipement endommagé
|
||||||
|
if (status === 'DAMAGED') {
|
||||||
|
const alertData = await createAlertInFirestore({
|
||||||
|
type: 'DAMAGED',
|
||||||
|
severity: 'WARNING',
|
||||||
|
title: 'Équipement endommagé',
|
||||||
|
message: `Équipement "${equipment.name || equipmentId}" endommagé durant l'événement "${eventName}" (${eventDate})`,
|
||||||
|
equipmentId,
|
||||||
|
eventId,
|
||||||
|
eventName,
|
||||||
|
eventDate,
|
||||||
|
createdBy: auth.uid,
|
||||||
|
metadata: {
|
||||||
|
validationType,
|
||||||
|
equipment,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
alerts.push(alertData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Mettre à jour les équipements de l'événement
|
||||||
|
await eventRef.update({
|
||||||
|
equipment: equipmentList,
|
||||||
|
lastValidation: {
|
||||||
|
type: validationType,
|
||||||
|
timestamp: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
by: auth.uid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Envoyer les notifications pour les alertes critiques
|
||||||
|
const criticalAlerts = alerts.filter((a) => a.severity === 'CRITICAL');
|
||||||
|
if (criticalAlerts.length > 0) {
|
||||||
|
for (const alert of criticalAlerts) {
|
||||||
|
try {
|
||||||
|
await sendAlertNotifications(alert, eventId);
|
||||||
|
} catch (notificationError) {
|
||||||
|
logger.error(`[processEquipmentValidation] Erreur notification alerte ${alert.id}:`, notificationError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
alertsCreated: alerts.length,
|
||||||
|
criticalAlertsCount: criticalAlerts.length,
|
||||||
|
alertIds: alerts.map((a) => a.id),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[processEquipmentValidation] Erreur:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée une alerte dans Firestore
|
||||||
|
*/
|
||||||
|
async function createAlertInFirestore(alertData) {
|
||||||
|
const db = admin.firestore();
|
||||||
|
const alertRef = db.collection('alerts').doc();
|
||||||
|
|
||||||
|
const fullAlertData = {
|
||||||
|
id: alertRef.id,
|
||||||
|
...alertData,
|
||||||
|
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
isRead: false,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
emailSent: false,
|
||||||
|
assignedTo: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await alertRef.set(fullAlertData);
|
||||||
|
|
||||||
|
return {...fullAlertData, id: alertRef.id};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détermine les utilisateurs à notifier et envoie les notifications
|
||||||
|
*/
|
||||||
|
async function sendAlertNotifications(alert, eventId) {
|
||||||
|
const db = admin.firestore();
|
||||||
|
const targetUserIds = new Set();
|
||||||
|
const usersWithPermission = new Set();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Récupérer TOUS les utilisateurs et leurs permissions
|
||||||
|
const allUsersSnapshot = await db.collection('users').get();
|
||||||
|
|
||||||
|
// Créer un map pour stocker les références de rôles à récupérer
|
||||||
|
const roleRefs = new Map();
|
||||||
|
|
||||||
|
for (const doc of allUsersSnapshot.docs) {
|
||||||
|
const user = doc.data();
|
||||||
|
|
||||||
|
if (!user.role) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraire le chemin du rôle
|
||||||
|
let rolePath = '';
|
||||||
|
let roleId = '';
|
||||||
|
|
||||||
|
if (typeof user.role === 'string') {
|
||||||
|
rolePath = user.role;
|
||||||
|
roleId = user.role.split('/').pop();
|
||||||
|
} else if (user.role.path) {
|
||||||
|
rolePath = user.role.path;
|
||||||
|
roleId = user.role.path.split('/').pop();
|
||||||
|
} else if (user.role._path && user.role._path.segments) {
|
||||||
|
rolePath = user.role._path.segments.join('/');
|
||||||
|
roleId = user.role._path.segments[user.role._path.segments.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roleId && !roleRefs.has(roleId)) {
|
||||||
|
roleRefs.set(roleId, {users: [], rolePath});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roleId) {
|
||||||
|
roleRefs.get(roleId).users.push(doc.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Récupérer les permissions de chaque rôle unique
|
||||||
|
for (const [roleId, {users, rolePath}] of roleRefs.entries()) {
|
||||||
|
try {
|
||||||
|
const roleDoc = await db.collection('roles').doc(roleId).get();
|
||||||
|
|
||||||
|
if (roleDoc.exists) {
|
||||||
|
const roleData = roleDoc.data();
|
||||||
|
const permissions = roleData.permissions || [];
|
||||||
|
|
||||||
|
// Vérifier si le rôle a la permission view_all_events
|
||||||
|
if (permissions.includes('view_all_events')) {
|
||||||
|
users.forEach((userId) => {
|
||||||
|
usersWithPermission.add(userId);
|
||||||
|
targetUserIds.add(userId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[sendAlertNotifications] Erreur récupération rôle ${roleId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Ajouter la workforce de l'événement
|
||||||
|
if (eventId) {
|
||||||
|
const eventDoc = await db.collection('events').doc(eventId).get();
|
||||||
|
|
||||||
|
if (eventDoc.exists) {
|
||||||
|
const event = eventDoc.data();
|
||||||
|
const workforce = event.workforce || [];
|
||||||
|
|
||||||
|
workforce.forEach((member) => {
|
||||||
|
// Extraire l'userId selon différentes structures possibles
|
||||||
|
let userId = null;
|
||||||
|
|
||||||
|
if (typeof member === 'string') {
|
||||||
|
userId = member;
|
||||||
|
} else if (member.userId) {
|
||||||
|
userId = member.userId;
|
||||||
|
} else if (member.id) {
|
||||||
|
userId = member.id;
|
||||||
|
} else if (member.user) {
|
||||||
|
if (typeof member.user === 'string') {
|
||||||
|
userId = member.user;
|
||||||
|
} else if (member.user.id) {
|
||||||
|
userId = member.user.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
targetUserIds.add(userId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userIds = Array.from(targetUserIds);
|
||||||
|
|
||||||
|
// 4. Mettre à jour l'alerte avec la liste des utilisateurs
|
||||||
|
await db.collection('alerts').doc(alert.id).update({
|
||||||
|
assignedTo: userIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. Envoyer les emails si alerte critique
|
||||||
|
if (alert.severity === 'CRITICAL') {
|
||||||
|
await sendAlertEmails(alert, userIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIds;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[sendAlertNotifications] Erreur:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie les emails d'alerte
|
||||||
|
*/
|
||||||
|
async function sendAlertEmails(alert, userIds) {
|
||||||
|
try {
|
||||||
|
const {renderTemplate, getEmailSubject, prepareTemplateData} = require('./utils/emailTemplates');
|
||||||
|
const db = admin.firestore();
|
||||||
|
|
||||||
|
// Vérifier que EMAIL_CONFIG est disponible
|
||||||
|
if (!EMAIL_CONFIG || !EMAIL_CONFIG.from) {
|
||||||
|
logger.error('[sendAlertEmails] EMAIL_CONFIG non configuré');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport(getSmtpConfig());
|
||||||
|
let successCount = 0;
|
||||||
|
|
||||||
|
// Envoyer les emails par lots de 5
|
||||||
|
const batches = [];
|
||||||
|
for (let i = 0; i < userIds.length; i += 5) {
|
||||||
|
batches.push(userIds.slice(i, i + 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const batch of batches) {
|
||||||
|
const promises = batch.map(async (userId) => {
|
||||||
|
try {
|
||||||
|
// Récupérer l'utilisateur
|
||||||
|
const userDoc = await db.collection('users').doc(userId).get();
|
||||||
|
|
||||||
|
if (!userDoc.exists) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userDoc.data();
|
||||||
|
|
||||||
|
// Vérifier les préférences email
|
||||||
|
const prefs = user.notificationPreferences || {};
|
||||||
|
if (!prefs.emailEnabled) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.email) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Préparer et envoyer l'email
|
||||||
|
let html;
|
||||||
|
try {
|
||||||
|
const templateData = await prepareTemplateData(alert, user);
|
||||||
|
html = await renderTemplate('alert-individual', templateData);
|
||||||
|
} catch (templateError) {
|
||||||
|
logger.error(`[sendAlertEmails] Erreur template pour ${userId}:`, templateError);
|
||||||
|
html = `
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h2>${alert.title || 'Nouvelle alerte'}</h2>
|
||||||
|
<p>${alert.message}</p>
|
||||||
|
<a href="${EMAIL_CONFIG.appUrl}/alerts">Voir l'alerte</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: `"${EMAIL_CONFIG.from.name}" <${EMAIL_CONFIG.from.address}>`,
|
||||||
|
to: user.email,
|
||||||
|
replyTo: EMAIL_CONFIG.replyTo,
|
||||||
|
subject: getEmailSubject(alert),
|
||||||
|
html: html,
|
||||||
|
text: alert.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[sendAlertEmails] Erreur email ${userId}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
successCount += results.filter((r) => r).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour l'alerte
|
||||||
|
await db.collection('alerts').doc(alert.id).update({
|
||||||
|
emailSent: true,
|
||||||
|
emailSentAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
emailsSentCount: successCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
return successCount;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[sendAlertEmails] Erreur globale:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formate la date d'un événement
|
||||||
|
*/
|
||||||
|
function formatEventDate(event) {
|
||||||
|
if (event.startDate) {
|
||||||
|
const date = event.startDate.toDate ? event.startDate.toDate() : new Date(event.startDate);
|
||||||
|
return date.toLocaleDateString('fr-FR', {day: 'numeric', month: 'numeric', year: 'numeric'});
|
||||||
|
}
|
||||||
|
return 'Date inconnue';
|
||||||
|
}
|
||||||
|
|
||||||
277
em2rp/functions/sendAlertEmail.js
Normal file
277
em2rp/functions/sendAlertEmail.js
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
const functions = require('firebase-functions');
|
||||||
|
const admin = require('firebase-admin');
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
const handlebars = require('handlebars');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const {getSmtpConfig, EMAIL_CONFIG} = require('./utils/emailConfig');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un email d'alerte à un utilisateur
|
||||||
|
* Appelé par le client Dart via callable function
|
||||||
|
*/
|
||||||
|
exports.sendAlertEmail = functions.https.onCall(async (data, context) => {
|
||||||
|
// Vérifier l'authentification
|
||||||
|
if (!context.auth) {
|
||||||
|
throw new functions.https.HttpsError(
|
||||||
|
'unauthenticated',
|
||||||
|
'L\'utilisateur doit être authentifié',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {alertId, userId, templateType} = data;
|
||||||
|
|
||||||
|
if (!alertId || !userId) {
|
||||||
|
throw new functions.https.HttpsError(
|
||||||
|
'invalid-argument',
|
||||||
|
'alertId et userId sont requis',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Récupérer l'alerte depuis Firestore
|
||||||
|
const alertDoc = await admin.firestore()
|
||||||
|
.collection('alerts')
|
||||||
|
.doc(alertId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (!alertDoc.exists) {
|
||||||
|
throw new functions.https.HttpsError(
|
||||||
|
'not-found',
|
||||||
|
'Alerte introuvable',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const alert = alertDoc.data();
|
||||||
|
|
||||||
|
// Récupérer l'utilisateur
|
||||||
|
const userDoc = await admin.firestore()
|
||||||
|
.collection('users')
|
||||||
|
.doc(userId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (!userDoc.exists) {
|
||||||
|
throw new functions.https.HttpsError(
|
||||||
|
'not-found',
|
||||||
|
'Utilisateur introuvable',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userDoc.data();
|
||||||
|
|
||||||
|
// Vérifier les préférences email de l'utilisateur
|
||||||
|
const prefs = user.notificationPreferences || {};
|
||||||
|
if (!prefs.emailEnabled) {
|
||||||
|
console.log(`Email désactivé pour l'utilisateur ${userId}`);
|
||||||
|
return {success: true, skipped: true, reason: 'email_disabled'};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier la préférence pour ce type d'alerte
|
||||||
|
const alertType = alert.type;
|
||||||
|
const shouldSend = checkAlertPreference(alertType, prefs);
|
||||||
|
if (!shouldSend) {
|
||||||
|
console.log(`Type d'alerte ${alertType} désactivé pour ${userId}`);
|
||||||
|
return {success: true, skipped: true, reason: 'alert_type_disabled'};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Préparer les données pour le template
|
||||||
|
const templateData = await prepareTemplateData(alert, user);
|
||||||
|
|
||||||
|
// Rendre le template HTML
|
||||||
|
const html = await renderTemplate(
|
||||||
|
templateType || 'alert-individual',
|
||||||
|
templateData,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Configurer le transporteur SMTP
|
||||||
|
const transporter = nodemailer.createTransporter(getSmtpConfig());
|
||||||
|
|
||||||
|
// Envoyer l'email
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: `"${EMAIL_CONFIG.from.name}" <${EMAIL_CONFIG.from.address}>`,
|
||||||
|
to: user.email,
|
||||||
|
replyTo: EMAIL_CONFIG.replyTo,
|
||||||
|
subject: getEmailSubject(alert),
|
||||||
|
html: html,
|
||||||
|
// Fallback texte brut
|
||||||
|
text: alert.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Email envoyé:', info.messageId);
|
||||||
|
|
||||||
|
// Marquer l'email comme envoyé dans l'alerte
|
||||||
|
await alertDoc.ref.update({
|
||||||
|
emailSent: true,
|
||||||
|
emailSentAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
messageId: info.messageId,
|
||||||
|
skipped: false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur envoi email:', error);
|
||||||
|
throw new functions.https.HttpsError(
|
||||||
|
'internal',
|
||||||
|
`Erreur lors de l'envoi de l'email: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur souhaite recevoir ce type d'alerte
|
||||||
|
*/
|
||||||
|
function checkAlertPreference(alertType, preferences) {
|
||||||
|
const typeMapping = {
|
||||||
|
'EVENT_CREATED': 'eventsNotifications',
|
||||||
|
'EVENT_MODIFIED': 'eventsNotifications',
|
||||||
|
'EVENT_CANCELLED': 'eventsNotifications',
|
||||||
|
'LOST': 'equipmentNotifications',
|
||||||
|
'EQUIPMENT_MISSING': 'equipmentNotifications',
|
||||||
|
'MAINTENANCE_REMINDER': 'maintenanceNotifications',
|
||||||
|
'STOCK_LOW': 'stockNotifications',
|
||||||
|
};
|
||||||
|
|
||||||
|
const prefKey = typeMapping[alertType];
|
||||||
|
return prefKey ? (preferences[prefKey] !== false) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prépare les données pour le template
|
||||||
|
*/
|
||||||
|
async function prepareTemplateData(alert, user) {
|
||||||
|
const data = {
|
||||||
|
userName: `${user.firstName || ''} ${user.lastName || ''}`.trim() ||
|
||||||
|
'Utilisateur',
|
||||||
|
alertTitle: getAlertTitle(alert.type),
|
||||||
|
alertMessage: alert.message,
|
||||||
|
isCritical: alert.severity === 'CRITICAL',
|
||||||
|
actionUrl: `${EMAIL_CONFIG.appUrl}${alert.actionUrl || '/alerts'}`,
|
||||||
|
appUrl: EMAIL_CONFIG.appUrl,
|
||||||
|
unsubscribeUrl: `${EMAIL_CONFIG.appUrl}/my_account?tab=notifications`,
|
||||||
|
year: new Date().getFullYear(),
|
||||||
|
subject: getEmailSubject(alert),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ajouter des détails selon le type d'alerte
|
||||||
|
if (alert.eventId) {
|
||||||
|
try {
|
||||||
|
const eventDoc = await admin.firestore()
|
||||||
|
.collection('events')
|
||||||
|
.doc(alert.eventId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (eventDoc.exists) {
|
||||||
|
const event = eventDoc.data();
|
||||||
|
data.eventName = event.Name;
|
||||||
|
if (event.StartDateTime) {
|
||||||
|
const date = event.StartDateTime.toDate();
|
||||||
|
data.eventDate = date.toLocaleDateString('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur récupération événement:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alert.equipmentId) {
|
||||||
|
try {
|
||||||
|
const eqDoc = await admin.firestore()
|
||||||
|
.collection('equipments')
|
||||||
|
.doc(alert.equipmentId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (eqDoc.exists) {
|
||||||
|
data.equipmentName = eqDoc.data().name;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur récupération équipement:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère le titre de l'email selon le type d'alerte
|
||||||
|
*/
|
||||||
|
function getEmailSubject(alert) {
|
||||||
|
const subjects = {
|
||||||
|
'EVENT_CREATED': '📅 Nouvel événement créé',
|
||||||
|
'EVENT_MODIFIED': '📝 Événement modifié',
|
||||||
|
'EVENT_CANCELLED': '❌ Événement annulé',
|
||||||
|
'LOST': '🔴 Alerte critique : Équipement perdu',
|
||||||
|
'EQUIPMENT_MISSING': '⚠️ Équipement manquant',
|
||||||
|
'MAINTENANCE_REMINDER': '🔧 Rappel de maintenance',
|
||||||
|
'STOCK_LOW': '📦 Stock faible',
|
||||||
|
};
|
||||||
|
|
||||||
|
return subjects[alert.type] || '🔔 Nouvelle alerte - EM2 Events';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère le titre pour le corps de l'email
|
||||||
|
*/
|
||||||
|
function getAlertTitle(type) {
|
||||||
|
const titles = {
|
||||||
|
'EVENT_CREATED': 'Nouvel événement créé',
|
||||||
|
'EVENT_MODIFIED': 'Événement modifié',
|
||||||
|
'EVENT_CANCELLED': 'Événement annulé',
|
||||||
|
'LOST': 'Équipement perdu',
|
||||||
|
'EQUIPMENT_MISSING': 'Équipement manquant',
|
||||||
|
'MAINTENANCE_REMINDER': 'Maintenance requise',
|
||||||
|
'STOCK_LOW': 'Stock faible',
|
||||||
|
};
|
||||||
|
|
||||||
|
return titles[type] || 'Nouvelle alerte';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rend un template HTML avec Handlebars
|
||||||
|
*/
|
||||||
|
async function renderTemplate(templateName, data) {
|
||||||
|
try {
|
||||||
|
// Lire le template de base
|
||||||
|
const basePath = path.join(__dirname, 'templates', 'base-template.html');
|
||||||
|
const baseTemplate = await fs.readFile(basePath, 'utf8');
|
||||||
|
|
||||||
|
// Lire le template de contenu
|
||||||
|
const contentPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'templates',
|
||||||
|
`${templateName}.html`,
|
||||||
|
);
|
||||||
|
const contentTemplate = await fs.readFile(contentPath, 'utf8');
|
||||||
|
|
||||||
|
// Compiler les templates
|
||||||
|
const compileContent = handlebars.compile(contentTemplate);
|
||||||
|
const compileBase = handlebars.compile(baseTemplate);
|
||||||
|
|
||||||
|
// Rendre le contenu
|
||||||
|
const renderedContent = compileContent(data);
|
||||||
|
|
||||||
|
// Rendre le template de base avec le contenu
|
||||||
|
return compileBase({
|
||||||
|
...data,
|
||||||
|
content: renderedContent,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur rendu template:', error);
|
||||||
|
// Fallback vers un template simple
|
||||||
|
return `
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h2>${data.alertTitle}</h2>
|
||||||
|
<p>${data.alertMessage}</p>
|
||||||
|
<a href="${data.actionUrl}">Voir l'alerte</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
267
em2rp/functions/sendDailyDigest.js
Normal file
267
em2rp/functions/sendDailyDigest.js
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
/**
|
||||||
|
* Fonction schedulée : Envoie quotidienne d'un résumé des alertes non lues
|
||||||
|
* S'exécute tous les jours à 8h00 (Europe/Paris)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const admin = require('firebase-admin');
|
||||||
|
const logger = require('firebase-functions/logger');
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
const { getSmtpConfig } = require('./utils/emailConfig');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fonction principale : envoie le digest quotidien
|
||||||
|
*/
|
||||||
|
async function sendDailyDigest() {
|
||||||
|
const db = admin.firestore();
|
||||||
|
|
||||||
|
logger.info('[sendDailyDigest] ===== DÉBUT ENVOI DIGEST QUOTIDIEN =====');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Récupérer tous les utilisateurs avec email activé
|
||||||
|
const usersSnapshot = await db.collection('users').get();
|
||||||
|
const eligibleUsers = [];
|
||||||
|
|
||||||
|
usersSnapshot.forEach((doc) => {
|
||||||
|
const user = doc.data();
|
||||||
|
const prefs = user.notificationPreferences || {};
|
||||||
|
|
||||||
|
// Vérifier si l'utilisateur a activé les emails
|
||||||
|
if (prefs.emailEnabled !== false && user.email) {
|
||||||
|
eligibleUsers.push({
|
||||||
|
uid: doc.id,
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName || 'Utilisateur',
|
||||||
|
lastName: user.lastName || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`[sendDailyDigest] ${eligibleUsers.length} utilisateurs éligibles`);
|
||||||
|
|
||||||
|
// 2. Pour chaque utilisateur, récupérer ses alertes non lues des dernières 24h
|
||||||
|
const now = admin.firestore.Timestamp.now();
|
||||||
|
const yesterday = admin.firestore.Timestamp.fromMillis(now.toMillis() - 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport(getSmtpConfig());
|
||||||
|
let emailsSent = 0;
|
||||||
|
|
||||||
|
for (const user of eligibleUsers) {
|
||||||
|
try {
|
||||||
|
// Récupérer les alertes non lues de l'utilisateur créées dans les dernières 24h
|
||||||
|
const alertsSnapshot = await db.collection('alerts')
|
||||||
|
.where('assignedTo', 'array-contains', user.uid)
|
||||||
|
.where('isRead', '==', false)
|
||||||
|
.where('createdAt', '>=', yesterday)
|
||||||
|
.orderBy('createdAt', 'desc')
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (alertsSnapshot.empty) {
|
||||||
|
continue; // Pas d'alertes non lues pour cet utilisateur
|
||||||
|
}
|
||||||
|
|
||||||
|
const alerts = [];
|
||||||
|
alertsSnapshot.forEach((doc) => {
|
||||||
|
alerts.push({ id: doc.id, ...doc.data() });
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`[sendDailyDigest] ${user.email}: ${alerts.length} alertes non lues`);
|
||||||
|
|
||||||
|
// 3. Envoyer l'email de digest
|
||||||
|
const sent = await sendDigestEmail(transporter, user, alerts);
|
||||||
|
if (sent) {
|
||||||
|
emailsSent++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[sendDailyDigest] Erreur pour ${user.email}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[sendDailyDigest] ✓ ${emailsSent}/${eligibleUsers.length} emails envoyés`);
|
||||||
|
logger.info('[sendDailyDigest] ===== FIN DIGEST QUOTIDIEN =====');
|
||||||
|
|
||||||
|
return { success: true, emailsSent };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[sendDailyDigest] Erreur globale:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie l'email de digest à un utilisateur
|
||||||
|
*/
|
||||||
|
async function sendDigestEmail(transporter, user, alerts) {
|
||||||
|
try {
|
||||||
|
// Grouper les alertes par sévérité
|
||||||
|
const criticalAlerts = alerts.filter(a => a.severity === 'CRITICAL');
|
||||||
|
const warningAlerts = alerts.filter(a => a.severity === 'WARNING');
|
||||||
|
const infoAlerts = alerts.filter(a => a.severity === 'INFO');
|
||||||
|
|
||||||
|
// Construire le HTML
|
||||||
|
const html = buildDigestHtml(user, {
|
||||||
|
critical: criticalAlerts,
|
||||||
|
warning: warningAlerts,
|
||||||
|
info: infoAlerts,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Envoyer l'email
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: `"EM2RP Notifications" <${process.env.SMTP_USER}>`,
|
||||||
|
to: user.email,
|
||||||
|
subject: `📬 ${alerts.length} nouvelle(s) alerte(s) EM2RP`,
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`[sendDigestEmail] ✓ Email envoyé à ${user.email}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[sendDigestEmail] Erreur pour ${user.email}:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit le HTML du digest
|
||||||
|
*/
|
||||||
|
function buildDigestHtml(user, alertsByType) {
|
||||||
|
const totalAlerts = alertsByType.critical.length + alertsByType.warning.length + alertsByType.info.length;
|
||||||
|
|
||||||
|
let alertsHtml = '';
|
||||||
|
|
||||||
|
// Alertes critiques
|
||||||
|
if (alertsByType.critical.length > 0) {
|
||||||
|
alertsHtml += `
|
||||||
|
<div style="margin-bottom: 24px;">
|
||||||
|
<h3 style="color: #dc2626; margin: 0 0 12px 0;">
|
||||||
|
🔴 Alertes critiques (${alertsByType.critical.length})
|
||||||
|
</h3>
|
||||||
|
${alertsByType.critical.map(alert => formatAlertItem(alert)).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alertes warning
|
||||||
|
if (alertsByType.warning.length > 0) {
|
||||||
|
alertsHtml += `
|
||||||
|
<div style="margin-bottom: 24px;">
|
||||||
|
<h3 style="color: #f59e0b; margin: 0 0 12px 0;">
|
||||||
|
⚠️ Avertissements (${alertsByType.warning.length})
|
||||||
|
</h3>
|
||||||
|
${alertsByType.warning.map(alert => formatAlertItem(alert)).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alertes info
|
||||||
|
if (alertsByType.info.length > 0) {
|
||||||
|
alertsHtml += `
|
||||||
|
<div style="margin-bottom: 24px;">
|
||||||
|
<h3 style="color: #3b82f6; margin: 0 0 12px 0;">
|
||||||
|
ℹ️ Informations (${alertsByType.info.length})
|
||||||
|
</h3>
|
||||||
|
${alertsByType.info.map(alert => formatAlertItem(alert)).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f9fafb;">
|
||||||
|
<!-- En-tête -->
|
||||||
|
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 32px; border-radius: 12px 12px 0 0; text-align: center;">
|
||||||
|
<h1 style="color: white; margin: 0; font-size: 28px;">📬 Résumé quotidien</h1>
|
||||||
|
<p style="color: rgba(255,255,255,0.9); margin: 8px 0 0 0; font-size: 16px;">
|
||||||
|
Bonjour ${user.firstName},
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contenu -->
|
||||||
|
<div style="background-color: white; padding: 32px; border-radius: 0 0 12px 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
||||||
|
<p style="color: #374151; font-size: 16px; line-height: 1.6; margin: 0 0 24px 0;">
|
||||||
|
Vous avez <strong>${totalAlerts} nouvelle(s) alerte(s)</strong> dans les dernières 24 heures.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
${alertsHtml}
|
||||||
|
|
||||||
|
<div style="margin-top: 32px; padding-top: 24px; border-top: 1px solid #e5e7eb; text-align: center;">
|
||||||
|
<a href="https://app.em2event.fr/#/alerts"
|
||||||
|
style="display: inline-block; background-color: #667eea; color: white; padding: 12px 32px; text-decoration: none; border-radius: 8px; font-weight: 600;">
|
||||||
|
Voir toutes les alertes
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pied de page -->
|
||||||
|
<div style="text-align: center; padding: 24px; color: #6b7280; font-size: 14px;">
|
||||||
|
<p style="margin: 0 0 8px 0;">EM2RP - Gestion d'événements</p>
|
||||||
|
<p style="margin: 0;">
|
||||||
|
<a href="https://app.em2event.fr/#/settings" style="color: #667eea; text-decoration: none;">
|
||||||
|
Gérer mes préférences de notification
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formate un item d'alerte pour l'email
|
||||||
|
*/
|
||||||
|
function formatAlertItem(alert) {
|
||||||
|
const date = alert.createdAt?.toDate ?
|
||||||
|
new Date(alert.createdAt.toDate()).toLocaleString('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}) :
|
||||||
|
'Date inconnue';
|
||||||
|
|
||||||
|
// Type d'alerte en français
|
||||||
|
const typeLabels = {
|
||||||
|
'EQUIPMENT_MISSING': 'Équipement manquant',
|
||||||
|
'LOST': 'Équipement perdu',
|
||||||
|
'DAMAGED': 'Équipement endommagé',
|
||||||
|
'QUANTITY_MISMATCH': 'Écart de quantité',
|
||||||
|
'EVENT_CREATED': 'Événement créé',
|
||||||
|
'EVENT_MODIFIED': 'Événement modifié',
|
||||||
|
'WORKFORCE_ADDED': 'Ajout à la workforce',
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeLabel = typeLabels[alert.type] || alert.type;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div style="background-color: #f9fafb; padding: 16px; border-radius: 8px; margin-bottom: 12px; border-left: 4px solid ${getSeverityColor(alert.severity)};">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
|
||||||
|
<strong style="color: #111827; font-size: 15px;">${typeLabel}</strong>
|
||||||
|
<span style="color: #6b7280; font-size: 13px;">${date}</span>
|
||||||
|
</div>
|
||||||
|
<p style="color: #4b5563; margin: 0; font-size: 14px; line-height: 1.5;">
|
||||||
|
${alert.message || 'Aucun message'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne la couleur selon la sévérité
|
||||||
|
*/
|
||||||
|
function getSeverityColor(severity) {
|
||||||
|
switch (severity) {
|
||||||
|
case 'CRITICAL': return '#dc2626';
|
||||||
|
case 'WARNING': return '#f59e0b';
|
||||||
|
case 'INFO': return '#3b82f6';
|
||||||
|
default: return '#6b7280';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { sendDailyDigest };
|
||||||
|
|
||||||
107
em2rp/functions/templates/alert-digest.html
Normal file
107
em2rp/functions/templates/alert-digest.html
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<div style="margin-bottom: 30px;">
|
||||||
|
<!-- En-tête du digest -->
|
||||||
|
<div style="margin-bottom: 25px;">
|
||||||
|
<h2 style="color: #111827; margin: 0 0 10px 0; font-size: 24px; font-weight: 600;">
|
||||||
|
📬 Votre résumé quotidien
|
||||||
|
</h2>
|
||||||
|
<p style="color: #6b7280; margin: 0; font-size: 14px;">
|
||||||
|
{{digestDate}} • {{alertCount}} nouvelle(s) alerte(s)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message d'introduction -->
|
||||||
|
<p style="color: #374151; margin: 0 0 30px 0; font-size: 16px; line-height: 1.6;">
|
||||||
|
Bonjour <strong>{{userName}}</strong>,<br>
|
||||||
|
Voici le récapitulatif de vos alertes des dernières 24 heures.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Liste des alertes -->
|
||||||
|
{{#each alerts}}
|
||||||
|
<div style="background-color: #f9fafb; border-left: 4px solid {{#if this.isCritical}}#DC2626{{else}}#3B82F6{{/if}}; padding: 20px; margin-bottom: 15px; border-radius: 4px;">
|
||||||
|
<!-- Badge type -->
|
||||||
|
<div style="display: inline-block; padding: 4px 12px; border-radius: 12px; margin-bottom: 10px; background-color: {{#if this.isCritical}}#FEE2E2{{else}}#DBEAFE{{/if}}; color: {{#if this.isCritical}}#991B1B{{else}}#1E40AF{{/if}}; font-size: 11px; font-weight: 600; text-transform: uppercase;">
|
||||||
|
{{this.typeLabel}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Titre de l'alerte -->
|
||||||
|
<h3 style="color: #111827; margin: 0 0 8px 0; font-size: 16px; font-weight: 600;">
|
||||||
|
{{this.title}}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Message -->
|
||||||
|
<p style="color: #4b5563; margin: 0 0 12px 0; font-size: 14px; line-height: 1.5;">
|
||||||
|
{{this.message}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Contexte -->
|
||||||
|
{{#if this.context}}
|
||||||
|
<p style="color: #6b7280; margin: 0; font-size: 13px;">
|
||||||
|
<strong>Contexte :</strong> {{this.context}}
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<!-- Timestamp -->
|
||||||
|
<p style="color: #9ca3af; margin: 8px 0 0 0; font-size: 12px;">
|
||||||
|
🕐 {{this.timestamp}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
<!-- Aucune alerte -->
|
||||||
|
{{#unless alerts}}
|
||||||
|
<div style="background-color: #f0fdf4; border: 1px solid #86efac; padding: 20px; margin-bottom: 20px; border-radius: 8px; text-align: center;">
|
||||||
|
<p style="color: #166534; margin: 0; font-size: 16px;">
|
||||||
|
✅ <strong>Aucune alerte aujourd'hui</strong><br>
|
||||||
|
<span style="font-size: 14px; color: #15803d;">Tout est en ordre !</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
|
<!-- Bouton d'action principal -->
|
||||||
|
<div style="text-align: center; margin-top: 30px;">
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="margin: 0 auto;">
|
||||||
|
<tr>
|
||||||
|
<td style="border-radius: 6px; background: #3B82F6;">
|
||||||
|
<a href="{{appUrl}}/alerts" target="_blank" style="display: inline-block; padding: 14px 30px; font-size: 16px; color: #ffffff; text-decoration: none; font-weight: 600; border-radius: 6px;">
|
||||||
|
Voir toutes mes alertes
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistiques -->
|
||||||
|
{{#if stats}}
|
||||||
|
<div style="margin-top: 30px; padding: 20px; background-color: #fef3c7; border-radius: 8px;">
|
||||||
|
<h3 style="color: #92400e; margin: 0 0 15px 0; font-size: 16px; font-weight: 600;">
|
||||||
|
📊 Vos statistiques
|
||||||
|
</h3>
|
||||||
|
<table style="width: 100%;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; font-size: 14px; color: #78350f;">
|
||||||
|
<strong>Alertes non lues :</strong>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 8px 0; font-size: 14px; color: #78350f; text-align: right;">
|
||||||
|
{{stats.unreadCount}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; font-size: 14px; color: #78350f;">
|
||||||
|
<strong>Événements en cours :</strong>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 8px 0; font-size: 14px; color: #78350f; text-align: right;">
|
||||||
|
{{stats.activeEvents}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<!-- Note de bas de page -->
|
||||||
|
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
|
||||||
|
<p style="margin: 0; font-size: 13px; color: #6b7280; line-height: 1.5;">
|
||||||
|
💡 Ce résumé est envoyé quotidiennement à 8h. Vous pouvez modifier cette préférence dans votre <a href="{{appUrl}}/my_account" style="color: #3B82F6; text-decoration: none;">espace personnel</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
81
em2rp/functions/templates/alert-individual.html
Normal file
81
em2rp/functions/templates/alert-individual.html
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<div style="margin-bottom: 30px;">
|
||||||
|
<!-- Badge de sévérité -->
|
||||||
|
<div style="display: inline-block; padding: 8px 16px; border-radius: 20px; margin-bottom: 20px; {{#if isCritical}}background-color: #FEE2E2; color: #991B1B;{{else}}background-color: #FEF3C7; color: #92400E;{{/if}}">
|
||||||
|
<strong style="font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||||
|
{{#if isCritical}}🔴 Alerte Critique{{else}}⚠️ Attention{{/if}}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Titre -->
|
||||||
|
<h2 style="color: #111827; margin: 0 0 20px 0; font-size: 24px; font-weight: 600;">
|
||||||
|
{{alertTitle}}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Message -->
|
||||||
|
<p style="color: #374151; margin: 0 0 25px 0; font-size: 16px; line-height: 1.6;">
|
||||||
|
{{alertMessage}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Détails de l'alerte -->
|
||||||
|
{{#if alertDetails}}
|
||||||
|
<div style="background-color: #f9fafb; border-left: 4px solid #3B82F6; padding: 16px; margin-bottom: 25px; border-radius: 4px;">
|
||||||
|
<p style="margin: 0; font-size: 14px; color: #6b7280;">
|
||||||
|
<strong style="color: #374151;">Détails :</strong><br>
|
||||||
|
{{alertDetails}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<!-- Informations contextuelles -->
|
||||||
|
{{#if eventName}}
|
||||||
|
<table style="width: 100%; margin-bottom: 25px; border-collapse: collapse;">
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; font-size: 14px; color: #6b7280;">
|
||||||
|
<strong style="color: #374151;">Événement :</strong>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 8px 0; font-size: 14px; color: #374151;">
|
||||||
|
{{eventName}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{#if eventDate}}
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; font-size: 14px; color: #6b7280;">
|
||||||
|
<strong style="color: #374151;">Date :</strong>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 8px 0; font-size: 14px; color: #374151;">
|
||||||
|
{{eventDate}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/if}}
|
||||||
|
{{#if equipmentName}}
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 8px 0; font-size: 14px; color: #6b7280;">
|
||||||
|
<strong style="color: #374151;">Équipement :</strong>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 8px 0; font-size: 14px; color: #374151;">
|
||||||
|
{{equipmentName}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/if}}
|
||||||
|
</table>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<!-- Bouton d'action -->
|
||||||
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
|
||||||
|
<tr>
|
||||||
|
<td style="border-radius: 6px; {{#if isCritical}}background: #DC2626;{{else}}background: #3B82F6;{{/if}}">
|
||||||
|
<a href="{{actionUrl}}" target="_blank" style="display: inline-block; padding: 14px 30px; font-size: 16px; color: #ffffff; text-decoration: none; font-weight: 600; border-radius: 6px;">
|
||||||
|
{{#if isCritical}}Voir l'alerte immédiatement{{else}}Consulter les détails{{/if}}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Note de bas de page -->
|
||||||
|
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
|
||||||
|
<p style="margin: 0; font-size: 13px; color: #6b7280; line-height: 1.5;">
|
||||||
|
💡 <strong>Astuce :</strong> Vous pouvez gérer vos préférences de notifications dans votre <a href="{{appUrl}}/my_account" style="color: #3B82F6; text-decoration: none;">espace personnel</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
65
em2rp/functions/templates/base-template.html
Normal file
65
em2rp/functions/templates/base-template.html
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>{{subject}}</title>
|
||||||
|
<style>
|
||||||
|
/* Reset styles */
|
||||||
|
body, table, td, a { -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; }
|
||||||
|
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
||||||
|
img { -ms-interpolation-mode: bicubic; border: 0; outline: none; text-decoration: none; }
|
||||||
|
body { margin: 0; padding: 0; width: 100% !important; height: 100% !important; }
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.container { width: 100% !important; }
|
||||||
|
.content { padding: 20px !important; }
|
||||||
|
.button { width: 100% !important; display: block !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body style="margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f3f4f6;">
|
||||||
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f3f4f6;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="padding: 40px 0;">
|
||||||
|
<!-- Container -->
|
||||||
|
<table role="presentation" class="container" width="600" cellpadding="0" cellspacing="0" border="0" style="background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
|
||||||
|
<!-- Header -->
|
||||||
|
<tr>
|
||||||
|
<td align="center" style="background: linear-gradient(135deg, #1E3A8A 0%, #3B82F6 100%); padding: 30px; border-radius: 8px 8px 0 0;">
|
||||||
|
<h1 style="color: #ffffff; margin: 0; font-size: 28px; font-weight: bold;">
|
||||||
|
EM2 Events
|
||||||
|
</h1>
|
||||||
|
<p style="color: #E0E7FF; margin: 8px 0 0 0; font-size: 14px;">
|
||||||
|
Gestion d'événements professionnelle
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<tr>
|
||||||
|
<td class="content" style="padding: 40px 30px;">
|
||||||
|
{{{content}}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<tr>
|
||||||
|
<td style="background-color: #f9fafb; padding: 30px; border-radius: 0 0 8px 8px; border-top: 1px solid #e5e7eb;">
|
||||||
|
<p style="margin: 0 0 15px 0; font-size: 13px; color: #6b7280; text-align: center;">
|
||||||
|
Cet email a été envoyé automatiquement par EM2 Events
|
||||||
|
</p>
|
||||||
|
<p style="margin: 15px 0 0 0; font-size: 11px; color: #9ca3af; text-align: center;">
|
||||||
|
© {{year}} EM2 Events. Tous droits réservés.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/**
|
|
||||||
* Test rapide des Cloud Functions
|
|
||||||
* Vérifie que toutes les fonctions sont exportées correctement
|
|
||||||
*/
|
|
||||||
|
|
||||||
const functions = require('./index');
|
|
||||||
|
|
||||||
console.log('🧪 Test des Cloud Functions\n');
|
|
||||||
|
|
||||||
const expectedFunctions = [
|
|
||||||
'moveEventFileV2',
|
|
||||||
'createEquipment',
|
|
||||||
'updateEquipment',
|
|
||||||
'deleteEquipment',
|
|
||||||
'getEquipment',
|
|
||||||
'createContainer',
|
|
||||||
'updateContainer',
|
|
||||||
'deleteContainer',
|
|
||||||
'createEvent',
|
|
||||||
'updateEvent',
|
|
||||||
'deleteEvent',
|
|
||||||
'createMaintenance',
|
|
||||||
'updateMaintenance',
|
|
||||||
'createOption',
|
|
||||||
'updateOption',
|
|
||||||
'deleteOption',
|
|
||||||
'createUser',
|
|
||||||
'updateUser',
|
|
||||||
'updateEquipmentStatus'
|
|
||||||
];
|
|
||||||
|
|
||||||
let passed = 0;
|
|
||||||
let failed = 0;
|
|
||||||
|
|
||||||
for (const funcName of expectedFunctions) {
|
|
||||||
if (functions[funcName]) {
|
|
||||||
console.log(`✓ ${funcName}`);
|
|
||||||
passed++;
|
|
||||||
} else {
|
|
||||||
console.log(`✗ ${funcName} - MANQUANTE`);
|
|
||||||
failed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n📊 Résultats: ${passed} passées, ${failed} échouées`);
|
|
||||||
|
|
||||||
if (failed > 0) {
|
|
||||||
console.log('\n❌ Certaines fonctions sont manquantes !');
|
|
||||||
process.exit(1);
|
|
||||||
} else {
|
|
||||||
console.log('\n✅ Toutes les fonctions sont présentes !');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
39
em2rp/functions/utils/emailConfig.js
Normal file
39
em2rp/functions/utils/emailConfig.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Configuration SMTP pour l'envoi d'emails
|
||||||
|
* Les credentials sont stockés dans les variables d'environnement
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Configuration SMTP depuis les variables d'environnement
|
||||||
|
// Pour configurer : Définir SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS dans .env ou Firebase
|
||||||
|
const getSmtpConfig = () => {
|
||||||
|
return {
|
||||||
|
host: process.env.SMTP_HOST || 'mail.em2events.fr',
|
||||||
|
port: parseInt(process.env.SMTP_PORT || '465'),
|
||||||
|
secure: true, // true pour port 465, false pour autres ports
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER || 'notify@em2events.fr',
|
||||||
|
pass: process.env.SMTP_PASS || '',
|
||||||
|
},
|
||||||
|
tls: {
|
||||||
|
// Ne pas échouer sur certificats invalides
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configuration email par défaut
|
||||||
|
const EMAIL_CONFIG = {
|
||||||
|
from: {
|
||||||
|
name: 'EM2 Events',
|
||||||
|
address: 'notify@em2events.fr',
|
||||||
|
},
|
||||||
|
replyTo: 'contact@em2events.fr',
|
||||||
|
// URL de l'application pour les liens
|
||||||
|
appUrl: process.env.APP_URL || 'https://em2rp-951dc.web.app',
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getSmtpConfig,
|
||||||
|
EMAIL_CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
177
em2rp/functions/utils/emailTemplates.js
Normal file
177
em2rp/functions/utils/emailTemplates.js
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
const admin = require('firebase-admin');
|
||||||
|
const handlebars = require('handlebars');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
|
const {EMAIL_CONFIG} = require('./emailConfig');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si l'utilisateur souhaite recevoir ce type d'alerte
|
||||||
|
*/
|
||||||
|
function checkAlertPreference(alertType, preferences) {
|
||||||
|
const typeMapping = {
|
||||||
|
'EVENT_CREATED': 'eventsNotifications',
|
||||||
|
'EVENT_MODIFIED': 'eventsNotifications',
|
||||||
|
'EVENT_CANCELLED': 'eventsNotifications',
|
||||||
|
'LOST': 'equipmentNotifications',
|
||||||
|
'EQUIPMENT_MISSING': 'equipmentNotifications',
|
||||||
|
'DAMAGED': 'equipmentNotifications',
|
||||||
|
'QUANTITY_MISMATCH': 'equipmentNotifications',
|
||||||
|
'MAINTENANCE_REMINDER': 'maintenanceNotifications',
|
||||||
|
'STOCK_LOW': 'stockNotifications',
|
||||||
|
};
|
||||||
|
|
||||||
|
const prefKey = typeMapping[alertType];
|
||||||
|
return prefKey ? (preferences[prefKey] !== false) : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prépare les données pour le template
|
||||||
|
*/
|
||||||
|
async function prepareTemplateData(alert, user) {
|
||||||
|
const data = {
|
||||||
|
userName: `${user.firstName || ''} ${user.lastName || ''}`.trim() ||
|
||||||
|
'Utilisateur',
|
||||||
|
alertTitle: getAlertTitle(alert.type),
|
||||||
|
alertMessage: alert.message,
|
||||||
|
isCritical: alert.severity === 'CRITICAL',
|
||||||
|
actionUrl: `${EMAIL_CONFIG.appUrl}${alert.actionUrl || '/alerts'}`,
|
||||||
|
appUrl: EMAIL_CONFIG.appUrl,
|
||||||
|
unsubscribeUrl: `${EMAIL_CONFIG.appUrl}/my_account?tab=notifications`,
|
||||||
|
year: new Date().getFullYear(),
|
||||||
|
subject: getEmailSubject(alert),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ajouter des détails selon le type d'alerte
|
||||||
|
if (alert.eventId) {
|
||||||
|
try {
|
||||||
|
const eventDoc = await admin.firestore()
|
||||||
|
.collection('events')
|
||||||
|
.doc(alert.eventId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (eventDoc.exists) {
|
||||||
|
const event = eventDoc.data();
|
||||||
|
data.eventName = event.Name || event.name || 'Événement';
|
||||||
|
if (event.StartDateTime || event.startDate) {
|
||||||
|
const dateField = event.StartDateTime || event.startDate;
|
||||||
|
const date = dateField.toDate ? dateField.toDate() : new Date(dateField);
|
||||||
|
data.eventDate = date.toLocaleDateString('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignorer silencieusement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alert.equipmentId) {
|
||||||
|
try {
|
||||||
|
const eqDoc = await admin.firestore()
|
||||||
|
.collection('equipments')
|
||||||
|
.doc(alert.equipmentId)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
if (eqDoc.exists) {
|
||||||
|
data.equipmentName = eqDoc.data().name;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignorer silencieusement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère le titre de l'email selon le type d'alerte
|
||||||
|
*/
|
||||||
|
function getEmailSubject(alert) {
|
||||||
|
const subjects = {
|
||||||
|
'EVENT_CREATED': '📅 Nouvel événement créé',
|
||||||
|
'EVENT_MODIFIED': '📝 Événement modifié',
|
||||||
|
'EVENT_CANCELLED': '❌ Événement annulé',
|
||||||
|
'LOST': '🔴 Alerte critique : Équipement perdu',
|
||||||
|
'EQUIPMENT_MISSING': '⚠️ Équipement manquant',
|
||||||
|
'DAMAGED': '⚠️ Équipement endommagé',
|
||||||
|
'QUANTITY_MISMATCH': 'ℹ️ Quantité incorrecte',
|
||||||
|
'MAINTENANCE_REMINDER': '🔧 Rappel de maintenance',
|
||||||
|
'STOCK_LOW': '📦 Stock faible',
|
||||||
|
};
|
||||||
|
|
||||||
|
return subjects[alert.type] || '🔔 Nouvelle alerte - EM2 Events';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère le titre pour le corps de l'email
|
||||||
|
*/
|
||||||
|
function getAlertTitle(type) {
|
||||||
|
const titles = {
|
||||||
|
'EVENT_CREATED': 'Nouvel événement créé',
|
||||||
|
'EVENT_MODIFIED': 'Événement modifié',
|
||||||
|
'EVENT_CANCELLED': 'Événement annulé',
|
||||||
|
'LOST': 'Équipement perdu',
|
||||||
|
'EQUIPMENT_MISSING': 'Équipement manquant',
|
||||||
|
'DAMAGED': 'Équipement endommagé',
|
||||||
|
'QUANTITY_MISMATCH': 'Quantité incorrecte',
|
||||||
|
'MAINTENANCE_REMINDER': 'Maintenance requise',
|
||||||
|
'STOCK_LOW': 'Stock faible',
|
||||||
|
};
|
||||||
|
|
||||||
|
return titles[type] || 'Nouvelle alerte';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rend un template HTML avec Handlebars
|
||||||
|
*/
|
||||||
|
async function renderTemplate(templateName, data) {
|
||||||
|
try {
|
||||||
|
// Lire le template de base
|
||||||
|
const basePath = path.join(__dirname, '..', 'templates', 'base-template.html');
|
||||||
|
const baseTemplate = await fs.readFile(basePath, 'utf8');
|
||||||
|
|
||||||
|
// Lire le template de contenu
|
||||||
|
const contentPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
'..',
|
||||||
|
'templates',
|
||||||
|
`${templateName}.html`,
|
||||||
|
);
|
||||||
|
const contentTemplate = await fs.readFile(contentPath, 'utf8');
|
||||||
|
|
||||||
|
// Compiler les templates
|
||||||
|
const compileContent = handlebars.compile(contentTemplate);
|
||||||
|
const compileBase = handlebars.compile(baseTemplate);
|
||||||
|
|
||||||
|
// Rendre le contenu
|
||||||
|
const renderedContent = compileContent(data);
|
||||||
|
|
||||||
|
// Rendre le template de base avec le contenu
|
||||||
|
return compileBase({
|
||||||
|
...data,
|
||||||
|
content: renderedContent,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback vers un template simple
|
||||||
|
return `
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h2>${data.alertTitle}</h2>
|
||||||
|
<p>${data.alertMessage}</p>
|
||||||
|
<a href="${data.actionUrl}">Voir l'alerte</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
checkAlertPreference,
|
||||||
|
prepareTemplateData,
|
||||||
|
getEmailSubject,
|
||||||
|
getAlertTitle,
|
||||||
|
renderTemplate,
|
||||||
|
};
|
||||||
|
|
||||||
@@ -5,6 +5,7 @@ import 'package:em2rp/providers/container_provider.dart';
|
|||||||
import 'package:em2rp/providers/maintenance_provider.dart';
|
import 'package:em2rp/providers/maintenance_provider.dart';
|
||||||
import 'package:em2rp/providers/alert_provider.dart';
|
import 'package:em2rp/providers/alert_provider.dart';
|
||||||
import 'package:em2rp/utils/auth_guard_widget.dart';
|
import 'package:em2rp/utils/auth_guard_widget.dart';
|
||||||
|
import 'package:em2rp/views/alerts_page.dart';
|
||||||
import 'package:em2rp/views/calendar_page.dart';
|
import 'package:em2rp/views/calendar_page.dart';
|
||||||
import 'package:em2rp/views/login_page.dart';
|
import 'package:em2rp/views/login_page.dart';
|
||||||
import 'package:em2rp/views/equipment_management_page.dart';
|
import 'package:em2rp/views/equipment_management_page.dart';
|
||||||
@@ -131,9 +132,11 @@ class MyApp extends StatelessWidget {
|
|||||||
GlobalWidgetsLocalizations.delegate,
|
GlobalWidgetsLocalizations.delegate,
|
||||||
GlobalCupertinoLocalizations.delegate,
|
GlobalCupertinoLocalizations.delegate,
|
||||||
],
|
],
|
||||||
home: const AutoLoginWrapper(),
|
initialRoute: '/',
|
||||||
routes: {
|
routes: {
|
||||||
|
'/': (context) => const AutoLoginWrapper(),
|
||||||
'/login': (context) => const LoginPage(),
|
'/login': (context) => const LoginPage(),
|
||||||
|
'/alerts': (context) => const AuthGuard(child: AlertsPage()),
|
||||||
'/calendar': (context) => const AuthGuard(child: CalendarPage()),
|
'/calendar': (context) => const AuthGuard(child: CalendarPage()),
|
||||||
'/my_account': (context) => const AuthGuard(child: MyAccountPage()),
|
'/my_account': (context) => const AuthGuard(child: MyAccountPage()),
|
||||||
'/user_management': (context) => const AuthGuard(
|
'/user_management': (context) => const AuthGuard(
|
||||||
@@ -214,8 +217,23 @@ class _AutoLoginWrapperState extends State<AutoLoginWrapper> {
|
|||||||
await localAuthProvider.loadUserData();
|
await localAuthProvider.loadUserData();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
// MODIFIÉ : Vérifier si une route spécifique est demandée dans l'URL
|
||||||
|
// En Flutter Web, on peut vérifier window.location.hash
|
||||||
|
final currentUri = Uri.base;
|
||||||
|
final fragment = currentUri.fragment; // Ex: "/alerts" si URL est /#/alerts
|
||||||
|
|
||||||
|
print('[AutoLoginWrapper] Fragment URL: $fragment');
|
||||||
|
|
||||||
|
// Si une route spécifique est demandée (autre que / ou vide)
|
||||||
|
if (fragment.isNotEmpty && fragment != '/' && fragment != '/calendar') {
|
||||||
|
print('[AutoLoginWrapper] Redirection vers: $fragment');
|
||||||
|
Navigator.of(context).pushReplacementNamed(fragment);
|
||||||
|
} else {
|
||||||
|
// Route par défaut : calendrier
|
||||||
|
print('[AutoLoginWrapper] Redirection vers: /calendar (défaut)');
|
||||||
Navigator.of(context).pushReplacementNamed('/calendar');
|
Navigator.of(context).pushReplacementNamed('/calendar');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Auto login failed: $e');
|
print('Auto login failed: $e');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
|
/// Type d'alerte
|
||||||
enum AlertType {
|
enum AlertType {
|
||||||
lowStock, // Stock faible
|
lowStock, // Stock faible
|
||||||
maintenanceDue, // Maintenance à venir
|
maintenanceDue, // Maintenance à venir
|
||||||
conflict, // Conflit disponibilité
|
conflict, // Conflit disponibilité
|
||||||
lost // Équipement perdu
|
lost, // Équipement perdu
|
||||||
|
eventCreated, // Événement créé
|
||||||
|
eventModified, // Événement modifié
|
||||||
|
eventCancelled, // Événement annulé
|
||||||
|
eventAssigned, // Assigné à un événement
|
||||||
|
maintenanceReminder, // Rappel maintenance périodique
|
||||||
|
equipmentMissing, // Équipement manquant à une étape
|
||||||
|
quantityMismatch, // Quantité incorrecte
|
||||||
|
damaged, // Équipement endommagé
|
||||||
|
workforceAdded, // Ajouté à la workforce d'un événement
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gravité de l'alerte
|
||||||
|
enum AlertSeverity {
|
||||||
|
info, // Information (bleu)
|
||||||
|
warning, // Avertissement (orange)
|
||||||
|
critical, // Critique (rouge)
|
||||||
}
|
}
|
||||||
|
|
||||||
String alertTypeToString(AlertType type) {
|
String alertTypeToString(AlertType type) {
|
||||||
@@ -17,6 +34,24 @@ String alertTypeToString(AlertType type) {
|
|||||||
return 'CONFLICT';
|
return 'CONFLICT';
|
||||||
case AlertType.lost:
|
case AlertType.lost:
|
||||||
return 'LOST';
|
return 'LOST';
|
||||||
|
case AlertType.eventCreated:
|
||||||
|
return 'EVENT_CREATED';
|
||||||
|
case AlertType.eventModified:
|
||||||
|
return 'EVENT_MODIFIED';
|
||||||
|
case AlertType.eventCancelled:
|
||||||
|
return 'EVENT_CANCELLED';
|
||||||
|
case AlertType.eventAssigned:
|
||||||
|
return 'EVENT_ASSIGNED';
|
||||||
|
case AlertType.maintenanceReminder:
|
||||||
|
return 'MAINTENANCE_REMINDER';
|
||||||
|
case AlertType.equipmentMissing:
|
||||||
|
return 'EQUIPMENT_MISSING';
|
||||||
|
case AlertType.quantityMismatch:
|
||||||
|
return 'QUANTITY_MISMATCH';
|
||||||
|
case AlertType.damaged:
|
||||||
|
return 'DAMAGED';
|
||||||
|
case AlertType.workforceAdded:
|
||||||
|
return 'WORKFORCE_ADDED';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,26 +65,88 @@ AlertType alertTypeFromString(String? type) {
|
|||||||
return AlertType.conflict;
|
return AlertType.conflict;
|
||||||
case 'LOST':
|
case 'LOST':
|
||||||
return AlertType.lost;
|
return AlertType.lost;
|
||||||
|
case 'EVENT_CREATED':
|
||||||
|
return AlertType.eventCreated;
|
||||||
|
case 'EVENT_MODIFIED':
|
||||||
|
return AlertType.eventModified;
|
||||||
|
case 'EVENT_CANCELLED':
|
||||||
|
return AlertType.eventCancelled;
|
||||||
|
case 'EVENT_ASSIGNED':
|
||||||
|
return AlertType.eventAssigned;
|
||||||
|
case 'MAINTENANCE_REMINDER':
|
||||||
|
return AlertType.maintenanceReminder;
|
||||||
|
case 'EQUIPMENT_MISSING':
|
||||||
|
return AlertType.equipmentMissing;
|
||||||
|
case 'QUANTITY_MISMATCH':
|
||||||
|
return AlertType.quantityMismatch;
|
||||||
|
case 'DAMAGED':
|
||||||
|
return AlertType.damaged;
|
||||||
|
case 'WORKFORCE_ADDED':
|
||||||
|
return AlertType.workforceAdded;
|
||||||
default:
|
default:
|
||||||
return AlertType.conflict;
|
return AlertType.conflict;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String alertSeverityToString(AlertSeverity severity) {
|
||||||
|
switch (severity) {
|
||||||
|
case AlertSeverity.info:
|
||||||
|
return 'INFO';
|
||||||
|
case AlertSeverity.warning:
|
||||||
|
return 'WARNING';
|
||||||
|
case AlertSeverity.critical:
|
||||||
|
return 'CRITICAL';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertSeverity alertSeverityFromString(String? severity) {
|
||||||
|
switch (severity) {
|
||||||
|
case 'INFO':
|
||||||
|
return AlertSeverity.info;
|
||||||
|
case 'WARNING':
|
||||||
|
return AlertSeverity.warning;
|
||||||
|
case 'CRITICAL':
|
||||||
|
return AlertSeverity.critical;
|
||||||
|
default:
|
||||||
|
return AlertSeverity.info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AlertModel {
|
class AlertModel {
|
||||||
final String id; // ID généré automatiquement
|
final String id; // ID généré automatiquement
|
||||||
final AlertType type; // Type d'alerte
|
final AlertType type; // Type d'alerte
|
||||||
|
final AlertSeverity severity; // Gravité de l'alerte
|
||||||
final String message; // Message de l'alerte
|
final String message; // Message de l'alerte
|
||||||
|
final List<String> assignedToUserIds; // Utilisateurs concernés
|
||||||
|
final String? eventId; // ID de l'événement concerné (optionnel)
|
||||||
final String? equipmentId; // ID de l'équipement concerné (optionnel)
|
final String? equipmentId; // ID de l'équipement concerné (optionnel)
|
||||||
|
final String? createdByUserId; // Qui a déclenché l'alerte
|
||||||
final DateTime createdAt; // Date de création
|
final DateTime createdAt; // Date de création
|
||||||
|
final DateTime? dueDate; // Date d'échéance (pour maintenance)
|
||||||
|
final String? actionUrl; // URL de redirection (deep link)
|
||||||
final bool isRead; // Statut lu/non lu
|
final bool isRead; // Statut lu/non lu
|
||||||
|
final bool isResolved; // Résolue ou non
|
||||||
|
final String? resolution; // Message de résolution
|
||||||
|
final DateTime? resolvedAt; // Date de résolution
|
||||||
|
final String? resolvedByUserId; // Qui a résolu
|
||||||
|
|
||||||
AlertModel({
|
AlertModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.type,
|
required this.type,
|
||||||
|
this.severity = AlertSeverity.info,
|
||||||
required this.message,
|
required this.message,
|
||||||
|
this.assignedToUserIds = const [],
|
||||||
|
this.eventId,
|
||||||
this.equipmentId,
|
this.equipmentId,
|
||||||
|
this.createdByUserId,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
|
this.dueDate,
|
||||||
|
this.actionUrl,
|
||||||
this.isRead = false,
|
this.isRead = false,
|
||||||
|
this.isResolved = false,
|
||||||
|
this.resolution,
|
||||||
|
this.resolvedAt,
|
||||||
|
this.resolvedByUserId,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory AlertModel.fromMap(Map<String, dynamic> map, String id) {
|
factory AlertModel.fromMap(Map<String, dynamic> map, String id) {
|
||||||
@@ -61,42 +158,116 @@ class AlertModel {
|
|||||||
return DateTime.now();
|
return DateTime.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parser les assignedToUserIds (peut être List ou null)
|
||||||
|
List<String> parseUserIds(dynamic value) {
|
||||||
|
if (value == null) return [];
|
||||||
|
if (value is List) return value.map((e) => e.toString()).toList();
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return AlertModel(
|
return AlertModel(
|
||||||
id: id,
|
id: id,
|
||||||
type: alertTypeFromString(map['type']),
|
type: alertTypeFromString(map['type']),
|
||||||
|
severity: alertSeverityFromString(map['severity']),
|
||||||
message: map['message'] ?? '',
|
message: map['message'] ?? '',
|
||||||
|
assignedToUserIds: parseUserIds(map['assignedToUserIds'] ?? map['assignedTo']),
|
||||||
|
eventId: map['eventId'],
|
||||||
equipmentId: map['equipmentId'],
|
equipmentId: map['equipmentId'],
|
||||||
|
createdByUserId: map['createdByUserId'] ?? map['createdBy'],
|
||||||
createdAt: _parseDate(map['createdAt']),
|
createdAt: _parseDate(map['createdAt']),
|
||||||
|
dueDate: map['dueDate'] != null ? _parseDate(map['dueDate']) : null,
|
||||||
|
actionUrl: map['actionUrl'],
|
||||||
isRead: map['isRead'] ?? false,
|
isRead: map['isRead'] ?? false,
|
||||||
|
isResolved: map['isResolved'] ?? false,
|
||||||
|
resolution: map['resolution'],
|
||||||
|
resolvedAt: map['resolvedAt'] != null ? _parseDate(map['resolvedAt']) : null,
|
||||||
|
resolvedByUserId: map['resolvedByUserId'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Factory depuis un document Firestore
|
||||||
|
factory AlertModel.fromFirestore(DocumentSnapshot doc) {
|
||||||
|
final data = doc.data() as Map<String, dynamic>?;
|
||||||
|
if (data == null) {
|
||||||
|
throw Exception('Document vide: ${doc.id}');
|
||||||
|
}
|
||||||
|
return AlertModel.fromMap(data, doc.id);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
return {
|
||||||
'type': alertTypeToString(type),
|
'type': alertTypeToString(type),
|
||||||
|
'severity': alertSeverityToString(severity),
|
||||||
'message': message,
|
'message': message,
|
||||||
'equipmentId': equipmentId,
|
'assignedToUserIds': assignedToUserIds,
|
||||||
|
if (eventId != null) 'eventId': eventId,
|
||||||
|
if (equipmentId != null) 'equipmentId': equipmentId,
|
||||||
|
if (createdByUserId != null) 'createdByUserId': createdByUserId,
|
||||||
'createdAt': Timestamp.fromDate(createdAt),
|
'createdAt': Timestamp.fromDate(createdAt),
|
||||||
|
if (dueDate != null) 'dueDate': Timestamp.fromDate(dueDate!),
|
||||||
|
if (actionUrl != null) 'actionUrl': actionUrl,
|
||||||
'isRead': isRead,
|
'isRead': isRead,
|
||||||
|
'isResolved': isResolved,
|
||||||
|
if (resolution != null) 'resolution': resolution,
|
||||||
|
if (resolvedAt != null) 'resolvedAt': Timestamp.fromDate(resolvedAt!),
|
||||||
|
if (resolvedByUserId != null) 'resolvedByUserId': resolvedByUserId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertModel copyWith({
|
AlertModel copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
AlertType? type,
|
AlertType? type,
|
||||||
|
AlertSeverity? severity,
|
||||||
String? message,
|
String? message,
|
||||||
|
List<String>? assignedToUserIds,
|
||||||
|
String? eventId,
|
||||||
String? equipmentId,
|
String? equipmentId,
|
||||||
|
String? createdByUserId,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
|
DateTime? dueDate,
|
||||||
|
String? actionUrl,
|
||||||
bool? isRead,
|
bool? isRead,
|
||||||
|
bool? isResolved,
|
||||||
|
String? resolution,
|
||||||
|
DateTime? resolvedAt,
|
||||||
|
String? resolvedByUserId,
|
||||||
}) {
|
}) {
|
||||||
return AlertModel(
|
return AlertModel(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
|
severity: severity ?? this.severity,
|
||||||
message: message ?? this.message,
|
message: message ?? this.message,
|
||||||
|
assignedToUserIds: assignedToUserIds ?? this.assignedToUserIds,
|
||||||
|
eventId: eventId ?? this.eventId,
|
||||||
equipmentId: equipmentId ?? this.equipmentId,
|
equipmentId: equipmentId ?? this.equipmentId,
|
||||||
|
createdByUserId: createdByUserId ?? this.createdByUserId,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
dueDate: dueDate ?? this.dueDate,
|
||||||
|
actionUrl: actionUrl ?? this.actionUrl,
|
||||||
isRead: isRead ?? this.isRead,
|
isRead: isRead ?? this.isRead,
|
||||||
|
isResolved: isResolved ?? this.isResolved,
|
||||||
|
resolution: resolution ?? this.resolution,
|
||||||
|
resolvedAt: resolvedAt ?? this.resolvedAt,
|
||||||
|
resolvedByUserId: resolvedByUserId ?? this.resolvedByUserId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Helper : Retourne true si l'alerte est pour un événement
|
||||||
|
bool get isEventAlert =>
|
||||||
|
type == AlertType.eventCreated ||
|
||||||
|
type == AlertType.eventModified ||
|
||||||
|
type == AlertType.eventCancelled ||
|
||||||
|
type == AlertType.eventAssigned;
|
||||||
|
|
||||||
|
/// Helper : Retourne true si l'alerte est pour la maintenance
|
||||||
|
bool get isMaintenanceAlert =>
|
||||||
|
type == AlertType.maintenanceDue ||
|
||||||
|
type == AlertType.maintenanceReminder;
|
||||||
|
|
||||||
|
/// Helper : Retourne true si l'alerte est pour un équipement
|
||||||
|
bool get isEquipmentAlert =>
|
||||||
|
type == AlertType.lost ||
|
||||||
|
type == AlertType.equipmentMissing ||
|
||||||
|
type == AlertType.lowStock;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
88
em2rp/lib/models/notification_preferences_model.dart
Normal file
88
em2rp/lib/models/notification_preferences_model.dart
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/// Préférences de notifications pour un utilisateur
|
||||||
|
class NotificationPreferences {
|
||||||
|
final bool emailEnabled; // Recevoir emails
|
||||||
|
final bool pushEnabled; // Recevoir notifications push
|
||||||
|
final bool inAppEnabled; // Recevoir alertes in-app
|
||||||
|
|
||||||
|
// Préférences par type d'alerte
|
||||||
|
final bool eventsNotifications; // Alertes événements
|
||||||
|
final bool maintenanceNotifications; // Alertes maintenance
|
||||||
|
final bool stockNotifications; // Alertes stock
|
||||||
|
final bool equipmentNotifications; // Alertes équipement
|
||||||
|
|
||||||
|
// Token FCM (pour push)
|
||||||
|
final String? fcmToken;
|
||||||
|
|
||||||
|
const NotificationPreferences({
|
||||||
|
this.emailEnabled = true, // ✓ Activé par défaut
|
||||||
|
this.pushEnabled = false,
|
||||||
|
this.inAppEnabled = true,
|
||||||
|
this.eventsNotifications = true,
|
||||||
|
this.maintenanceNotifications = true,
|
||||||
|
this.stockNotifications = true,
|
||||||
|
this.equipmentNotifications = true,
|
||||||
|
this.fcmToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Valeurs par défaut pour un nouvel utilisateur
|
||||||
|
factory NotificationPreferences.defaults() {
|
||||||
|
return const NotificationPreferences(
|
||||||
|
emailEnabled: true, // ✓ Activé par défaut
|
||||||
|
pushEnabled: false,
|
||||||
|
inAppEnabled: true,
|
||||||
|
eventsNotifications: true,
|
||||||
|
maintenanceNotifications: true,
|
||||||
|
stockNotifications: true,
|
||||||
|
equipmentNotifications: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory NotificationPreferences.fromMap(Map<String, dynamic> map) {
|
||||||
|
return NotificationPreferences(
|
||||||
|
emailEnabled: map['emailEnabled'] ?? true, // ✓ true par défaut
|
||||||
|
pushEnabled: map['pushEnabled'] ?? false,
|
||||||
|
inAppEnabled: map['inAppEnabled'] ?? true,
|
||||||
|
eventsNotifications: map['eventsNotifications'] ?? true,
|
||||||
|
maintenanceNotifications: map['maintenanceNotifications'] ?? true,
|
||||||
|
stockNotifications: map['stockNotifications'] ?? true,
|
||||||
|
equipmentNotifications: map['equipmentNotifications'] ?? true,
|
||||||
|
fcmToken: map['fcmToken'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'emailEnabled': emailEnabled,
|
||||||
|
'pushEnabled': pushEnabled,
|
||||||
|
'inAppEnabled': inAppEnabled,
|
||||||
|
'eventsNotifications': eventsNotifications,
|
||||||
|
'maintenanceNotifications': maintenanceNotifications,
|
||||||
|
'stockNotifications': stockNotifications,
|
||||||
|
'equipmentNotifications': equipmentNotifications,
|
||||||
|
if (fcmToken != null) 'fcmToken': fcmToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationPreferences copyWith({
|
||||||
|
bool? emailEnabled,
|
||||||
|
bool? pushEnabled,
|
||||||
|
bool? inAppEnabled,
|
||||||
|
bool? eventsNotifications,
|
||||||
|
bool? maintenanceNotifications,
|
||||||
|
bool? stockNotifications,
|
||||||
|
bool? equipmentNotifications,
|
||||||
|
String? fcmToken,
|
||||||
|
}) {
|
||||||
|
return NotificationPreferences(
|
||||||
|
emailEnabled: emailEnabled ?? this.emailEnabled,
|
||||||
|
pushEnabled: pushEnabled ?? this.pushEnabled,
|
||||||
|
inAppEnabled: inAppEnabled ?? this.inAppEnabled,
|
||||||
|
eventsNotifications: eventsNotifications ?? this.eventsNotifications,
|
||||||
|
maintenanceNotifications: maintenanceNotifications ?? this.maintenanceNotifications,
|
||||||
|
stockNotifications: stockNotifications ?? this.stockNotifications,
|
||||||
|
equipmentNotifications: equipmentNotifications ?? this.equipmentNotifications,
|
||||||
|
fcmToken: fcmToken ?? this.fcmToken,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:em2rp/models/notification_preferences_model.dart';
|
||||||
|
|
||||||
class UserModel {
|
class UserModel {
|
||||||
final String uid;
|
final String uid;
|
||||||
@@ -8,6 +9,7 @@ class UserModel {
|
|||||||
final String profilePhotoUrl;
|
final String profilePhotoUrl;
|
||||||
final String email;
|
final String email;
|
||||||
final String phoneNumber;
|
final String phoneNumber;
|
||||||
|
final NotificationPreferences? notificationPreferences;
|
||||||
|
|
||||||
UserModel({
|
UserModel({
|
||||||
required this.uid,
|
required this.uid,
|
||||||
@@ -17,6 +19,7 @@ class UserModel {
|
|||||||
required this.profilePhotoUrl,
|
required this.profilePhotoUrl,
|
||||||
required this.email,
|
required this.email,
|
||||||
required this.phoneNumber,
|
required this.phoneNumber,
|
||||||
|
this.notificationPreferences,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convertit une Map (Firestore) en UserModel
|
// Convertit une Map (Firestore) en UserModel
|
||||||
@@ -57,6 +60,9 @@ class UserModel {
|
|||||||
profilePhotoUrl: data['profilePhotoUrl'] ?? '',
|
profilePhotoUrl: data['profilePhotoUrl'] ?? '',
|
||||||
email: data['email'] ?? '',
|
email: data['email'] ?? '',
|
||||||
phoneNumber: data['phoneNumber'] ?? '',
|
phoneNumber: data['phoneNumber'] ?? '',
|
||||||
|
notificationPreferences: data['notificationPreferences'] != null
|
||||||
|
? NotificationPreferences.fromMap(data['notificationPreferences'] as Map<String, dynamic>)
|
||||||
|
: NotificationPreferences.defaults(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +75,8 @@ class UserModel {
|
|||||||
'profilePhotoUrl': profilePhotoUrl,
|
'profilePhotoUrl': profilePhotoUrl,
|
||||||
'email': email,
|
'email': email,
|
||||||
'phoneNumber': phoneNumber,
|
'phoneNumber': phoneNumber,
|
||||||
|
if (notificationPreferences != null)
|
||||||
|
'notificationPreferences': notificationPreferences!.toMap(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +87,7 @@ class UserModel {
|
|||||||
String? profilePhotoUrl,
|
String? profilePhotoUrl,
|
||||||
String? email,
|
String? email,
|
||||||
String? phoneNumber,
|
String? phoneNumber,
|
||||||
|
NotificationPreferences? notificationPreferences,
|
||||||
}) {
|
}) {
|
||||||
return UserModel(
|
return UserModel(
|
||||||
uid: uid, // L'UID ne change pas
|
uid: uid, // L'UID ne change pas
|
||||||
@@ -88,6 +97,7 @@ class UserModel {
|
|||||||
profilePhotoUrl: profilePhotoUrl ?? this.profilePhotoUrl,
|
profilePhotoUrl: profilePhotoUrl ?? this.profilePhotoUrl,
|
||||||
email: email ?? this.email,
|
email: email ?? this.email,
|
||||||
phoneNumber: phoneNumber ?? this.phoneNumber,
|
phoneNumber: phoneNumber ?? this.phoneNumber,
|
||||||
|
notificationPreferences: notificationPreferences ?? this.notificationPreferences,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import '../models/user_model.dart';
|
import '../models/user_model.dart';
|
||||||
import '../models/role_model.dart';
|
import '../models/role_model.dart';
|
||||||
|
import '../models/notification_preferences_model.dart';
|
||||||
import '../utils/firebase_storage_manager.dart';
|
import '../utils/firebase_storage_manager.dart';
|
||||||
import '../services/api_service.dart';
|
import '../services/api_service.dart';
|
||||||
import '../services/data_service.dart';
|
import '../services/data_service.dart';
|
||||||
@@ -107,6 +108,25 @@ class LocalUserProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mise à jour des préférences de notifications
|
||||||
|
Future<void> updateNotificationPreferences(NotificationPreferences preferences) async {
|
||||||
|
if (_currentUser == null) return;
|
||||||
|
try {
|
||||||
|
await _dataService.updateUser(
|
||||||
|
_currentUser!.uid,
|
||||||
|
{
|
||||||
|
'notificationPreferences': preferences.toMap(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
_currentUser = _currentUser!.copyWith(notificationPreferences: preferences);
|
||||||
|
notifyListeners();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Erreur mise à jour préférences notifications : $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Changement de photo de profil
|
/// Changement de photo de profil
|
||||||
Future<void> changeProfilePicture(XFile image) async {
|
Future<void> changeProfilePicture(XFile image) async {
|
||||||
if (_currentUser == null) return;
|
if (_currentUser == null) return;
|
||||||
|
|||||||
255
em2rp/lib/services/alert_service.dart
Normal file
255
em2rp/lib/services/alert_service.dart
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import '../models/alert_model.dart';
|
||||||
|
import '../utils/debug_log.dart';
|
||||||
|
import 'api_service.dart' show FirebaseFunctionsApiService;
|
||||||
|
/// Service de gestion des alertes
|
||||||
|
/// Architecture simplifiée : le client appelle uniquement les Cloud Functions
|
||||||
|
/// Toute la logique métier est gérée côté backend
|
||||||
|
class AlertService {
|
||||||
|
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||||
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||||
|
|
||||||
|
/// Stream des alertes pour l'utilisateur connecté
|
||||||
|
Stream<List<AlertModel>> getAlertsStream() {
|
||||||
|
final user = _auth.currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
DebugLog.info('[AlertService] Pas d\'utilisateur connecté');
|
||||||
|
return Stream.value([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugLog.info('[AlertService] Stream alertes pour utilisateur: ${user.uid}');
|
||||||
|
|
||||||
|
return _firestore
|
||||||
|
.collection('alerts')
|
||||||
|
.where('assignedTo', arrayContains: user.uid)
|
||||||
|
.where('status', isEqualTo: 'ACTIVE')
|
||||||
|
.orderBy('createdAt', descending: true)
|
||||||
|
.snapshots()
|
||||||
|
.map((snapshot) {
|
||||||
|
final alerts = snapshot.docs
|
||||||
|
.map((doc) => AlertModel.fromFirestore(doc))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
DebugLog.info('[AlertService] ${alerts.length} alertes actives');
|
||||||
|
return alerts;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère les alertes non lues
|
||||||
|
Future<List<AlertModel>> getUnreadAlerts() async {
|
||||||
|
final user = _auth.currentUser;
|
||||||
|
if (user == null) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('alerts')
|
||||||
|
.where('assignedTo', arrayContains: user.uid)
|
||||||
|
.where('isRead', isEqualTo: false)
|
||||||
|
.where('status', isEqualTo: 'ACTIVE')
|
||||||
|
.orderBy('createdAt', descending: true)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return snapshot.docs
|
||||||
|
.map((doc) => AlertModel.fromFirestore(doc))
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[AlertService] Erreur récupération alertes', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marque une alerte comme lue
|
||||||
|
Future<void> markAsRead(String alertId) async {
|
||||||
|
try {
|
||||||
|
await _firestore.collection('alerts').doc(alertId).update({
|
||||||
|
'isRead': true,
|
||||||
|
'readAt': FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
DebugLog.info('[AlertService] Alerte $alertId marquée comme lue');
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[AlertService] Erreur marquage alerte', e);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marque toutes les alertes comme lues
|
||||||
|
Future<void> markAllAsRead() async {
|
||||||
|
final user = _auth.currentUser;
|
||||||
|
if (user == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('alerts')
|
||||||
|
.where('assignedTo', arrayContains: user.uid)
|
||||||
|
.where('isRead', isEqualTo: false)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
final batch = _firestore.batch();
|
||||||
|
for (var doc in snapshot.docs) {
|
||||||
|
batch.update(doc.reference, {
|
||||||
|
'isRead': true,
|
||||||
|
'readAt': FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await batch.commit();
|
||||||
|
DebugLog.info('[AlertService] ${snapshot.docs.length} alertes marquées comme lues');
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[AlertService] Erreur marquage alertes', e);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Archive une alerte
|
||||||
|
Future<void> archiveAlert(String alertId) async {
|
||||||
|
try {
|
||||||
|
await _firestore.collection('alerts').doc(alertId).update({
|
||||||
|
'status': 'ARCHIVED',
|
||||||
|
'archivedAt': FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
DebugLog.info('[AlertService] Alerte $alertId archivée');
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[AlertService] Erreur archivage alerte', e);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crée une alerte manuelle (appelée par l'utilisateur)
|
||||||
|
/// Cette méthode appelle la Cloud Function createAlert
|
||||||
|
Future<String> createManualAlert({
|
||||||
|
required AlertType type,
|
||||||
|
required AlertSeverity severity,
|
||||||
|
required String message,
|
||||||
|
String? title,
|
||||||
|
String? equipmentId,
|
||||||
|
String? eventId,
|
||||||
|
String? actionUrl,
|
||||||
|
Map<String, dynamic>? metadata,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
DebugLog.info('[AlertService] === CRÉATION ALERTE MANUELLE ===');
|
||||||
|
DebugLog.info('[AlertService] Type: $type');
|
||||||
|
DebugLog.info('[AlertService] Severity: $severity');
|
||||||
|
|
||||||
|
final apiService = FirebaseFunctionsApiService();
|
||||||
|
final result = await apiService.call(
|
||||||
|
'createAlert',
|
||||||
|
{
|
||||||
|
'type': alertTypeToString(type),
|
||||||
|
'severity': severity.name.toUpperCase(),
|
||||||
|
'title': title,
|
||||||
|
'message': message,
|
||||||
|
'equipmentId': equipmentId,
|
||||||
|
'eventId': eventId,
|
||||||
|
'actionUrl': actionUrl,
|
||||||
|
'metadata': metadata ?? {},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final alertId = result['alertId'] as String;
|
||||||
|
DebugLog.info('[AlertService] ✓ Alerte créée: $alertId');
|
||||||
|
|
||||||
|
return alertId;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
DebugLog.error('[AlertService] ❌ Erreur création alerte', e);
|
||||||
|
DebugLog.error('[AlertService] Stack', stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stream des alertes pour un utilisateur spécifique
|
||||||
|
Stream<List<AlertModel>> alertsStreamForUser(String userId) {
|
||||||
|
return _firestore
|
||||||
|
.collection('alerts')
|
||||||
|
.where('assignedTo', arrayContains: userId)
|
||||||
|
.where('status', isEqualTo: 'ACTIVE')
|
||||||
|
.orderBy('createdAt', descending: true)
|
||||||
|
.snapshots()
|
||||||
|
.map((snapshot) => snapshot.docs
|
||||||
|
.map((doc) => AlertModel.fromFirestore(doc))
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Récupère les alertes pour un utilisateur
|
||||||
|
Future<List<AlertModel>> getAlertsForUser(String userId) async {
|
||||||
|
try {
|
||||||
|
final snapshot = await _firestore
|
||||||
|
.collection('alerts')
|
||||||
|
.where('assignedTo', arrayContains: userId)
|
||||||
|
.where('status', isEqualTo: 'ACTIVE')
|
||||||
|
.orderBy('createdAt', descending: true)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return snapshot.docs
|
||||||
|
.map((doc) => AlertModel.fromFirestore(doc))
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[AlertService] Erreur récupération alertes', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stream du nombre d'alertes non lues pour un utilisateur
|
||||||
|
Stream<int> unreadCountStreamForUser(String userId) {
|
||||||
|
return _firestore
|
||||||
|
.collection('alerts')
|
||||||
|
.where('assignedTo', arrayContains: userId)
|
||||||
|
.where('isRead', isEqualTo: false)
|
||||||
|
.where('status', isEqualTo: 'ACTIVE')
|
||||||
|
.snapshots()
|
||||||
|
.map((snapshot) => snapshot.docs.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Supprime une alerte
|
||||||
|
Future<void> deleteAlert(String alertId) async {
|
||||||
|
try {
|
||||||
|
await _firestore.collection('alerts').doc(alertId).delete();
|
||||||
|
DebugLog.info('[AlertService] Alerte $alertId supprimée');
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[AlertService] Erreur suppression alerte', e);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crée une alerte de création d'événement
|
||||||
|
Future<void> createEventCreatedAlert({
|
||||||
|
required String eventId,
|
||||||
|
required String eventName,
|
||||||
|
required DateTime eventDate,
|
||||||
|
}) async {
|
||||||
|
await createManualAlert(
|
||||||
|
type: AlertType.eventCreated,
|
||||||
|
severity: AlertSeverity.info,
|
||||||
|
message: 'Nouvel événement créé: "$eventName" le ${_formatDate(eventDate)}',
|
||||||
|
eventId: eventId,
|
||||||
|
metadata: {
|
||||||
|
'eventName': eventName,
|
||||||
|
'eventDate': eventDate.toIso8601String(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crée une alerte de modification d'événement
|
||||||
|
Future<void> createEventModifiedAlert({
|
||||||
|
required String eventId,
|
||||||
|
required String eventName,
|
||||||
|
required String modification,
|
||||||
|
}) async {
|
||||||
|
await createManualAlert(
|
||||||
|
type: AlertType.eventModified,
|
||||||
|
severity: AlertSeverity.info,
|
||||||
|
message: 'Événement "$eventName" modifié: $modification',
|
||||||
|
eventId: eventId,
|
||||||
|
metadata: {
|
||||||
|
'eventName': eventName,
|
||||||
|
'modification': modification,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDate(DateTime date) {
|
||||||
|
return '${date.day}/${date.month}/${date.year}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
150
em2rp/lib/services/email_service.dart
Normal file
150
em2rp/lib/services/email_service.dart
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import 'package:cloud_functions/cloud_functions.dart';
|
||||||
|
import 'package:em2rp/models/alert_model.dart';
|
||||||
|
import 'package:em2rp/models/user_model.dart';
|
||||||
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
|
||||||
|
/// Service d'envoi d'emails via Cloud Functions
|
||||||
|
class EmailService {
|
||||||
|
final FirebaseFunctions _functions = FirebaseFunctions.instanceFor(region: 'us-central1');
|
||||||
|
|
||||||
|
/// Envoie un email d'alerte à un utilisateur
|
||||||
|
///
|
||||||
|
/// [alert] : L'alerte à envoyer
|
||||||
|
/// [userId] : ID de l'utilisateur destinataire
|
||||||
|
/// [templateType] : Type de template à utiliser (par défaut: 'alert-individual')
|
||||||
|
Future<bool> sendAlertEmail({
|
||||||
|
required AlertModel alert,
|
||||||
|
required String userId,
|
||||||
|
String templateType = 'alert-individual',
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Vérifier que l'utilisateur est authentifié
|
||||||
|
final currentUser = FirebaseAuth.instance.currentUser;
|
||||||
|
if (currentUser == null) {
|
||||||
|
DebugLog.error('[EmailService] Utilisateur non authentifié');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugLog.info('[EmailService] Envoi email alerte ${alert.id} à $userId');
|
||||||
|
|
||||||
|
final result = await _functions.httpsCallable('sendAlertEmail').call({
|
||||||
|
'alertId': alert.id,
|
||||||
|
'userId': userId,
|
||||||
|
'templateType': templateType,
|
||||||
|
});
|
||||||
|
|
||||||
|
final data = result.data as Map<String, dynamic>;
|
||||||
|
final success = data['success'] as bool? ?? false;
|
||||||
|
final skipped = data['skipped'] as bool? ?? false;
|
||||||
|
|
||||||
|
if (skipped) {
|
||||||
|
final reason = data['reason'] as String? ?? 'unknown';
|
||||||
|
DebugLog.info('[EmailService] Email non envoyé: $reason');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
DebugLog.info('[EmailService] Email envoyé avec succès');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[EmailService] Erreur envoi email', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Envoie un email d'alerte à plusieurs utilisateurs
|
||||||
|
///
|
||||||
|
/// [alert] : L'alerte à envoyer
|
||||||
|
/// [userIds] : Liste des IDs des utilisateurs destinataires
|
||||||
|
Future<Map<String, bool>> sendAlertEmailToMultipleUsers({
|
||||||
|
required AlertModel alert,
|
||||||
|
required List<String> userIds,
|
||||||
|
String templateType = 'alert-individual',
|
||||||
|
}) async {
|
||||||
|
final results = <String, bool>{};
|
||||||
|
|
||||||
|
DebugLog.info('[EmailService] Envoi emails à ${userIds.length} utilisateurs');
|
||||||
|
|
||||||
|
// Envoyer en parallèle (max 5 à la fois pour éviter surcharge)
|
||||||
|
final batches = <List<String>>[];
|
||||||
|
for (var i = 0; i < userIds.length; i += 5) {
|
||||||
|
batches.add(userIds.sublist(
|
||||||
|
i,
|
||||||
|
i + 5 > userIds.length ? userIds.length : i + 5,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final batch in batches) {
|
||||||
|
final futures = batch.map((userId) => sendAlertEmail(
|
||||||
|
alert: alert,
|
||||||
|
userId: userId,
|
||||||
|
templateType: templateType,
|
||||||
|
));
|
||||||
|
|
||||||
|
final batchResults = await Future.wait(futures);
|
||||||
|
|
||||||
|
for (var i = 0; i < batch.length; i++) {
|
||||||
|
results[batch[i]] = batchResults[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final successCount = results.values.where((v) => v).length;
|
||||||
|
DebugLog.info('[EmailService] $successCount/${ userIds.length} emails envoyés');
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Détermine si une alerte doit être envoyée immédiatement ou en digest
|
||||||
|
///
|
||||||
|
/// [alert] : L'alerte à vérifier
|
||||||
|
/// Returns: true si immédiat, false si digest
|
||||||
|
bool shouldSendImmediate(AlertModel alert) {
|
||||||
|
// Les alertes critiques sont envoyées immédiatement
|
||||||
|
if (alert.severity == AlertSeverity.critical) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types d'alertes toujours immédiates
|
||||||
|
const immediateTypes = [
|
||||||
|
AlertType.lost, // Équipement perdu
|
||||||
|
AlertType.eventCancelled, // Événement annulé
|
||||||
|
];
|
||||||
|
|
||||||
|
return immediateTypes.contains(alert.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Envoie un email d'alerte en tenant compte des préférences
|
||||||
|
///
|
||||||
|
/// [alert] : L'alerte à envoyer
|
||||||
|
/// [userIds] : Liste des IDs des utilisateurs destinataires
|
||||||
|
Future<void> sendAlertWithPreferences({
|
||||||
|
required AlertModel alert,
|
||||||
|
required List<String> userIds,
|
||||||
|
}) async {
|
||||||
|
if (userIds.isEmpty) {
|
||||||
|
DebugLog.warning('[EmailService] Aucun utilisateur à notifier');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final immediate = shouldSendImmediate(alert);
|
||||||
|
|
||||||
|
if (immediate) {
|
||||||
|
DebugLog.info('[EmailService] Envoi immédiat (alerte critique)');
|
||||||
|
await sendAlertEmailToMultipleUsers(
|
||||||
|
alert: alert,
|
||||||
|
userIds: userIds,
|
||||||
|
templateType: 'alert-individual',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
DebugLog.info('[EmailService] Ajout au digest (alerte non critique)');
|
||||||
|
// Les alertes non critiques seront envoyées dans le digest quotidien
|
||||||
|
// La Cloud Function sendDailyDigest s'en occupera
|
||||||
|
// Rien à faire ici, les alertes sont déjà dans Firestore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ import 'package:em2rp/models/event_type_model.dart';
|
|||||||
import 'package:em2rp/models/user_model.dart';
|
import 'package:em2rp/models/user_model.dart';
|
||||||
import 'package:em2rp/services/api_service.dart';
|
import 'package:em2rp/services/api_service.dart';
|
||||||
import 'package:em2rp/services/data_service.dart';
|
import 'package:em2rp/services/data_service.dart';
|
||||||
|
import 'package:em2rp/services/alert_service.dart';
|
||||||
import 'dart:developer' as developer;
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
class EventFormService {
|
class EventFormService {
|
||||||
@@ -109,7 +110,24 @@ class EventFormService {
|
|||||||
static Future<String> createEvent(EventModel event) async {
|
static Future<String> createEvent(EventModel event) async {
|
||||||
try {
|
try {
|
||||||
final result = await _apiService.call('createEvent', event.toMap());
|
final result = await _apiService.call('createEvent', event.toMap());
|
||||||
return result['id'] as String;
|
final eventId = result['id'] as String;
|
||||||
|
|
||||||
|
// NOUVEAU : Créer alerte automatique pour les utilisateurs assignés
|
||||||
|
try {
|
||||||
|
await AlertService().createEventCreatedAlert(
|
||||||
|
eventId: eventId,
|
||||||
|
eventName: event.name,
|
||||||
|
eventDate: event.startDateTime,
|
||||||
|
);
|
||||||
|
developer.log('Alert created for new event: $eventId', name: 'EventFormService');
|
||||||
|
} catch (alertError) {
|
||||||
|
// Ne pas bloquer la création de l'événement si l'alerte échoue
|
||||||
|
developer.log('Warning: Could not create alert for event',
|
||||||
|
name: 'EventFormService',
|
||||||
|
error: alertError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventId;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
developer.log('Error creating event', name: 'EventFormService', error: e);
|
developer.log('Error creating event', name: 'EventFormService', error: e);
|
||||||
rethrow;
|
rethrow;
|
||||||
@@ -129,6 +147,24 @@ class EventFormService {
|
|||||||
await _apiService.call('updateEvent', eventData);
|
await _apiService.call('updateEvent', eventData);
|
||||||
|
|
||||||
developer.log('Event updated successfully', name: 'EventFormService');
|
developer.log('Event updated successfully', name: 'EventFormService');
|
||||||
|
|
||||||
|
// NOUVEAU : Créer alerte automatique pour les utilisateurs assignés
|
||||||
|
try {
|
||||||
|
final currentUserId = FirebaseAuth.instance.currentUser?.uid;
|
||||||
|
if (currentUserId != null) {
|
||||||
|
await AlertService().createEventModifiedAlert(
|
||||||
|
eventId: event.id,
|
||||||
|
eventName: event.name,
|
||||||
|
modification: 'Informations modifiées',
|
||||||
|
);
|
||||||
|
developer.log('Alert created for modified event: ${event.id}', name: 'EventFormService');
|
||||||
|
}
|
||||||
|
} catch (alertError) {
|
||||||
|
// Ne pas bloquer la modification de l'événement si l'alerte échoue
|
||||||
|
developer.log('Warning: Could not create alert for event modification',
|
||||||
|
name: 'EventFormService',
|
||||||
|
error: alertError);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
developer.log('Error updating event', name: 'EventFormService', error: e);
|
developer.log('Error updating event', name: 'EventFormService', error: e);
|
||||||
rethrow;
|
rethrow;
|
||||||
|
|||||||
@@ -17,14 +17,19 @@ class AuthGuard extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final localAuthProvider = Provider.of<LocalUserProvider>(context);
|
final localAuthProvider = Provider.of<LocalUserProvider>(context);
|
||||||
|
|
||||||
|
// Log pour débug
|
||||||
|
print('[AuthGuard] Vérification accès - User: ${localAuthProvider.currentUser?.uid}, Permission requise: $requiredPermission');
|
||||||
|
|
||||||
// Si l'utilisateur n'est pas connecté
|
// Si l'utilisateur n'est pas connecté
|
||||||
if (localAuthProvider.currentUser == null) {
|
if (localAuthProvider.currentUser == null) {
|
||||||
|
print('[AuthGuard] Utilisateur non connecté, redirection vers LoginPage');
|
||||||
return const LoginPage();
|
return const LoginPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si la page requiert une permission spécifique et que l'utilisateur ne la possède pas
|
// Si la page requiert une permission spécifique et que l'utilisateur ne la possède pas
|
||||||
if (requiredPermission != null &&
|
if (requiredPermission != null &&
|
||||||
!localAuthProvider.hasPermission(requiredPermission!)) {
|
!localAuthProvider.hasPermission(requiredPermission!)) {
|
||||||
|
print('[AuthGuard] Permission "$requiredPermission" refusée');
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("Accès refusé")),
|
appBar: AppBar(title: const Text("Accès refusé")),
|
||||||
body: const Center(
|
body: const Center(
|
||||||
@@ -34,6 +39,7 @@ class AuthGuard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sinon, afficher la page demandée
|
// Sinon, afficher la page demandée
|
||||||
|
print('[AuthGuard] Accès autorisé, affichage de la page');
|
||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
296
em2rp/lib/views/alerts_page.dart
Normal file
296
em2rp/lib/views/alerts_page.dart
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:em2rp/models/alert_model.dart';
|
||||||
|
import 'package:em2rp/services/alert_service.dart';
|
||||||
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
|
import 'package:em2rp/views/widgets/alert_item.dart';
|
||||||
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
/// Page listant toutes les alertes de l'utilisateur
|
||||||
|
class AlertsPage extends StatefulWidget {
|
||||||
|
const AlertsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AlertsPage> createState() => _AlertsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AlertsPageState extends State<AlertsPage> with SingleTickerProviderStateMixin {
|
||||||
|
late TabController _tabController;
|
||||||
|
final AlertService _alertService = AlertService();
|
||||||
|
AlertType? _filter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_tabController = TabController(length: 4, vsync: this);
|
||||||
|
_tabController.addListener(() {
|
||||||
|
setState(() {
|
||||||
|
_filter = _getFilterForTab(_tabController.index);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertType? _getFilterForTab(int index) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
return null; // Toutes
|
||||||
|
case 1:
|
||||||
|
return AlertType.eventCreated; // Événements (on filtrera manuellement)
|
||||||
|
case 2:
|
||||||
|
return AlertType.maintenanceDue; // Maintenance
|
||||||
|
case 3:
|
||||||
|
return AlertType.lost; // Équipement
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final localUserProvider = context.watch<LocalUserProvider>();
|
||||||
|
final userId = localUserProvider.currentUser?.uid;
|
||||||
|
|
||||||
|
if (userId == null) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Notifications'),
|
||||||
|
),
|
||||||
|
body: const Center(
|
||||||
|
child: Text('Veuillez vous connecter'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Notifications'),
|
||||||
|
backgroundColor: AppColors.rouge,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.done_all),
|
||||||
|
onPressed: () => _markAllAsRead(userId),
|
||||||
|
tooltip: 'Tout marquer comme lu',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
bottom: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
indicatorColor: Colors.white,
|
||||||
|
labelColor: Colors.white,
|
||||||
|
unselectedLabelColor: Colors.white70,
|
||||||
|
tabs: const [
|
||||||
|
Tab(text: 'Toutes'),
|
||||||
|
Tab(text: 'Événements'),
|
||||||
|
Tab(text: 'Maintenance'),
|
||||||
|
Tab(text: 'Équipement'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: _buildAlertsList(userId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAlertsList(String userId) {
|
||||||
|
return StreamBuilder<List<AlertModel>>(
|
||||||
|
stream: _alertService.alertsStreamForUser(userId),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
// Log détaillé de l'erreur
|
||||||
|
print('[AlertsPage] ERREUR Stream: ${snapshot.error}');
|
||||||
|
print('[AlertsPage] StackTrace: ${snapshot.stackTrace}');
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline, size: 64, color: Colors.red),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text('Erreur de chargement des alertes'),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
snapshot.error.toString(),
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => setState(() {}),
|
||||||
|
child: const Text('Réessayer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final allAlerts = snapshot.data ?? [];
|
||||||
|
|
||||||
|
// Filtrer selon l'onglet sélectionné
|
||||||
|
final filteredAlerts = _filterAlerts(allAlerts);
|
||||||
|
|
||||||
|
if (filteredAlerts.isEmpty) {
|
||||||
|
return _buildEmptyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
itemCount: filteredAlerts.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final alert = filteredAlerts[index];
|
||||||
|
return AlertItem(
|
||||||
|
alert: alert,
|
||||||
|
onTap: () => _handleAlertTap(alert),
|
||||||
|
onMarkAsRead: () => _markAsRead(alert.id),
|
||||||
|
onDelete: () => _deleteAlert(alert.id),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AlertModel> _filterAlerts(List<AlertModel> alerts) {
|
||||||
|
if (_filter == null) {
|
||||||
|
return alerts; // Toutes
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_tabController.index) {
|
||||||
|
case 1: // Événements
|
||||||
|
return alerts.where((a) => a.isEventAlert).toList();
|
||||||
|
case 2: // Maintenance
|
||||||
|
return alerts.where((a) => a.isMaintenanceAlert).toList();
|
||||||
|
case 3: // Équipement
|
||||||
|
return alerts.where((a) => a.isEquipmentAlert).toList();
|
||||||
|
default:
|
||||||
|
return alerts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyState() {
|
||||||
|
String message;
|
||||||
|
IconData icon;
|
||||||
|
|
||||||
|
switch (_tabController.index) {
|
||||||
|
case 1:
|
||||||
|
message = 'Aucune alerte d\'événement';
|
||||||
|
icon = Icons.event;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
message = 'Aucune alerte de maintenance';
|
||||||
|
icon = Icons.build;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
message = 'Aucune alerte d\'équipement';
|
||||||
|
icon = Icons.inventory_2;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
message = 'Aucune notification';
|
||||||
|
icon = Icons.notifications_none;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 64, color: Colors.grey.shade400),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleAlertTap(AlertModel alert) async {
|
||||||
|
// Marquer comme lu si pas déjà lu
|
||||||
|
if (!alert.isRead) {
|
||||||
|
await _markAsRead(alert.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirection selon actionUrl (pour l'instant, juste rester sur la page)
|
||||||
|
// TODO: Implémenter navigation vers événement/équipement si besoin
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _markAsRead(String alertId) async {
|
||||||
|
try {
|
||||||
|
await _alertService.markAsRead(alertId);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur : $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteAlert(String alertId) async {
|
||||||
|
try {
|
||||||
|
await _alertService.deleteAlert(alertId);
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Alerte supprimée'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur : $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _markAllAsRead(String userId) async {
|
||||||
|
try {
|
||||||
|
final alerts = await _alertService.getAlertsForUser(userId);
|
||||||
|
for (final alert in alerts.where((a) => !a.isRead)) {
|
||||||
|
await _alertService.markAsRead(alert.id);
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Toutes les alertes ont été marquées comme lues'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur : $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
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: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';
|
||||||
@@ -321,38 +322,40 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
}
|
}
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
// Si on est à la dernière étape (retour), vérifier les équipements LOST
|
|
||||||
if (_currentStep == PreparationStep.return_) {
|
|
||||||
await _checkAndMarkLostEquipment(updatedEquipment);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mettre à jour Firestore selon l'étape
|
// Mettre à jour Firestore selon l'étape
|
||||||
final updateData = <String, dynamic>{
|
final updateData = <String, dynamic>{
|
||||||
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ajouter les statuts selon l'étape et la checkbox
|
// Ajouter les statuts selon l'étape et la checkbox
|
||||||
|
String validationType = 'CHECK';
|
||||||
switch (_currentStep) {
|
switch (_currentStep) {
|
||||||
case PreparationStep.preparation:
|
case PreparationStep.preparation:
|
||||||
updateData['preparationStatus'] = preparationStatusToString(PreparationStatus.completed);
|
updateData['preparationStatus'] = preparationStatusToString(PreparationStatus.completed);
|
||||||
|
validationType = 'CHECK_OUT';
|
||||||
if (_loadSimultaneously) {
|
if (_loadSimultaneously) {
|
||||||
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.completed);
|
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.completed);
|
||||||
|
validationType = 'LOADING';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PreparationStep.loadingOutbound:
|
case PreparationStep.loadingOutbound:
|
||||||
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.completed);
|
updateData['loadingStatus'] = loadingStatusToString(LoadingStatus.completed);
|
||||||
|
validationType = 'LOADING';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PreparationStep.unloadingReturn:
|
case PreparationStep.unloadingReturn:
|
||||||
updateData['unloadingStatus'] = unloadingStatusToString(UnloadingStatus.completed);
|
updateData['unloadingStatus'] = unloadingStatusToString(UnloadingStatus.completed);
|
||||||
|
validationType = 'UNLOADING';
|
||||||
if (_loadSimultaneously) {
|
if (_loadSimultaneously) {
|
||||||
updateData['returnStatus'] = returnStatusToString(ReturnStatus.completed);
|
updateData['returnStatus'] = returnStatusToString(ReturnStatus.completed);
|
||||||
|
validationType = 'CHECK_IN';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PreparationStep.return_:
|
case PreparationStep.return_:
|
||||||
updateData['returnStatus'] = returnStatusToString(ReturnStatus.completed);
|
updateData['returnStatus'] = returnStatusToString(ReturnStatus.completed);
|
||||||
|
validationType = 'CHECK_IN';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +375,41 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
await _updateEquipmentStatuses(updatedEquipment);
|
await _updateEquipmentStatuses(updatedEquipment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOUVEAU: Appeler la Cloud Function pour traiter la validation
|
||||||
|
// et créer les alertes automatiquement
|
||||||
|
try {
|
||||||
|
DebugLog.info('[EventPreparationPage] Appel processEquipmentValidation');
|
||||||
|
|
||||||
|
final equipmentList = updatedEquipment.map((eq) {
|
||||||
|
final equipment = _equipmentCache[eq.equipmentId];
|
||||||
|
return {
|
||||||
|
'equipmentId': eq.equipmentId,
|
||||||
|
'name': equipment?.name ?? 'Équipement inconnu',
|
||||||
|
'status': _determineEquipmentStatus(eq),
|
||||||
|
'quantity': _getQuantityForStep(eq),
|
||||||
|
'expectedQuantity': eq.quantity,
|
||||||
|
'isMissingAtPreparation': eq.isMissingAtPreparation,
|
||||||
|
'isMissingAtReturn': eq.isMissingAtReturn,
|
||||||
|
};
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final result = await FirebaseFunctions.instanceFor(region: 'us-central1')
|
||||||
|
.httpsCallable('processEquipmentValidation')
|
||||||
|
.call({
|
||||||
|
'eventId': _currentEvent.id,
|
||||||
|
'equipmentList': equipmentList,
|
||||||
|
'validationType': validationType,
|
||||||
|
});
|
||||||
|
|
||||||
|
final alertsCreated = result.data['alertsCreated'] ?? 0;
|
||||||
|
if (alertsCreated > 0) {
|
||||||
|
DebugLog.info('[EventPreparationPage] $alertsCreated alertes créées automatiquement');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
DebugLog.error('[EventPreparationPage] Erreur appel processEquipmentValidation', e);
|
||||||
|
// Ne pas bloquer la validation si les alertes échouent
|
||||||
|
}
|
||||||
|
|
||||||
// Recharger l'événement depuis le provider
|
// Recharger l'événement depuis le provider
|
||||||
final eventProvider = context.read<EventProvider>();
|
final eventProvider = context.read<EventProvider>();
|
||||||
// Recharger la liste des événements pour rafraîchir les données
|
// Recharger la liste des événements pour rafraîchir les données
|
||||||
@@ -667,38 +705,68 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
|
|||||||
return result ?? false;
|
return result ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vérifier et marquer les équipements LOST (logique intelligente)
|
/// Détermine le statut d'un équipement selon l'étape actuelle
|
||||||
Future<void> _checkAndMarkLostEquipment(List<EventEquipment> updatedEquipment) async {
|
String _determineEquipmentStatus(EventEquipment eq) {
|
||||||
for (final eq in updatedEquipment) {
|
// Vérifier d'abord si l'équipement est perdu (LOST)
|
||||||
final isMissingNow = eq.isMissingAtReturn;
|
if (_shouldMarkAsLost(eq)) {
|
||||||
|
return 'LOST';
|
||||||
|
}
|
||||||
|
|
||||||
if (isMissingNow) {
|
// Vérifier si manquant à l'étape actuelle
|
||||||
// Vérifier si c'était manquant dès la préparation (étape 0)
|
if (_isMissingAtCurrentStep(eq)) {
|
||||||
final wasMissingAtPreparation = eq.isMissingAtPreparation;
|
return 'MISSING';
|
||||||
|
}
|
||||||
|
|
||||||
if (!wasMissingAtPreparation) {
|
// Vérifier les quantités
|
||||||
// Était présent au départ mais manquant maintenant = LOST
|
final currentQty = _getQuantityForStep(eq);
|
||||||
try {
|
if (currentQty != null && currentQty < eq.quantity) {
|
||||||
await _dataService.updateEquipmentStatusOnly(
|
return 'QUANTITY_MISMATCH';
|
||||||
equipmentId: eq.equipmentId,
|
}
|
||||||
status: EquipmentStatus.lost.toString(),
|
|
||||||
);
|
|
||||||
|
|
||||||
DebugLog.info('[EventPreparationPage] Équipement ${eq.equipmentId} marqué comme LOST');
|
return 'AVAILABLE';
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Créer une alerte "Équipement perdu"
|
/// Vérifie si un équipement doit être marqué comme LOST
|
||||||
// await _createLostEquipmentAlert(eq.equipmentId);
|
bool _shouldMarkAsLost(EventEquipment eq) {
|
||||||
} catch (e) {
|
// Seulement aux étapes de retour
|
||||||
DebugLog.error('[EventPreparationPage] Erreur marquage LOST ${eq.equipmentId}', e);
|
if (_currentStep != PreparationStep.return_ &&
|
||||||
|
!(_currentStep == PreparationStep.unloadingReturn && _loadSimultaneously)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Manquant dès le début = PAS lost, juste manquant
|
// Si manquant maintenant mais PAS manquant à la préparation = LOST
|
||||||
DebugLog.info('[EventPreparationPage] Équipement ${eq.equipmentId} manquant depuis le début (pas LOST)');
|
return eq.isMissingAtReturn && !eq.isMissingAtPreparation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Vérifie si un équipement est manquant à l'étape actuelle
|
||||||
|
bool _isMissingAtCurrentStep(EventEquipment eq) {
|
||||||
|
switch (_currentStep) {
|
||||||
|
case PreparationStep.preparation:
|
||||||
|
return eq.isMissingAtPreparation;
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
return eq.isMissingAtLoading;
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
return eq.isMissingAtUnloading;
|
||||||
|
case PreparationStep.return_:
|
||||||
|
return eq.isMissingAtReturn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Récupère la quantité pour l'étape actuelle
|
||||||
|
int? _getQuantityForStep(EventEquipment eq) {
|
||||||
|
switch (_currentStep) {
|
||||||
|
case PreparationStep.preparation:
|
||||||
|
return eq.quantityAtPreparation;
|
||||||
|
case PreparationStep.loadingOutbound:
|
||||||
|
return eq.quantityAtLoading;
|
||||||
|
case PreparationStep.unloadingReturn:
|
||||||
|
return eq.quantityAtUnloading;
|
||||||
|
case PreparationStep.return_:
|
||||||
|
return eq.quantityAtReturn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final allValidated = _isStepCompleted();
|
final allValidated = _isStepCompleted();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:em2rp/views/widgets/inputs/styled_text_field.dart';
|
import 'package:em2rp/views/widgets/inputs/styled_text_field.dart';
|
||||||
import 'package:em2rp/views/widgets/image/profile_picture_selector.dart';
|
import 'package:em2rp/views/widgets/image/profile_picture_selector.dart';
|
||||||
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
||||||
|
import 'package:em2rp/views/widgets/notification_preferences_widget.dart';
|
||||||
|
|
||||||
class MyAccountPage extends StatelessWidget {
|
class MyAccountPage extends StatelessWidget {
|
||||||
const MyAccountPage({super.key});
|
const MyAccountPage({super.key});
|
||||||
@@ -86,6 +87,13 @@ class MyAccountPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Section Préférences de notifications
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 600),
|
||||||
|
child: const NotificationPreferencesWidget(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
234
em2rp/lib/views/widgets/alert_item.dart
Normal file
234
em2rp/lib/views/widgets/alert_item.dart
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:em2rp/models/alert_model.dart';
|
||||||
|
// import 'package:timeago/timeago.dart' as timeago; // TODO: Ajouter dépendance dans pubspec.yaml
|
||||||
|
|
||||||
|
/// Widget pour afficher une alerte individuelle
|
||||||
|
class AlertItem extends StatelessWidget {
|
||||||
|
final AlertModel alert;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final VoidCallback? onMarkAsRead;
|
||||||
|
final VoidCallback? onDelete;
|
||||||
|
|
||||||
|
const AlertItem({
|
||||||
|
super.key,
|
||||||
|
required this.alert,
|
||||||
|
this.onTap,
|
||||||
|
this.onMarkAsRead,
|
||||||
|
this.onDelete,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Dismissible(
|
||||||
|
key: Key(alert.id),
|
||||||
|
background: _buildSwipeBackground(
|
||||||
|
Colors.blue,
|
||||||
|
Icons.check,
|
||||||
|
Alignment.centerLeft,
|
||||||
|
),
|
||||||
|
secondaryBackground: _buildSwipeBackground(
|
||||||
|
Colors.red,
|
||||||
|
Icons.delete,
|
||||||
|
Alignment.centerRight,
|
||||||
|
),
|
||||||
|
confirmDismiss: (direction) async {
|
||||||
|
if (direction == DismissDirection.startToEnd) {
|
||||||
|
// Swipe vers la droite = marquer comme lu
|
||||||
|
onMarkAsRead?.call();
|
||||||
|
return false; // Ne pas supprimer le widget
|
||||||
|
} else {
|
||||||
|
// Swipe vers la gauche = supprimer
|
||||||
|
return await _confirmDelete(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
color: alert.isRead ? Colors.white : Colors.blue.shade50,
|
||||||
|
elevation: alert.isRead ? 1 : 2,
|
||||||
|
child: ListTile(
|
||||||
|
leading: _buildIcon(),
|
||||||
|
title: Text(
|
||||||
|
alert.message,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: alert.isRead ? FontWeight.normal : FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
_formatDate(alert.createdAt),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (alert.isResolved) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.check_circle, size: 14, color: Colors.green),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Résolu',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.green,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: !alert.isRead
|
||||||
|
? Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getSeverityColor(alert.severity),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Nouveau',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSwipeBackground(Color color, IconData icon, Alignment alignment) {
|
||||||
|
return Container(
|
||||||
|
color: color,
|
||||||
|
alignment: alignment,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Icon(icon, color: Colors.white, size: 28),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildIcon() {
|
||||||
|
IconData iconData;
|
||||||
|
Color iconColor;
|
||||||
|
|
||||||
|
switch (alert.type) {
|
||||||
|
case AlertType.eventCreated:
|
||||||
|
case AlertType.eventModified:
|
||||||
|
case AlertType.eventAssigned:
|
||||||
|
iconData = Icons.event;
|
||||||
|
iconColor = Colors.blue;
|
||||||
|
break;
|
||||||
|
case AlertType.workforceAdded:
|
||||||
|
iconData = Icons.group_add;
|
||||||
|
iconColor = Colors.green;
|
||||||
|
break;
|
||||||
|
case AlertType.eventCancelled:
|
||||||
|
iconData = Icons.event_busy;
|
||||||
|
iconColor = Colors.red;
|
||||||
|
break;
|
||||||
|
case AlertType.maintenanceDue:
|
||||||
|
case AlertType.maintenanceReminder:
|
||||||
|
iconData = Icons.build;
|
||||||
|
iconColor = Colors.orange;
|
||||||
|
break;
|
||||||
|
case AlertType.lost:
|
||||||
|
iconData = Icons.error;
|
||||||
|
iconColor = Colors.red;
|
||||||
|
break;
|
||||||
|
case AlertType.equipmentMissing:
|
||||||
|
iconData = Icons.warning;
|
||||||
|
iconColor = Colors.orange;
|
||||||
|
break;
|
||||||
|
case AlertType.lowStock:
|
||||||
|
iconData = Icons.inventory_2;
|
||||||
|
iconColor = Colors.orange;
|
||||||
|
break;
|
||||||
|
case AlertType.conflict:
|
||||||
|
iconData = Icons.error_outline;
|
||||||
|
iconColor = Colors.red;
|
||||||
|
break;
|
||||||
|
case AlertType.quantityMismatch:
|
||||||
|
iconData = Icons.compare_arrows;
|
||||||
|
iconColor = Colors.orange;
|
||||||
|
break;
|
||||||
|
case AlertType.damaged:
|
||||||
|
iconData = Icons.broken_image;
|
||||||
|
iconColor = Colors.red;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: iconColor.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(iconData, color: iconColor, size: 24),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getSeverityColor(AlertSeverity severity) {
|
||||||
|
switch (severity) {
|
||||||
|
case AlertSeverity.info:
|
||||||
|
return Colors.blue;
|
||||||
|
case AlertSeverity.warning:
|
||||||
|
return Colors.orange;
|
||||||
|
case AlertSeverity.critical:
|
||||||
|
return Colors.red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDate(DateTime date) {
|
||||||
|
// TODO: Utiliser timeago une fois la dépendance ajoutée
|
||||||
|
final now = DateTime.now();
|
||||||
|
final difference = now.difference(date);
|
||||||
|
|
||||||
|
if (difference.inSeconds < 60) {
|
||||||
|
return 'À l\'instant';
|
||||||
|
} else if (difference.inMinutes < 60) {
|
||||||
|
return 'Il y a ${difference.inMinutes} min';
|
||||||
|
} else if (difference.inHours < 24) {
|
||||||
|
return 'Il y a ${difference.inHours}h';
|
||||||
|
} else if (difference.inDays < 7) {
|
||||||
|
return 'Il y a ${difference.inDays}j';
|
||||||
|
} else {
|
||||||
|
return '${date.day}/${date.month}/${date.year}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _confirmDelete(BuildContext context) async {
|
||||||
|
return await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Supprimer l\'alerte ?'),
|
||||||
|
content: const Text('Cette action est irréversible.'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
onDelete?.call();
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
child: const Text('Supprimer'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,6 +3,8 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:em2rp/providers/local_user_provider.dart';
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
import 'package:em2rp/utils/colors.dart';
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
|
||||||
|
import '../notification_badge.dart' show NotificationBadge;
|
||||||
|
|
||||||
class CustomAppBar extends StatefulWidget implements PreferredSizeWidget {
|
class CustomAppBar extends StatefulWidget implements PreferredSizeWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final List<Widget>? actions;
|
final List<Widget>? actions;
|
||||||
@@ -29,6 +31,7 @@ class _CustomAppBarState extends State<CustomAppBar> {
|
|||||||
title: Text(widget.title),
|
title: Text(widget.title),
|
||||||
backgroundColor: AppColors.rouge,
|
backgroundColor: AppColors.rouge,
|
||||||
actions: [
|
actions: [
|
||||||
|
NotificationBadge(),
|
||||||
if (widget.showLogoutButton)
|
if (widget.showLogoutButton)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.logout, color: AppColors.blanc),
|
icon: const Icon(Icons.logout, color: AppColors.blanc),
|
||||||
|
|||||||
43
em2rp/lib/views/widgets/notification_badge.dart
Normal file
43
em2rp/lib/views/widgets/notification_badge.dart
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:em2rp/services/alert_service.dart';
|
||||||
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
/// Badge de notifications dans l'AppBar
|
||||||
|
class NotificationBadge extends StatelessWidget {
|
||||||
|
const NotificationBadge({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final localUserProvider = context.watch<LocalUserProvider>();
|
||||||
|
final userId = localUserProvider.currentUser?.uid;
|
||||||
|
|
||||||
|
if (userId == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return StreamBuilder<int>(
|
||||||
|
stream: AlertService().unreadCountStreamForUser(userId),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final count = snapshot.data ?? 0;
|
||||||
|
|
||||||
|
return Badge(
|
||||||
|
label: Text('$count'),
|
||||||
|
isLabelVisible: count > 0,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
textColor: Colors.white,
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.notifications),
|
||||||
|
onPressed: () => _openAlertsPage(context),
|
||||||
|
tooltip: 'Notifications',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openAlertsPage(BuildContext context) {
|
||||||
|
Navigator.of(context).pushNamed('/alerts');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
264
em2rp/lib/views/widgets/notification_preferences_widget.dart
Normal file
264
em2rp/lib/views/widgets/notification_preferences_widget.dart
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:em2rp/models/notification_preferences_model.dart';
|
||||||
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
/// Widget pour afficher et modifier les préférences de notifications
|
||||||
|
class NotificationPreferencesWidget extends StatefulWidget {
|
||||||
|
const NotificationPreferencesWidget({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NotificationPreferencesWidget> createState() => _NotificationPreferencesWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotificationPreferencesWidgetState extends State<NotificationPreferencesWidget> {
|
||||||
|
// État local pour feedback immédiat
|
||||||
|
NotificationPreferences? _localPrefs;
|
||||||
|
bool _isSaving = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer<LocalUserProvider>(
|
||||||
|
builder: (context, userProvider, _) {
|
||||||
|
final user = userProvider.currentUser;
|
||||||
|
if (user == null) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
// Utiliser les prefs locales si disponibles, sinon les prefs du user
|
||||||
|
final prefs = _localPrefs ?? user.notificationPreferences ?? NotificationPreferences.defaults();
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Titre section
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.notifications, color: Theme.of(context).primaryColor),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Préférences de notifications',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_isSaving) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Choisissez comment vous souhaitez être notifié',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 24),
|
||||||
|
|
||||||
|
// Canaux de notification
|
||||||
|
Text(
|
||||||
|
'Canaux de notification',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
_buildSwitchTile(
|
||||||
|
context,
|
||||||
|
title: 'Notifications in-app',
|
||||||
|
subtitle: 'Alertes dans l\'application',
|
||||||
|
value: prefs.inAppEnabled,
|
||||||
|
icon: Icons.app_settings_alt,
|
||||||
|
onChanged: (value) => _updatePrefs(
|
||||||
|
context,
|
||||||
|
prefs.copyWith(inAppEnabled: value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildSwitchTile(
|
||||||
|
context,
|
||||||
|
title: 'Notifications email',
|
||||||
|
subtitle: 'Recevoir des emails',
|
||||||
|
value: prefs.emailEnabled,
|
||||||
|
icon: Icons.email,
|
||||||
|
onChanged: (value) => _updatePrefs(
|
||||||
|
context,
|
||||||
|
prefs.copyWith(emailEnabled: value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildSwitchTile(
|
||||||
|
context,
|
||||||
|
title: 'Notifications push',
|
||||||
|
subtitle: 'Notifications navigateur',
|
||||||
|
value: prefs.pushEnabled,
|
||||||
|
icon: Icons.notifications_active,
|
||||||
|
onChanged: (value) => _updatePrefs(
|
||||||
|
context,
|
||||||
|
prefs.copyWith(pushEnabled: value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const Divider(height: 24),
|
||||||
|
|
||||||
|
// Types de notifications
|
||||||
|
Text(
|
||||||
|
'Types de notifications',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
_buildSwitchTile(
|
||||||
|
context,
|
||||||
|
title: 'Événements',
|
||||||
|
subtitle: 'Création, modification, assignations',
|
||||||
|
value: prefs.eventsNotifications,
|
||||||
|
icon: Icons.event,
|
||||||
|
onChanged: (value) => _updatePrefs(
|
||||||
|
context,
|
||||||
|
prefs.copyWith(eventsNotifications: value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildSwitchTile(
|
||||||
|
context,
|
||||||
|
title: 'Maintenance',
|
||||||
|
subtitle: 'Rappels de maintenance',
|
||||||
|
value: prefs.maintenanceNotifications,
|
||||||
|
icon: Icons.build,
|
||||||
|
onChanged: (value) => _updatePrefs(
|
||||||
|
context,
|
||||||
|
prefs.copyWith(maintenanceNotifications: value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildSwitchTile(
|
||||||
|
context,
|
||||||
|
title: 'Stock',
|
||||||
|
subtitle: 'Stock faible, quantités',
|
||||||
|
value: prefs.stockNotifications,
|
||||||
|
icon: Icons.inventory_2,
|
||||||
|
onChanged: (value) => _updatePrefs(
|
||||||
|
context,
|
||||||
|
prefs.copyWith(stockNotifications: value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
_buildSwitchTile(
|
||||||
|
context,
|
||||||
|
title: 'Équipement',
|
||||||
|
subtitle: 'Perdu, manquant, conflits',
|
||||||
|
value: prefs.equipmentNotifications,
|
||||||
|
icon: Icons.warning,
|
||||||
|
onChanged: (value) => _updatePrefs(
|
||||||
|
context,
|
||||||
|
prefs.copyWith(equipmentNotifications: value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSwitchTile(
|
||||||
|
BuildContext context, {
|
||||||
|
required String title,
|
||||||
|
required String subtitle,
|
||||||
|
required bool value,
|
||||||
|
required IconData icon,
|
||||||
|
required ValueChanged<bool> onChanged,
|
||||||
|
}) {
|
||||||
|
return SwitchListTile(
|
||||||
|
secondary: Icon(icon, color: value ? Theme.of(context).primaryColor : Colors.grey),
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
subtitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
value: value,
|
||||||
|
onChanged: _isSaving ? null : onChanged, // Désactiver pendant sauvegarde
|
||||||
|
activeColor: Theme.of(context).primaryColor,
|
||||||
|
inactiveThumbColor: Colors.grey.shade400, // Couleur visible quand OFF
|
||||||
|
inactiveTrackColor: Colors.grey.shade300, // Track visible quand OFF
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
dense: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updatePrefs(BuildContext context, NotificationPreferences newPrefs) async {
|
||||||
|
// Mise à jour locale immédiate pour feedback visuel
|
||||||
|
setState(() {
|
||||||
|
_localPrefs = newPrefs;
|
||||||
|
_isSaving = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final userProvider = context.read<LocalUserProvider>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await userProvider.updateNotificationPreferences(newPrefs);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isSaving = false;
|
||||||
|
_localPrefs = null; // Revenir aux prefs du provider
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Préférences enregistrées'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isSaving = false;
|
||||||
|
_localPrefs = null; // Rollback en cas d'erreur
|
||||||
|
});
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Erreur : $e'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
16
em2rp/node_modules/.bin/JSONStream
generated
vendored
Normal file
16
em2rp/node_modules/.bin/JSONStream
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../JSONStream/bin.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../JSONStream/bin.js" "$@"
|
||||||
|
fi
|
||||||
17
em2rp/node_modules/.bin/JSONStream.cmd
generated
vendored
Normal file
17
em2rp/node_modules/.bin/JSONStream.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\JSONStream\bin.js" %*
|
||||||
28
em2rp/node_modules/.bin/JSONStream.ps1
generated
vendored
Normal file
28
em2rp/node_modules/.bin/JSONStream.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../JSONStream/bin.js" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../JSONStream/bin.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../JSONStream/bin.js" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../JSONStream/bin.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
em2rp/node_modules/.bin/base64url
generated
vendored
Normal file
16
em2rp/node_modules/.bin/base64url
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../base64url/bin/base64url" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../base64url/bin/base64url" "$@"
|
||||||
|
fi
|
||||||
17
em2rp/node_modules/.bin/base64url.cmd
generated
vendored
Normal file
17
em2rp/node_modules/.bin/base64url.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\base64url\bin\base64url" %*
|
||||||
28
em2rp/node_modules/.bin/base64url.ps1
generated
vendored
Normal file
28
em2rp/node_modules/.bin/base64url.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../base64url/bin/base64url" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../base64url/bin/base64url" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../base64url/bin/base64url" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../base64url/bin/base64url" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
em2rp/node_modules/.bin/gcs-upload
generated
vendored
Normal file
16
em2rp/node_modules/.bin/gcs-upload
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../gcs-resumable-upload/cli.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../gcs-resumable-upload/cli.js" "$@"
|
||||||
|
fi
|
||||||
17
em2rp/node_modules/.bin/gcs-upload.cmd
generated
vendored
Normal file
17
em2rp/node_modules/.bin/gcs-upload.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\gcs-resumable-upload\cli.js" %*
|
||||||
28
em2rp/node_modules/.bin/gcs-upload.ps1
generated
vendored
Normal file
28
em2rp/node_modules/.bin/gcs-upload.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../gcs-resumable-upload/cli.js" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../gcs-resumable-upload/cli.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../gcs-resumable-upload/cli.js" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../gcs-resumable-upload/cli.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
em2rp/node_modules/.bin/gp12-pem
generated
vendored
Normal file
16
em2rp/node_modules/.bin/gp12-pem
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../google-p12-pem/bin/gp12-pem" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../google-p12-pem/bin/gp12-pem" "$@"
|
||||||
|
fi
|
||||||
17
em2rp/node_modules/.bin/gp12-pem.cmd
generated
vendored
Normal file
17
em2rp/node_modules/.bin/gp12-pem.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\google-p12-pem\bin\gp12-pem" %*
|
||||||
28
em2rp/node_modules/.bin/gp12-pem.ps1
generated
vendored
Normal file
28
em2rp/node_modules/.bin/gp12-pem.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../google-p12-pem/bin/gp12-pem" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../google-p12-pem/bin/gp12-pem" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../google-p12-pem/bin/gp12-pem" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../google-p12-pem/bin/gp12-pem" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
em2rp/node_modules/.bin/indent-string
generated
vendored
Normal file
16
em2rp/node_modules/.bin/indent-string
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../indent-string/cli.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../indent-string/cli.js" "$@"
|
||||||
|
fi
|
||||||
17
em2rp/node_modules/.bin/indent-string.cmd
generated
vendored
Normal file
17
em2rp/node_modules/.bin/indent-string.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\indent-string\cli.js" %*
|
||||||
28
em2rp/node_modules/.bin/indent-string.ps1
generated
vendored
Normal file
28
em2rp/node_modules/.bin/indent-string.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../indent-string/cli.js" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../indent-string/cli.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../indent-string/cli.js" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../indent-string/cli.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
em2rp/node_modules/.bin/pbjs
generated
vendored
Normal file
16
em2rp/node_modules/.bin/pbjs
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../protobufjs/bin/pbjs" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../protobufjs/bin/pbjs" "$@"
|
||||||
|
fi
|
||||||
17
em2rp/node_modules/.bin/pbjs.cmd
generated
vendored
Normal file
17
em2rp/node_modules/.bin/pbjs.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\protobufjs\bin\pbjs" %*
|
||||||
28
em2rp/node_modules/.bin/pbjs.ps1
generated
vendored
Normal file
28
em2rp/node_modules/.bin/pbjs.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../protobufjs/bin/pbjs" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../protobufjs/bin/pbjs" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../protobufjs/bin/pbjs" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../protobufjs/bin/pbjs" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
em2rp/node_modules/.bin/repeating
generated
vendored
Normal file
16
em2rp/node_modules/.bin/repeating
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../repeating/cli.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../repeating/cli.js" "$@"
|
||||||
|
fi
|
||||||
17
em2rp/node_modules/.bin/repeating.cmd
generated
vendored
Normal file
17
em2rp/node_modules/.bin/repeating.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\repeating\cli.js" %*
|
||||||
28
em2rp/node_modules/.bin/repeating.ps1
generated
vendored
Normal file
28
em2rp/node_modules/.bin/repeating.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../repeating/cli.js" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../repeating/cli.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../repeating/cli.js" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../repeating/cli.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
em2rp/node_modules/.bin/sshpk-conv
generated
vendored
Normal file
16
em2rp/node_modules/.bin/sshpk-conv
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../sshpk/bin/sshpk-conv" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../sshpk/bin/sshpk-conv" "$@"
|
||||||
|
fi
|
||||||
17
em2rp/node_modules/.bin/sshpk-conv.cmd
generated
vendored
Normal file
17
em2rp/node_modules/.bin/sshpk-conv.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\sshpk\bin\sshpk-conv" %*
|
||||||
28
em2rp/node_modules/.bin/sshpk-conv.ps1
generated
vendored
Normal file
28
em2rp/node_modules/.bin/sshpk-conv.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../sshpk/bin/sshpk-conv" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../sshpk/bin/sshpk-conv" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../sshpk/bin/sshpk-conv" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../sshpk/bin/sshpk-conv" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
em2rp/node_modules/.bin/sshpk-sign
generated
vendored
Normal file
16
em2rp/node_modules/.bin/sshpk-sign
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../sshpk/bin/sshpk-sign" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../sshpk/bin/sshpk-sign" "$@"
|
||||||
|
fi
|
||||||
17
em2rp/node_modules/.bin/sshpk-sign.cmd
generated
vendored
Normal file
17
em2rp/node_modules/.bin/sshpk-sign.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\sshpk\bin\sshpk-sign" %*
|
||||||
28
em2rp/node_modules/.bin/sshpk-sign.ps1
generated
vendored
Normal file
28
em2rp/node_modules/.bin/sshpk-sign.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../sshpk/bin/sshpk-sign" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../sshpk/bin/sshpk-sign" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../sshpk/bin/sshpk-sign" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../sshpk/bin/sshpk-sign" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
em2rp/node_modules/.bin/sshpk-verify
generated
vendored
Normal file
16
em2rp/node_modules/.bin/sshpk-verify
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../sshpk/bin/sshpk-verify" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../sshpk/bin/sshpk-verify" "$@"
|
||||||
|
fi
|
||||||
17
em2rp/node_modules/.bin/sshpk-verify.cmd
generated
vendored
Normal file
17
em2rp/node_modules/.bin/sshpk-verify.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\sshpk\bin\sshpk-verify" %*
|
||||||
28
em2rp/node_modules/.bin/sshpk-verify.ps1
generated
vendored
Normal file
28
em2rp/node_modules/.bin/sshpk-verify.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../sshpk/bin/sshpk-verify" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../sshpk/bin/sshpk-verify" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../sshpk/bin/sshpk-verify" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../sshpk/bin/sshpk-verify" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
4
em2rp/node_modules/.bin/uuid
generated
vendored
4
em2rp/node_modules/.bin/uuid
generated
vendored
@@ -10,7 +10,7 @@ case `uname` in
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
if [ -x "$basedir/node" ]; then
|
if [ -x "$basedir/node" ]; then
|
||||||
exec "$basedir/node" "$basedir/../uuid/dist/bin/uuid" "$@"
|
exec "$basedir/node" "$basedir/../node-uuid/bin/uuid" "$@"
|
||||||
else
|
else
|
||||||
exec node "$basedir/../uuid/dist/bin/uuid" "$@"
|
exec node "$basedir/../node-uuid/bin/uuid" "$@"
|
||||||
fi
|
fi
|
||||||
|
|||||||
2
em2rp/node_modules/.bin/uuid.cmd
generated
vendored
2
em2rp/node_modules/.bin/uuid.cmd
generated
vendored
@@ -14,4 +14,4 @@ IF EXIST "%dp0%\node.exe" (
|
|||||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
)
|
)
|
||||||
|
|
||||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\uuid\dist\bin\uuid" %*
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\node-uuid\bin\uuid" %*
|
||||||
|
|||||||
8
em2rp/node_modules/.bin/uuid.ps1
generated
vendored
8
em2rp/node_modules/.bin/uuid.ps1
generated
vendored
@@ -11,17 +11,17 @@ $ret=0
|
|||||||
if (Test-Path "$basedir/node$exe") {
|
if (Test-Path "$basedir/node$exe") {
|
||||||
# Support pipeline input
|
# Support pipeline input
|
||||||
if ($MyInvocation.ExpectingInput) {
|
if ($MyInvocation.ExpectingInput) {
|
||||||
$input | & "$basedir/node$exe" "$basedir/../uuid/dist/bin/uuid" $args
|
$input | & "$basedir/node$exe" "$basedir/../node-uuid/bin/uuid" $args
|
||||||
} else {
|
} else {
|
||||||
& "$basedir/node$exe" "$basedir/../uuid/dist/bin/uuid" $args
|
& "$basedir/node$exe" "$basedir/../node-uuid/bin/uuid" $args
|
||||||
}
|
}
|
||||||
$ret=$LASTEXITCODE
|
$ret=$LASTEXITCODE
|
||||||
} else {
|
} else {
|
||||||
# Support pipeline input
|
# Support pipeline input
|
||||||
if ($MyInvocation.ExpectingInput) {
|
if ($MyInvocation.ExpectingInput) {
|
||||||
$input | & "node$exe" "$basedir/../uuid/dist/bin/uuid" $args
|
$input | & "node$exe" "$basedir/../node-uuid/bin/uuid" $args
|
||||||
} else {
|
} else {
|
||||||
& "node$exe" "$basedir/../uuid/dist/bin/uuid" $args
|
& "node$exe" "$basedir/../node-uuid/bin/uuid" $args
|
||||||
}
|
}
|
||||||
$ret=$LASTEXITCODE
|
$ret=$LASTEXITCODE
|
||||||
}
|
}
|
||||||
|
|||||||
16
em2rp/node_modules/.bin/window-size
generated
vendored
Normal file
16
em2rp/node_modules/.bin/window-size
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../window-size/cli.js" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../window-size/cli.js" "$@"
|
||||||
|
fi
|
||||||
17
em2rp/node_modules/.bin/window-size.cmd
generated
vendored
Normal file
17
em2rp/node_modules/.bin/window-size.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\window-size\cli.js" %*
|
||||||
28
em2rp/node_modules/.bin/window-size.ps1
generated
vendored
Normal file
28
em2rp/node_modules/.bin/window-size.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../window-size/cli.js" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../window-size/cli.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../window-size/cli.js" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../window-size/cli.js" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
16
em2rp/node_modules/.bin/zonefile
generated
vendored
Normal file
16
em2rp/node_modules/.bin/zonefile
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||||
|
|
||||||
|
case `uname` in
|
||||||
|
*CYGWIN*|*MINGW*|*MSYS*)
|
||||||
|
if command -v cygpath > /dev/null 2>&1; then
|
||||||
|
basedir=`cygpath -w "$basedir"`
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -x "$basedir/node" ]; then
|
||||||
|
exec "$basedir/node" "$basedir/../dns-zonefile/bin/zonefile" "$@"
|
||||||
|
else
|
||||||
|
exec node "$basedir/../dns-zonefile/bin/zonefile" "$@"
|
||||||
|
fi
|
||||||
17
em2rp/node_modules/.bin/zonefile.cmd
generated
vendored
Normal file
17
em2rp/node_modules/.bin/zonefile.cmd
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@ECHO off
|
||||||
|
GOTO start
|
||||||
|
:find_dp0
|
||||||
|
SET dp0=%~dp0
|
||||||
|
EXIT /b
|
||||||
|
:start
|
||||||
|
SETLOCAL
|
||||||
|
CALL :find_dp0
|
||||||
|
|
||||||
|
IF EXIST "%dp0%\node.exe" (
|
||||||
|
SET "_prog=%dp0%\node.exe"
|
||||||
|
) ELSE (
|
||||||
|
SET "_prog=node"
|
||||||
|
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||||
|
)
|
||||||
|
|
||||||
|
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\dns-zonefile\bin\zonefile" %*
|
||||||
28
em2rp/node_modules/.bin/zonefile.ps1
generated
vendored
Normal file
28
em2rp/node_modules/.bin/zonefile.ps1
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||||
|
|
||||||
|
$exe=""
|
||||||
|
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||||
|
# Fix case when both the Windows and Linux builds of Node
|
||||||
|
# are installed in the same directory
|
||||||
|
$exe=".exe"
|
||||||
|
}
|
||||||
|
$ret=0
|
||||||
|
if (Test-Path "$basedir/node$exe") {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "$basedir/node$exe" "$basedir/../dns-zonefile/bin/zonefile" $args
|
||||||
|
} else {
|
||||||
|
& "$basedir/node$exe" "$basedir/../dns-zonefile/bin/zonefile" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
} else {
|
||||||
|
# Support pipeline input
|
||||||
|
if ($MyInvocation.ExpectingInput) {
|
||||||
|
$input | & "node$exe" "$basedir/../dns-zonefile/bin/zonefile" $args
|
||||||
|
} else {
|
||||||
|
& "node$exe" "$basedir/../dns-zonefile/bin/zonefile" $args
|
||||||
|
}
|
||||||
|
$ret=$LASTEXITCODE
|
||||||
|
}
|
||||||
|
exit $ret
|
||||||
@@ -13,6 +13,7 @@ dependencies:
|
|||||||
firebase_core: ^4.2.0
|
firebase_core: ^4.2.0
|
||||||
firebase_auth: ^6.1.1
|
firebase_auth: ^6.1.1
|
||||||
cloud_firestore: ^6.0.3
|
cloud_firestore: ^6.0.3
|
||||||
|
cloud_functions: ^6.0.4
|
||||||
google_sign_in: ^7.2.0
|
google_sign_in: ^7.2.0
|
||||||
provider: ^6.1.2
|
provider: ^6.1.2
|
||||||
firebase_storage: ^13.0.3
|
firebase_storage: ^13.0.3
|
||||||
@@ -55,6 +56,7 @@ dependencies:
|
|||||||
flutter_dropzone: ^4.2.1
|
flutter_dropzone: ^4.2.1
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
timeago: ^3.6.1
|
||||||
|
|
||||||
path: any
|
path: any
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user