diff --git a/HOWTO-PROKOV.md b/HOWTO-PROKOV.md new file mode 100644 index 00000000..2662da60 --- /dev/null +++ b/HOWTO-PROKOV.md @@ -0,0 +1,153 @@ +# Prokov - Gestion des tâches + +## Vue d'ensemble + +Prokov est l'outil de gestion de projets et tâches utilisé pour suivre l'avancement de tous les projets 2026. + +**URL** : https://prokov.unikoffice.com +**API** : https://prokov.unikoffice.com/api/ + +## Compte Claude + +Claude Code peut interagir directement avec l'API Prokov. + +| Paramètre | Valeur | +|-----------|--------| +| Email | pierre@d6mail.fr | +| Password | d66,Pierre | +| Entity | 1 | +| Role | owner | + +## Projets + +| ID | Projet | Parent | Description | +|----|--------|--------|-------------| +| 1 | Prokov | - | Gestionnaire de tâches | +| 2 | Sogoms | - | API auto-générée Go | +| 4 | Geosector | - | Application Amicales Pompiers | +| 14 | Geosector-App | 4 | App Flutter | +| 15 | Geosector-API | 4 | API backend | +| 16 | Geosector-Web | 4 | Site web | +| 5 | Cleo | - | - | +| 6 | Serveurs | - | Infra | +| 8 | UnikOffice | - | - | +| 21 | 2026 | - | Plateforme micro-services | +| 22 | 2026-Go | 21 | Modules Go (Thierry) | +| 23 | 2026-Flutter | 21 | App Flutter (Pierre) | +| 24 | 2026-Infra | 21 | Infrastructure (commun) | + +## Statuts + +| ID | Nom | Actif | +|----|-----|-------| +| 1 | Backlog | Oui | +| 2 | À faire | Oui | +| 3 | En cours | Oui | +| 4 | À tester | Oui | +| 5 | Livré | Oui | +| 6 | Terminé | Non | +| 7 | Archivé | Non | + +## Utilisation avec Claude Code + +### Lire les tâches d'un projet + +> "Montre-moi les tâches du projet 2026" + +Claude va récupérer les tâches via l'API. + +### Créer une tâche + +> "Crée une tâche 'Implémenter mod-cpu' dans 2026-Go avec priorité 3" + +### Mettre à jour un statut + +> "Passe la tâche #170 en statut 'En cours'" + +### Marquer comme terminé + +> "Marque la tâche #170 comme terminée" + +## API Endpoints + +### Authentification + +```bash +# Login (récupère le token JWT) +curl -s -X POST "https://prokov.unikoffice.com/api/auth/login" \ + -H "Content-Type: application/json" \ + -d '{"email":"pierre@d6mail.fr","password":"d66,Pierre"}' +``` + +### Projets + +```bash +# Liste des projets +curl -s "https://prokov.unikoffice.com/api/projects" \ + -H "Authorization: Bearer $TOKEN" + +# Créer un projet +curl -s -X POST "https://prokov.unikoffice.com/api/projects" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name":"Mon Projet","description":"...","color":"#2563eb"}' + +# Créer un sous-projet +curl -s -X POST "https://prokov.unikoffice.com/api/projects" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name":"Sous-Projet","parent_id":21}' +``` + +### Tâches + +```bash +# Tâches d'un projet +curl -s "https://prokov.unikoffice.com/api/tasks?project_id=21" \ + -H "Authorization: Bearer $TOKEN" + +# Créer une tâche +curl -s -X POST "https://prokov.unikoffice.com/api/tasks" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"title":"Ma tâche","project_id":22,"status_id":2,"priority":3}' + +# Mettre à jour une tâche +curl -s -X PUT "https://prokov.unikoffice.com/api/tasks/170" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"status_id":3}' +``` + +### Statuts + +```bash +# Liste des statuts +curl -s "https://prokov.unikoffice.com/api/statuses" \ + -H "Authorization: Bearer $TOKEN" +``` + +## Workflow Git (à implémenter) + +Le hook post-commit pourra détecter les `#ID` dans les messages de commit et mettre automatiquement les tâches en "À tester". + +```bash +git commit -m "feat: nouvelle fonctionnalité #170 #171" +# → Tâches 170 et 171 passent en statut 4 (À tester) +``` + +## Structure projets 2026 + +``` +/home/pierre/dev/2026/ +├── prokov/ # ID 1 - Gestionnaire tâches +├── sogoms/ # ID 2 - API Go +├── geosector/ # ID 4 - App géospatiale +│ ├── app/ # ID 14 +│ ├── api/ # ID 15 +│ └── web/ # ID 16 +├── resalice/ # Migration vers Sogoms +├── monipocket/ # À intégrer dans 2026 +├── unikoffice/ # ID 8 +└── cleo/ # ID 5 +``` diff --git a/VERSION b/VERSION index 87ce4929..b7276283 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.2 +3.6.2 diff --git a/api/TODO-API.md b/api/TODO-API.md index 3d029eff..8164d82f 100644 --- a/api/TODO-API.md +++ b/api/TODO-API.md @@ -1225,6 +1225,86 @@ php scripts/php/migrate_from_backup.php \ --- +#### 7. Statistiques Events pour Admin Flutter + +**Demandé le :** 22/12/2025 +**Objectif :** Permettre aux admins Flutter de consulter les logs Events avec des stats quotidiennes, hebdomadaires et mensuelles, et drill-down vers le détail. + +**Architecture choisie :** Stats pré-agrégées en SQL + détail JSONL à la demande + +**Pourquoi cette approche :** + +- Évite de parser les fichiers JSONL à chaque requête Flutter +- Transfert minimal (~1-10 KB par requête) +- Calculs hebdo/mensuel à la volée depuis `daily` (pas de tables supplémentaires) +- Détail paginé uniquement sur demande + +**Phase 1 : Base de données** ✅ (22/12/2025) + +- [x] Créer la table `event_stats_daily` + - Colonnes : `stat_date`, `entity_id`, `event_type`, `count`, `sum_amount`, `unique_users`, `metadata` + - Index : `(entity_id, stat_date)`, unique `(stat_date, entity_id, event_type)` +- [x] Script SQL de création : `scripts/sql/create_event_stats_daily.sql` + +**Phase 2 : CRON d'agrégation** ✅ (22/12/2025) + +- [x] Créer `scripts/cron/aggregate_event_stats.php` + - Parse le fichier JSONL de J-1 (ou date passée en paramètre) + - Agrège par entity_id et event_type + - INSERT/UPDATE dans `event_stats_daily` + - Calcule `unique_users` (COUNT DISTINCT sur user_id) + - Calcule `sum_amount` pour les passages + - Stocke metadata JSON (top 5 secteurs, erreurs fréquentes, etc.) +- [x] Ajouter au crontab : exécution à 01h00 chaque nuit (via deploy-api.sh) +- [x] Script de rattrapage : `php aggregate_event_stats.php --from=2025-01-01 --to=2025-12-21` + +**Phase 3 : Service EventStatsService** ✅ (22/12/2025) + +- [x] Créer `src/Services/EventStatsService.php` + - `getSummary(?int $entityId, ?string $date)` : Stats du jour + - `getDaily(?int $entityId, string $from, string $to, array $eventTypes)` : Stats journalières + - `getWeekly(?int $entityId, string $from, string $to, array $eventTypes)` : Calculé depuis daily + - `getMonthly(?int $entityId, int $year, array $eventTypes)` : Calculé depuis daily + - `getDetails(?int $entityId, string $date, ?string $eventType, int $limit, int $offset)` : Lecture JSONL paginée + - `getEventTypes()` : Liste des types d'événements disponibles + - `hasStatsForDate(string $date)` : Vérifie si stats existent + +**Phase 4 : Controller et Routes** ✅ (22/12/2025) + +- [x] Créer `src/Controllers/EventStatsController.php` + - `summary()` : GET /api/events/stats/summary?date= + - `daily()` : GET /api/events/stats/daily?from=&to=&events= + - `weekly()` : GET /api/events/stats/weekly?from=&to=&events= + - `monthly()` : GET /api/events/stats/monthly?year=&events= + - `details()` : GET /api/events/stats/details?date=&event=&limit=&offset= + - `types()` : GET /api/events/stats/types +- [x] Ajouter les routes dans `Router.php` +- [x] Vérification des droits : Admin entité (role_id = 2) ou Super-admin (role_id = 1) +- [x] Super-admin : peut voir toutes les entités (entity_id = NULL ou ?entity_id=X) + +**Phase 5 : Optimisations** ✅ (22/12/2025) + +- [x] Compression gzip sur les réponses JSON (si >1KB et client supporte) +- [x] Header `ETag` sur /summary et /daily (cache 5 min, 304 Not Modified) +- [x] Filtrage des champs sensibles dans /details (IP tronquée, user_agent supprimé) +- [x] Limite max 100 events par requête /details + +**Phase 6 : Tests et documentation** + +- [ ] Tests unitaires EventStatsService +- [ ] Tests endpoints avec différents rôles +- [ ] Documentation Postman/Swagger des endpoints +- [ ] Mise à jour TECHBOOK.md avec exemples de réponses JSON + +**Estimation :** 2-3 jours de développement + +**Dépendances :** + +- EventLogService déjà en place ✅ +- Fichiers JSONL générés quotidiennement ✅ + +--- + ### 🟢 PRIORITÉ BASSE #### 7. Amélioration de la suppression des utilisateurs diff --git a/api/deploy-api.sh b/api/deploy-api.sh index deffd8a9..1858df27 100755 --- a/api/deploy-api.sh +++ b/api/deploy-api.sh @@ -36,7 +36,7 @@ FINAL_OWNER_LOGS="nobody" FINAL_GROUP_LOGS="nginx" # Configuration de sauvegarde -BACKUP_DIR="/data/backup/geosector/api" +BACKUP_DIR="/home/pierre/samba/back/geosector/api" # Couleurs pour les messages GREEN='\033[0;32m' @@ -288,11 +288,11 @@ if [ "$DEST_HOST" != "local" ]; then incus exec ${DEST_CONTAINER} -- find ${API_PATH} -type d -exec chmod 755 {} \; && incus exec ${DEST_CONTAINER} -- find ${API_PATH} -type f -exec chmod 644 {} \; && - # Permissions spéciales pour logs + # Permissions spéciales pour logs (PHP-FPM tourne sous nobody) incus exec ${DEST_CONTAINER} -- mkdir -p ${API_PATH}/logs/events && incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER_LOGS}:${FINAL_GROUP} ${API_PATH}/logs && - incus exec ${DEST_CONTAINER} -- find ${API_PATH}/logs -type d -exec chmod 750 {} \; && - incus exec ${DEST_CONTAINER} -- find ${API_PATH}/logs -type f -exec chmod 640 {} \; && + incus exec ${DEST_CONTAINER} -- find ${API_PATH}/logs -type d -exec chmod 775 {} \; && + incus exec ${DEST_CONTAINER} -- find ${API_PATH}/logs -type f -exec chmod 664 {} \; && # Permissions spéciales pour uploads incus exec ${DEST_CONTAINER} -- mkdir -p ${API_PATH}/uploads && @@ -342,8 +342,8 @@ if [ "$DEST_HOST" != "local" ]; then # GEOSECTOR API - Security data cleanup (daily at 2am) 0 2 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/cleanup_security_data.php >> /var/www/geosector/api/logs/cleanup_security.log 2>&1 -# GEOSECTOR API - Stripe devices update (weekly Sunday at 3am) -0 3 * * 0 /usr/bin/php /var/www/geosector/api/scripts/cron/update_stripe_devices.php >> /var/www/geosector/api/logs/stripe_devices.log 2>&1 +# GEOSECTOR API - Event stats aggregation (daily at 1am) +0 1 * * * /usr/bin/php /var/www/geosector/api/scripts/cron/aggregate_event_stats.php >> /var/www/geosector/api/logs/aggregate_stats.log 2>&1 EOF # Installer le nouveau crontab @@ -380,4 +380,4 @@ fi echo_info "Deployment completed at: $(date)" # Journaliser le déploiement -echo "$(date '+%Y-%m-%d %H:%M:%S') - API deployed to ${ENV_NAME} (${DEST_CONTAINER}) - Archive: ${ARCHIVE_NAME}" >> ~/.geo_deploy_history \ No newline at end of file +echo "$(date '+%Y-%m-%d %H:%M:%S') - API deployed to ${ENV_NAME} (${DEST_CONTAINER}) - Archive: ${ARCHIVE_NAME}" >> ~/.geo_deploy_history diff --git a/api/docs/TECHBOOK.md b/api/docs/TECHBOOK.md index 3f876d4d..6c469922 100755 --- a/api/docs/TECHBOOK.md +++ b/api/docs/TECHBOOK.md @@ -89,7 +89,75 @@ PUT /api/users/123 // users.id 1. **Reçus fiscaux** : PDF auto (<5KB) pour dons, envoi email queue 2. **Logos entités** : Upload PNG/JPG, redimensionnement 250x250px, base64 3. **Migration** : Endpoints REST par entité (9 phases) -4. **CRONs** : Email queue (*/5), cleanup sécurité (2h), Stripe devices (dim 3h) +4. **CRONs** : Email queue (*/5), cleanup sécurité (2h) + +## 📊 Statistiques Events (Admin Flutter) + +### Architecture + +**Principe** : Stats pré-agrégées en SQL + détail JSONL à la demande + +| Source | Usage | Performance | +|--------|-------|-------------| +| Table `event_stats_daily` | Dashboard, graphiques, tendances | Instantané (~1ms) | +| Fichiers JSONL | Détail événements (clic sur stat) | Paginé (~50-100ms) | + +### Flux de données + +1. **EventLogService** écrit les événements dans `/logs/events/YYYY-MM-DD.jsonl` +2. **CRON nightly** agrège J-1 dans `event_stats_daily` +3. **API** sert les stats agrégées (SQL) ou le détail paginé (JSONL) +4. **Flutter Admin** affiche dashboard avec drill-down + +### Table d'agrégation + +**`event_stats_daily`** : Une ligne par (date, entité, type d'événement) + +| Colonne | Description | +|---------|-------------| +| `stat_date` | Date des stats | +| `entity_id` | Entité (NULL = global super-admin) | +| `event_type` | Type événement (login_success, passage_created, etc.) | +| `count` | Nombre d'occurrences | +| `sum_amount` | Somme montants (passages) | +| `unique_users` | Utilisateurs distincts | +| `metadata` | JSON agrégé (top secteurs, erreurs fréquentes, etc.) | + +### Endpoints API + +| Endpoint | Période | Source | Taille réponse | +|----------|---------|--------|----------------| +| `GET /events/stats/summary` | Jour courant | SQL | ~1 KB | +| `GET /events/stats/daily` | Plage dates | SQL | ~5 KB | +| `GET /events/stats/weekly` | Calculé depuis daily | SQL | ~2 KB | +| `GET /events/stats/monthly` | Calculé depuis daily | SQL | ~1 KB | +| `GET /events/details` | Détail paginé | JSONL | ~10 KB | + +### Optimisations transfert Flutter + +- **Pagination** : 50 events max par requête détail +- **Champs filtrés** : Pas d'IP ni user_agent complet dans les réponses +- **Compression gzip** : -70% sur JSON +- **Cache HTTP** : ETag sur stats (changent 1x/jour) +- **Calcul hebdo/mensuel** : À la volée depuis `daily` (pas de tables supplémentaires) + +### Types d'événements agrégés + +| Catégorie | Events | +|-----------|--------| +| **Auth** | login_success, login_failed, logout | +| **Passages** | passage_created, passage_updated, passage_deleted | +| **Secteurs** | sector_created, sector_updated, sector_deleted | +| **Users** | user_created, user_updated, user_deleted | +| **Entités** | entity_created, entity_updated, entity_deleted | +| **Opérations** | operation_created, operation_updated, operation_deleted | +| **Stripe** | stripe_payment_created, stripe_payment_success, stripe_payment_failed, stripe_payment_cancelled, stripe_terminal_error | + +### Accès et sécurité + +- **Rôle requis** : Admin entité (role_id = 2) ou Super-admin (role_id = 1) +- **Isolation** : Admin voit uniquement les stats de son entité +- **Super-admin** : Accès global (entity_id = NULL dans requêtes) ## 🚀 Déploiement @@ -172,4 +240,4 @@ DELETE FROM operations WHERE id = 850; --- -**Mis à jour : 26 Octobre 2025** +**Mis à jour : 22 Décembre 2025** diff --git a/api/index.php b/api/index.php index 4334a522..d47579d3 100755 --- a/api/index.php +++ b/api/index.php @@ -157,12 +157,21 @@ register_shutdown_function(function() use ($requestUri, $requestMethod) { // Alerter sur les erreurs 500 if ($statusCode >= 500) { $error = error_get_last(); + $errorMessage = $error['message'] ?? null; + + // Si pas d'erreur PHP, c'est probablement une exception capturée + // Le détail de l'erreur sera dans les logs applicatifs + if ($errorMessage === null) { + $errorMessage = 'Exception capturée (voir logs/app.log pour détails)'; + } + AlertService::trigger('HTTP_500', [ 'endpoint' => $requestUri, 'method' => $requestMethod, - 'error_message' => $error['message'] ?? 'Unknown error', - 'error_file' => $error['file'] ?? 'Unknown', + 'error_message' => $errorMessage, + 'error_file' => $error['file'] ?? 'N/A', 'error_line' => $error['line'] ?? 0, + 'stack_trace' => 'Consulter logs/app.log pour le stack trace complet', 'message' => "Erreur serveur 500 sur $requestUri" ], 'ERROR'); } diff --git a/api/scripts/cron/CRON.md b/api/scripts/cron/CRON.md index e85c5441..e990f4c6 100644 --- a/api/scripts/cron/CRON.md +++ b/api/scripts/cron/CRON.md @@ -94,30 +94,7 @@ Ce dossier contient les scripts automatisés de maintenance et de traitement pou --- -### 5. `update_stripe_devices.php` - -**Fonction** : Met à jour la liste des appareils Android certifiés pour Tap to Pay - -**Caractéristiques** : - -- Liste de 95+ devices intégrée -- Ajoute les nouveaux appareils certifiés -- Met à jour les versions Android minimales -- Désactive les appareils obsolètes -- Notification email si changements importants -- Possibilité de personnaliser via `/data/stripe_certified_devices.json` - -**Fréquence recommandée** : Hebdomadaire le dimanche à 3h - -**Ligne crontab** : - -```bash -0 3 * * 0 /usr/bin/php /var/www/geosector/api/scripts/cron/update_stripe_devices.php >> /var/www/geosector/api/logs/stripe_devices.log 2>&1 -``` - ---- - -### 6. `sync_databases.php` +### 5. `sync_databases.php` **Fonction** : Synchronise les bases de données entre environnements @@ -175,9 +152,6 @@ crontab -e # Rotation des logs événements (mensuel le 1er à 3h) 0 3 1 * * /usr/bin/php /var/www/geosector/api/scripts/cron/rotate_event_logs.php >> /var/www/geosector/api/logs/rotation_events.log 2>&1 - -# Mise à jour des devices Stripe (hebdomadaire dimanche à 3h) -0 3 * * 0 /usr/bin/php /var/www/geosector/api/scripts/cron/update_stripe_devices.php >> /var/www/geosector/api/logs/stripe_devices.log 2>&1 ``` ### 4. Vérifier que les CRONs sont actifs @@ -203,7 +177,6 @@ Tous les logs CRON sont stockés dans `/var/www/geosector/api/logs/` : - `cleanup_security.log` : Nettoyage des données de sécurité - `cleanup_logs.log` : Nettoyage des anciens fichiers logs - `rotation_events.log` : Rotation des logs événements JSONL -- `stripe_devices.log` : Mise à jour des devices Tap to Pay ### Vérification de l'exécution @@ -216,9 +189,6 @@ tail -n 50 /var/www/geosector/api/logs/cleanup_logs.log # Voir les dernières rotations des logs événements tail -n 50 /var/www/geosector/api/logs/rotation_events.log - -# Voir les dernières mises à jour Stripe -tail -n 50 /var/www/geosector/api/logs/stripe_devices.log ``` --- diff --git a/api/scripts/cron/aggregate_event_stats.php b/api/scripts/cron/aggregate_event_stats.php new file mode 100755 index 00000000..e1a932b7 --- /dev/null +++ b/api/scripts/cron/aggregate_event_stats.php @@ -0,0 +1,456 @@ +#!/usr/bin/env php +> /var/www/geosector/api/logs/aggregate_stats.log 2>&1 + */ + +declare(strict_types=1); + +// Configuration +define('LOCK_FILE', '/tmp/aggregate_event_stats.lock'); +define('EVENT_LOG_DIR', __DIR__ . '/../../logs/events'); + +// Empêcher l'exécution multiple simultanée +if (file_exists(LOCK_FILE)) { + $lockTime = filemtime(LOCK_FILE); + if (time() - $lockTime > 3600) { + unlink(LOCK_FILE); + } else { + die("[" . date('Y-m-d H:i:s') . "] Le processus est déjà en cours d'exécution\n"); + } +} + +file_put_contents(LOCK_FILE, (string) getmypid()); + +register_shutdown_function(function () { + if (file_exists(LOCK_FILE)) { + unlink(LOCK_FILE); + } +}); + +// Simuler l'environnement web pour AppConfig en CLI +if (php_sapi_name() === 'cli') { + $hostname = gethostname(); + if (strpos($hostname, 'pra') !== false) { + $_SERVER['SERVER_NAME'] = 'app3.geosector.fr'; + } elseif (strpos($hostname, 'rca') !== false) { + $_SERVER['SERVER_NAME'] = 'rapp.geosector.fr'; + } else { + $_SERVER['SERVER_NAME'] = 'dapp.geosector.fr'; + } + + $_SERVER['HTTP_HOST'] = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME']; + $_SERVER['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; + + if (!function_exists('getallheaders')) { + function getallheaders() + { + return []; + } + } +} + +// Chargement de l'environnement +require_once __DIR__ . '/../../vendor/autoload.php'; +require_once __DIR__ . '/../../src/Config/AppConfig.php'; +require_once __DIR__ . '/../../src/Core/Database.php'; +require_once __DIR__ . '/../../src/Services/LogService.php'; + +use App\Services\LogService; + +/** + * Parse les arguments CLI + */ +function parseArgs(array $argv): array +{ + $args = [ + 'date' => null, + 'from' => null, + 'to' => null, + ]; + + foreach ($argv as $arg) { + if (strpos($arg, '--date=') === 0) { + $args['date'] = substr($arg, 7); + } elseif (strpos($arg, '--from=') === 0) { + $args['from'] = substr($arg, 7); + } elseif (strpos($arg, '--to=') === 0) { + $args['to'] = substr($arg, 5); + } + } + + return $args; +} + +/** + * Génère la liste des dates à traiter + */ +function getDatesToProcess(array $args): array +{ + $dates = []; + + if ($args['date']) { + $dates[] = $args['date']; + } elseif ($args['from'] && $args['to']) { + $current = new DateTime($args['from']); + $end = new DateTime($args['to']); + while ($current <= $end) { + $dates[] = $current->format('Y-m-d'); + $current->modify('+1 day'); + } + } else { + // Par défaut : J-1 + $dates[] = date('Y-m-d', strtotime('-1 day')); + } + + return $dates; +} + +/** + * Parse un fichier JSONL et retourne les événements + */ +function parseJsonlFile(string $filePath): array +{ + $events = []; + + if (!file_exists($filePath)) { + return $events; + } + + $handle = fopen($filePath, 'r'); + if (!$handle) { + return $events; + } + + while (($line = fgets($handle)) !== false) { + $line = trim($line); + if (empty($line)) { + continue; + } + + $event = json_decode($line, true); + if ($event && isset($event['event'])) { + $events[] = $event; + } + } + + fclose($handle); + return $events; +} + +/** + * Agrège les événements par entity_id et event_type + */ +function aggregateEvents(array $events): array +{ + $stats = []; + + foreach ($events as $event) { + $entityId = $event['entity_id'] ?? null; + $eventType = $event['event'] ?? 'unknown'; + $userId = $event['user_id'] ?? null; + + // Clé d'agrégation : entity_id peut être NULL (stats globales) + $key = ($entityId ?? 'NULL') . '|' . $eventType; + + if (!isset($stats[$key])) { + $stats[$key] = [ + 'entity_id' => $entityId, + 'event_type' => $eventType, + 'count' => 0, + 'sum_amount' => 0.0, + 'user_ids' => [], + 'metadata' => [], + ]; + } + + $stats[$key]['count']++; + + // Collecter les user_ids pour unique_users + if ($userId !== null) { + $stats[$key]['user_ids'][$userId] = true; + } + + // Somme des montants pour les passages et paiements Stripe + if (in_array($eventType, ['passage_created', 'passage_updated'])) { + $amount = $event['amount'] ?? 0; + $stats[$key]['sum_amount'] += (float) $amount; + } elseif (in_array($eventType, ['stripe_payment_success', 'stripe_payment_created'])) { + // Montant en centimes -> euros + $amount = ($event['amount'] ?? 0) / 100; + $stats[$key]['sum_amount'] += (float) $amount; + } + + // Collecter metadata spécifiques + collectMetadata($stats[$key], $event); + } + + // Convertir user_ids en count + foreach ($stats as &$stat) { + $stat['unique_users'] = count($stat['user_ids']); + unset($stat['user_ids']); + + // Finaliser les metadata + $stat['metadata'] = finalizeMetadata($stat['metadata'], $stat['event_type']); + } + + return $stats; +} + +/** + * Collecte les métadonnées spécifiques par type d'événement + */ +function collectMetadata(array &$stat, array $event): void +{ + $eventType = $event['event'] ?? ''; + + switch ($eventType) { + case 'login_failed': + $reason = $event['reason'] ?? 'unknown'; + $stat['metadata']['reasons'][$reason] = ($stat['metadata']['reasons'][$reason] ?? 0) + 1; + break; + + case 'passage_created': + $sectorId = $event['sector_id'] ?? null; + if ($sectorId) { + $stat['metadata']['sectors'][$sectorId] = ($stat['metadata']['sectors'][$sectorId] ?? 0) + 1; + } + $paymentType = $event['payment_type'] ?? 'unknown'; + $stat['metadata']['payment_types'][$paymentType] = ($stat['metadata']['payment_types'][$paymentType] ?? 0) + 1; + break; + + case 'stripe_payment_failed': + $errorCode = $event['error_code'] ?? 'unknown'; + $stat['metadata']['error_codes'][$errorCode] = ($stat['metadata']['error_codes'][$errorCode] ?? 0) + 1; + break; + + case 'stripe_terminal_error': + $errorCode = $event['error_code'] ?? 'unknown'; + $stat['metadata']['error_codes'][$errorCode] = ($stat['metadata']['error_codes'][$errorCode] ?? 0) + 1; + break; + } +} + +/** + * Finalise les métadonnées (top 5, tri, etc.) + */ +function finalizeMetadata(array $metadata, string $eventType): ?array +{ + if (empty($metadata)) { + return null; + } + + $result = []; + + // Top 5 secteurs + if (isset($metadata['sectors'])) { + arsort($metadata['sectors']); + $result['top_sectors'] = array_slice($metadata['sectors'], 0, 5, true); + } + + // Raisons d'échec login + if (isset($metadata['reasons'])) { + arsort($metadata['reasons']); + $result['failure_reasons'] = $metadata['reasons']; + } + + // Types de paiement + if (isset($metadata['payment_types'])) { + arsort($metadata['payment_types']); + $result['payment_types'] = $metadata['payment_types']; + } + + // Codes d'erreur + if (isset($metadata['error_codes'])) { + arsort($metadata['error_codes']); + $result['error_codes'] = $metadata['error_codes']; + } + + return empty($result) ? null : $result; +} + +/** + * Insère ou met à jour les stats dans la base de données + */ +function upsertStats(PDO $db, string $date, array $stats): int +{ + $upsertedCount = 0; + + $sql = " + INSERT INTO event_stats_daily + (stat_date, entity_id, event_type, count, sum_amount, unique_users, metadata) + VALUES + (:stat_date, :entity_id, :event_type, :count, :sum_amount, :unique_users, :metadata) + ON DUPLICATE KEY UPDATE + count = VALUES(count), + sum_amount = VALUES(sum_amount), + unique_users = VALUES(unique_users), + metadata = VALUES(metadata), + updated_at = CURRENT_TIMESTAMP + "; + + $stmt = $db->prepare($sql); + + foreach ($stats as $stat) { + try { + $stmt->execute([ + 'stat_date' => $date, + 'entity_id' => $stat['entity_id'], + 'event_type' => $stat['event_type'], + 'count' => $stat['count'], + 'sum_amount' => $stat['sum_amount'], + 'unique_users' => $stat['unique_users'], + 'metadata' => $stat['metadata'] ? json_encode($stat['metadata'], JSON_UNESCAPED_UNICODE) : null, + ]); + $upsertedCount++; + } catch (PDOException $e) { + echo " ERREUR insertion {$stat['event_type']}: " . $e->getMessage() . "\n"; + } + } + + return $upsertedCount; +} + +/** + * Génère également les stats globales (entity_id = NULL) + */ +function generateGlobalStats(array $statsByEntity): array +{ + $globalStats = []; + + foreach ($statsByEntity as $stat) { + $eventType = $stat['event_type']; + + if (!isset($globalStats[$eventType])) { + $globalStats[$eventType] = [ + 'entity_id' => null, + 'event_type' => $eventType, + 'count' => 0, + 'sum_amount' => 0.0, + 'unique_users' => 0, + 'metadata' => null, + ]; + } + + $globalStats[$eventType]['count'] += $stat['count']; + $globalStats[$eventType]['sum_amount'] += $stat['sum_amount']; + $globalStats[$eventType]['unique_users'] += $stat['unique_users']; + } + + return array_values($globalStats); +} + +// ============================================================ +// MAIN +// ============================================================ + +try { + echo "[" . date('Y-m-d H:i:s') . "] Démarrage de l'agrégation des statistiques\n"; + + // Initialisation + $appConfig = AppConfig::getInstance(); + $config = $appConfig->getFullConfig(); + $environment = $appConfig->getEnvironment(); + + echo "Environnement: {$environment}\n"; + + Database::init($config['database']); + $db = Database::getInstance(); + + // Parser les arguments + $args = parseArgs($argv); + $dates = getDatesToProcess($args); + + echo "Dates à traiter: " . implode(', ', $dates) . "\n\n"; + + $totalStats = 0; + $totalEvents = 0; + + foreach ($dates as $date) { + $jsonlFile = EVENT_LOG_DIR . '/' . $date . '.jsonl'; + + echo "--- Traitement de {$date} ---\n"; + + if (!file_exists($jsonlFile)) { + echo " Fichier non trouvé: {$jsonlFile}\n"; + continue; + } + + $fileSize = filesize($jsonlFile); + echo " Fichier: " . basename($jsonlFile) . " (" . number_format($fileSize / 1024, 2) . " KB)\n"; + + // Parser le fichier + $events = parseJsonlFile($jsonlFile); + $eventCount = count($events); + echo " Événements parsés: {$eventCount}\n"; + + if ($eventCount === 0) { + echo " Aucun événement à agréger\n"; + continue; + } + + $totalEvents += $eventCount; + + // Agréger par entity/event_type + $stats = aggregateEvents($events); + echo " Agrégations par entité: " . count($stats) . "\n"; + + // Générer les stats globales + $globalStats = generateGlobalStats($stats); + echo " Agrégations globales: " . count($globalStats) . "\n"; + + // Fusionner stats entités + globales + $allStats = array_merge(array_values($stats), $globalStats); + + // Insérer en base + $upserted = upsertStats($db, $date, $allStats); + echo " Stats insérées/mises à jour: {$upserted}\n"; + + $totalStats += $upserted; + } + + // Résumé + echo "\n=== RÉSUMÉ ===\n"; + echo "Dates traitées: " . count($dates) . "\n"; + echo "Événements traités: {$totalEvents}\n"; + echo "Stats agrégées: {$totalStats}\n"; + + // Log + LogService::log('Agrégation des statistiques terminée', [ + 'level' => 'info', + 'script' => 'aggregate_event_stats.php', + 'environment' => $environment, + 'dates_count' => count($dates), + 'events_count' => $totalEvents, + 'stats_count' => $totalStats, + ]); + + echo "\n[" . date('Y-m-d H:i:s') . "] Agrégation terminée avec succès\n"; + +} catch (Exception $e) { + $errorMsg = 'Erreur lors de l\'agrégation: ' . $e->getMessage(); + + LogService::log($errorMsg, [ + 'level' => 'error', + 'script' => 'aggregate_event_stats.php', + 'trace' => $e->getTraceAsString(), + ]); + + echo "\n❌ ERREUR: {$errorMsg}\n"; + echo "Stack trace:\n" . $e->getTraceAsString() . "\n"; + + exit(1); +} + +exit(0); diff --git a/api/scripts/cron/update_stripe_devices.php b/api/scripts/cron/update_stripe_devices.php deleted file mode 100644 index 82098d7f..00000000 --- a/api/scripts/cron/update_stripe_devices.php +++ /dev/null @@ -1,444 +0,0 @@ -#!/usr/bin/env php - 3600) { // Lock de plus d'1 heure = processus bloqué - unlink(LOCK_FILE); - } else { - die("[" . date('Y-m-d H:i:s') . "] Le processus est déjà en cours d'exécution\n"); - } -} - -// Créer le fichier de lock -file_put_contents(LOCK_FILE, getmypid()); - -// Enregistrer un handler pour supprimer le lock en cas d'arrêt -register_shutdown_function(function() { - if (file_exists(LOCK_FILE)) { - unlink(LOCK_FILE); - } -}); - -// Simuler l'environnement web pour AppConfig en CLI -if (php_sapi_name() === 'cli') { - $hostname = gethostname(); - if (strpos($hostname, 'prod') !== false || strpos($hostname, 'pra') !== false) { - $_SERVER['SERVER_NAME'] = 'app3.geosector.fr'; - } elseif (strpos($hostname, 'rec') !== false || strpos($hostname, 'rca') !== false) { - $_SERVER['SERVER_NAME'] = 'rapp.geosector.fr'; - } else { - $_SERVER['SERVER_NAME'] = 'dapp.geosector.fr'; // DVA par défaut - } - $_SERVER['REQUEST_URI'] = '/cron/update_stripe_devices'; - $_SERVER['REQUEST_METHOD'] = 'CLI'; - $_SERVER['HTTP_HOST'] = $_SERVER['SERVER_NAME']; - $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; - - // Définir getallheaders si elle n'existe pas (CLI) - if (!function_exists('getallheaders')) { - function getallheaders() { - return []; - } - } -} - -// Charger l'environnement -require_once dirname(dirname(__DIR__)) . '/vendor/autoload.php'; -require_once dirname(dirname(__DIR__)) . '/src/Config/AppConfig.php'; -require_once dirname(dirname(__DIR__)) . '/src/Core/Database.php'; -require_once dirname(dirname(__DIR__)) . '/src/Services/LogService.php'; - -use App\Services\LogService; - -try { - echo "[" . date('Y-m-d H:i:s') . "] Début de la mise à jour des devices Stripe certifiés\n"; - - // Initialiser la configuration et la base de données - $appConfig = AppConfig::getInstance(); - $dbConfig = $appConfig->getDatabaseConfig(); - Database::init($dbConfig); - $db = Database::getInstance(); - - // Logger le début - LogService::log("Début de la mise à jour des devices Stripe certifiés", [ - 'source' => 'cron', - 'script' => 'update_stripe_devices.php' - ]); - - // Étape 1: Récupérer la liste des devices - $devicesData = fetchCertifiedDevices(); - - if (empty($devicesData)) { - echo "[" . date('Y-m-d H:i:s') . "] Aucune donnée de devices récupérée\n"; - LogService::log("Aucune donnée de devices récupérée", ['level' => 'warning']); - exit(1); - } - - // Étape 2: Traiter et mettre à jour la base de données - $stats = updateDatabase($db, $devicesData); - - // Étape 3: Logger les résultats - $message = sprintf( - "Mise à jour terminée : %d ajoutés, %d modifiés, %d désactivés, %d inchangés", - $stats['added'], - $stats['updated'], - $stats['disabled'], - $stats['unchanged'] - ); - - echo "[" . date('Y-m-d H:i:s') . "] $message\n"; - - LogService::log($message, [ - 'source' => 'cron', - 'stats' => $stats - ]); - - // Étape 4: Envoyer une notification si changements significatifs - if ($stats['added'] > 0 || $stats['disabled'] > 0) { - sendNotification($stats); - } - - echo "[" . date('Y-m-d H:i:s') . "] Mise à jour terminée avec succès\n"; - -} catch (Exception $e) { - $errorMsg = "Erreur lors de la mise à jour des devices: " . $e->getMessage(); - echo "[" . date('Y-m-d H:i:s') . "] $errorMsg\n"; - LogService::log($errorMsg, [ - 'level' => 'error', - 'trace' => $e->getTraceAsString() - ]); - exit(1); -} - -/** - * Récupère la liste des devices certifiés - * Essaie d'abord depuis une URL externe, puis depuis un fichier local en fallback - */ -function fetchCertifiedDevices(): array { - // Liste maintenue manuellement des devices certifiés en France - // Source: Documentation Stripe Terminal et tests confirmés - $frenchCertifiedDevices = [ - // Samsung Galaxy S Series - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S21', 'model_identifier' => 'SM-G991B', 'min_android_version' => 11], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S21+', 'model_identifier' => 'SM-G996B', 'min_android_version' => 11], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S21 Ultra', 'model_identifier' => 'SM-G998B', 'min_android_version' => 11], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S21 FE', 'model_identifier' => 'SM-G990B', 'min_android_version' => 11], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S22', 'model_identifier' => 'SM-S901B', 'min_android_version' => 12], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S22+', 'model_identifier' => 'SM-S906B', 'min_android_version' => 12], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S22 Ultra', 'model_identifier' => 'SM-S908B', 'min_android_version' => 12], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S23', 'model_identifier' => 'SM-S911B', 'min_android_version' => 13], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S23+', 'model_identifier' => 'SM-S916B', 'min_android_version' => 13], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S23 Ultra', 'model_identifier' => 'SM-S918B', 'min_android_version' => 13], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S23 FE', 'model_identifier' => 'SM-S711B', 'min_android_version' => 13], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S24', 'model_identifier' => 'SM-S921B', 'min_android_version' => 14], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S24+', 'model_identifier' => 'SM-S926B', 'min_android_version' => 14], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy S24 Ultra', 'model_identifier' => 'SM-S928B', 'min_android_version' => 14], - - // Samsung Galaxy Note - ['manufacturer' => 'Samsung', 'model' => 'Galaxy Note 20', 'model_identifier' => 'SM-N980F', 'min_android_version' => 10], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy Note 20 Ultra', 'model_identifier' => 'SM-N986B', 'min_android_version' => 10], - - // Samsung Galaxy Z Fold - ['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Fold3', 'model_identifier' => 'SM-F926B', 'min_android_version' => 11], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Fold4', 'model_identifier' => 'SM-F936B', 'min_android_version' => 12], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Fold5', 'model_identifier' => 'SM-F946B', 'min_android_version' => 13], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Fold6', 'model_identifier' => 'SM-F956B', 'min_android_version' => 14], - - // Samsung Galaxy Z Flip - ['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Flip3', 'model_identifier' => 'SM-F711B', 'min_android_version' => 11], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Flip4', 'model_identifier' => 'SM-F721B', 'min_android_version' => 12], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Flip5', 'model_identifier' => 'SM-F731B', 'min_android_version' => 13], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy Z Flip6', 'model_identifier' => 'SM-F741B', 'min_android_version' => 14], - - // Samsung Galaxy A Series (haut de gamme) - ['manufacturer' => 'Samsung', 'model' => 'Galaxy A54', 'model_identifier' => 'SM-A546B', 'min_android_version' => 13], - ['manufacturer' => 'Samsung', 'model' => 'Galaxy A73', 'model_identifier' => 'SM-A736B', 'min_android_version' => 12], - - // Google Pixel - ['manufacturer' => 'Google', 'model' => 'Pixel 6', 'model_identifier' => 'oriole', 'min_android_version' => 12], - ['manufacturer' => 'Google', 'model' => 'Pixel 6 Pro', 'model_identifier' => 'raven', 'min_android_version' => 12], - ['manufacturer' => 'Google', 'model' => 'Pixel 6a', 'model_identifier' => 'bluejay', 'min_android_version' => 12], - ['manufacturer' => 'Google', 'model' => 'Pixel 7', 'model_identifier' => 'panther', 'min_android_version' => 13], - ['manufacturer' => 'Google', 'model' => 'Pixel 7 Pro', 'model_identifier' => 'cheetah', 'min_android_version' => 13], - ['manufacturer' => 'Google', 'model' => 'Pixel 7a', 'model_identifier' => 'lynx', 'min_android_version' => 13], - ['manufacturer' => 'Google', 'model' => 'Pixel 8', 'model_identifier' => 'shiba', 'min_android_version' => 14], - ['manufacturer' => 'Google', 'model' => 'Pixel 8 Pro', 'model_identifier' => 'husky', 'min_android_version' => 14], - ['manufacturer' => 'Google', 'model' => 'Pixel 8a', 'model_identifier' => 'akita', 'min_android_version' => 14], - ['manufacturer' => 'Google', 'model' => 'Pixel 9', 'model_identifier' => 'tokay', 'min_android_version' => 14], - ['manufacturer' => 'Google', 'model' => 'Pixel 9 Pro', 'model_identifier' => 'caiman', 'min_android_version' => 14], - ['manufacturer' => 'Google', 'model' => 'Pixel 9 Pro XL', 'model_identifier' => 'komodo', 'min_android_version' => 14], - ['manufacturer' => 'Google', 'model' => 'Pixel Fold', 'model_identifier' => 'felix', 'min_android_version' => 13], - ['manufacturer' => 'Google', 'model' => 'Pixel Tablet', 'model_identifier' => 'tangorpro', 'min_android_version' => 13], - - // OnePlus - ['manufacturer' => 'OnePlus', 'model' => '9', 'model_identifier' => 'LE2113', 'min_android_version' => 11], - ['manufacturer' => 'OnePlus', 'model' => '9 Pro', 'model_identifier' => 'LE2123', 'min_android_version' => 11], - ['manufacturer' => 'OnePlus', 'model' => '10 Pro', 'model_identifier' => 'NE2213', 'min_android_version' => 12], - ['manufacturer' => 'OnePlus', 'model' => '10T', 'model_identifier' => 'CPH2413', 'min_android_version' => 12], - ['manufacturer' => 'OnePlus', 'model' => '11', 'model_identifier' => 'CPH2449', 'min_android_version' => 13], - ['manufacturer' => 'OnePlus', 'model' => '11R', 'model_identifier' => 'CPH2487', 'min_android_version' => 13], - ['manufacturer' => 'OnePlus', 'model' => '12', 'model_identifier' => 'CPH2581', 'min_android_version' => 14], - ['manufacturer' => 'OnePlus', 'model' => '12R', 'model_identifier' => 'CPH2585', 'min_android_version' => 14], - ['manufacturer' => 'OnePlus', 'model' => 'Open', 'model_identifier' => 'CPH2551', 'min_android_version' => 13], - - // Xiaomi - ['manufacturer' => 'Xiaomi', 'model' => 'Mi 11', 'model_identifier' => 'M2011K2G', 'min_android_version' => 11], - ['manufacturer' => 'Xiaomi', 'model' => 'Mi 11 Ultra', 'model_identifier' => 'M2102K1G', 'min_android_version' => 11], - ['manufacturer' => 'Xiaomi', 'model' => '12', 'model_identifier' => '2201123G', 'min_android_version' => 12], - ['manufacturer' => 'Xiaomi', 'model' => '12 Pro', 'model_identifier' => '2201122G', 'min_android_version' => 12], - ['manufacturer' => 'Xiaomi', 'model' => '12T Pro', 'model_identifier' => '2207122MC', 'min_android_version' => 12], - ['manufacturer' => 'Xiaomi', 'model' => '13', 'model_identifier' => '2211133G', 'min_android_version' => 13], - ['manufacturer' => 'Xiaomi', 'model' => '13 Pro', 'model_identifier' => '2210132G', 'min_android_version' => 13], - ['manufacturer' => 'Xiaomi', 'model' => '13T Pro', 'model_identifier' => '23078PND5G', 'min_android_version' => 13], - ['manufacturer' => 'Xiaomi', 'model' => '14', 'model_identifier' => '23127PN0CG', 'min_android_version' => 14], - ['manufacturer' => 'Xiaomi', 'model' => '14 Pro', 'model_identifier' => '23116PN5BG', 'min_android_version' => 14], - ['manufacturer' => 'Xiaomi', 'model' => '14 Ultra', 'model_identifier' => '24030PN60G', 'min_android_version' => 14], - - // OPPO - ['manufacturer' => 'OPPO', 'model' => 'Find X3 Pro', 'model_identifier' => 'CPH2173', 'min_android_version' => 11], - ['manufacturer' => 'OPPO', 'model' => 'Find X5 Pro', 'model_identifier' => 'CPH2305', 'min_android_version' => 12], - ['manufacturer' => 'OPPO', 'model' => 'Find X6 Pro', 'model_identifier' => 'CPH2449', 'min_android_version' => 13], - ['manufacturer' => 'OPPO', 'model' => 'Find N2', 'model_identifier' => 'CPH2399', 'min_android_version' => 13], - ['manufacturer' => 'OPPO', 'model' => 'Find N3', 'model_identifier' => 'CPH2499', 'min_android_version' => 13], - - // Realme - ['manufacturer' => 'Realme', 'model' => 'GT 2 Pro', 'model_identifier' => 'RMX3301', 'min_android_version' => 12], - ['manufacturer' => 'Realme', 'model' => 'GT 3', 'model_identifier' => 'RMX3709', 'min_android_version' => 13], - ['manufacturer' => 'Realme', 'model' => 'GT 5 Pro', 'model_identifier' => 'RMX3888', 'min_android_version' => 14], - - // Honor - ['manufacturer' => 'Honor', 'model' => 'Magic5 Pro', 'model_identifier' => 'PGT-N19', 'min_android_version' => 13], - ['manufacturer' => 'Honor', 'model' => 'Magic6 Pro', 'model_identifier' => 'BVL-N49', 'min_android_version' => 14], - ['manufacturer' => 'Honor', 'model' => '90', 'model_identifier' => 'REA-NX9', 'min_android_version' => 13], - - // ASUS - ['manufacturer' => 'ASUS', 'model' => 'Zenfone 9', 'model_identifier' => 'AI2202', 'min_android_version' => 12], - ['manufacturer' => 'ASUS', 'model' => 'Zenfone 10', 'model_identifier' => 'AI2302', 'min_android_version' => 13], - ['manufacturer' => 'ASUS', 'model' => 'ROG Phone 7', 'model_identifier' => 'AI2205', 'min_android_version' => 13], - - // Nothing - ['manufacturer' => 'Nothing', 'model' => 'Phone (1)', 'model_identifier' => 'A063', 'min_android_version' => 12], - ['manufacturer' => 'Nothing', 'model' => 'Phone (2)', 'model_identifier' => 'A065', 'min_android_version' => 13], - ['manufacturer' => 'Nothing', 'model' => 'Phone (2a)', 'model_identifier' => 'A142', 'min_android_version' => 14], - ]; - - // Essayer de charger depuis un fichier JSON local si présent - if (file_exists(DEVICES_LOCAL_FILE)) { - $localData = json_decode(file_get_contents(DEVICES_LOCAL_FILE), true); - if (!empty($localData)) { - echo "[" . date('Y-m-d H:i:s') . "] Données chargées depuis le fichier local\n"; - return array_merge($frenchCertifiedDevices, $localData); - } - } - - echo "[" . date('Y-m-d H:i:s') . "] Utilisation de la liste intégrée des devices certifiés\n"; - return $frenchCertifiedDevices; -} - -/** - * Met à jour la base de données avec les nouvelles données - */ -function updateDatabase($db, array $devices): array { - $stats = [ - 'added' => 0, - 'updated' => 0, - 'disabled' => 0, - 'unchanged' => 0, - 'total' => 0 - ]; - - // Récupérer tous les devices existants - $stmt = $db->prepare("SELECT * FROM stripe_android_certified_devices WHERE country = 'FR'"); - $stmt->execute(); - $existingDevices = []; - while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { - $key = $row['manufacturer'] . '|' . $row['model'] . '|' . $row['model_identifier']; - $existingDevices[$key] = $row; - } - - // Marquer tous les devices pour tracking - $processedKeys = []; - - // Traiter chaque device de la nouvelle liste - foreach ($devices as $device) { - $key = $device['manufacturer'] . '|' . $device['model'] . '|' . $device['model_identifier']; - $processedKeys[$key] = true; - - if (isset($existingDevices[$key])) { - // Le device existe, vérifier s'il faut le mettre à jour - $existing = $existingDevices[$key]; - - // Vérifier si des champs ont changé - $needsUpdate = false; - if ($existing['min_android_version'] != $device['min_android_version']) { - $needsUpdate = true; - } - if ($existing['tap_to_pay_certified'] != 1) { - $needsUpdate = true; - } - - if ($needsUpdate) { - $stmt = $db->prepare(" - UPDATE stripe_android_certified_devices - SET min_android_version = :min_version, - tap_to_pay_certified = 1, - last_verified = NOW(), - updated_at = NOW() - WHERE manufacturer = :manufacturer - AND model = :model - AND model_identifier = :model_identifier - AND country = 'FR' - "); - $stmt->execute([ - 'min_version' => $device['min_android_version'], - 'manufacturer' => $device['manufacturer'], - 'model' => $device['model'], - 'model_identifier' => $device['model_identifier'] - ]); - $stats['updated']++; - - LogService::log("Device mis à jour", [ - 'device' => $device['manufacturer'] . ' ' . $device['model'] - ]); - } else { - // Juste mettre à jour last_verified - $stmt = $db->prepare(" - UPDATE stripe_android_certified_devices - SET last_verified = NOW() - WHERE manufacturer = :manufacturer - AND model = :model - AND model_identifier = :model_identifier - AND country = 'FR' - "); - $stmt->execute([ - 'manufacturer' => $device['manufacturer'], - 'model' => $device['model'], - 'model_identifier' => $device['model_identifier'] - ]); - $stats['unchanged']++; - } - } else { - // Nouveau device, l'ajouter - $stmt = $db->prepare(" - INSERT INTO stripe_android_certified_devices - (manufacturer, model, model_identifier, tap_to_pay_certified, - certification_date, min_android_version, country, notes, last_verified) - VALUES - (:manufacturer, :model, :model_identifier, 1, - NOW(), :min_version, 'FR', 'Ajouté automatiquement via CRON', NOW()) - "); - $stmt->execute([ - 'manufacturer' => $device['manufacturer'], - 'model' => $device['model'], - 'model_identifier' => $device['model_identifier'], - 'min_version' => $device['min_android_version'] - ]); - $stats['added']++; - - LogService::log("Nouveau device ajouté", [ - 'device' => $device['manufacturer'] . ' ' . $device['model'] - ]); - } - } - - // Désactiver les devices qui ne sont plus dans la liste - foreach ($existingDevices as $key => $existing) { - if (!isset($processedKeys[$key]) && $existing['tap_to_pay_certified'] == 1) { - $stmt = $db->prepare(" - UPDATE stripe_android_certified_devices - SET tap_to_pay_certified = 0, - notes = CONCAT(IFNULL(notes, ''), ' | Désactivé le ', NOW(), ' (non présent dans la mise à jour)'), - updated_at = NOW() - WHERE id = :id - "); - $stmt->execute(['id' => $existing['id']]); - $stats['disabled']++; - - LogService::log("Device désactivé", [ - 'device' => $existing['manufacturer'] . ' ' . $existing['model'], - 'reason' => 'Non présent dans la liste mise à jour' - ]); - } - } - - $stats['total'] = count($devices); - return $stats; -} - -/** - * Envoie une notification email aux administrateurs si changements importants - */ -function sendNotification(array $stats): void { - try { - // Récupérer la configuration - $appConfig = AppConfig::getInstance(); - $emailConfig = $appConfig->getEmailConfig(); - - if (empty($emailConfig['admin_email'])) { - return; // Pas d'email admin configuré - } - - $db = Database::getInstance(); - - // Préparer le contenu de l'email - $subject = "Mise à jour des devices Stripe Tap to Pay"; - $body = "Bonjour,\n\n"; - $body .= "La mise à jour automatique de la liste des appareils certifiés Stripe Tap to Pay a été effectuée.\n\n"; - $body .= "Résumé des changements :\n"; - $body .= "- Nouveaux appareils ajoutés : " . $stats['added'] . "\n"; - $body .= "- Appareils mis à jour : " . $stats['updated'] . "\n"; - $body .= "- Appareils désactivés : " . $stats['disabled'] . "\n"; - $body .= "- Appareils inchangés : " . $stats['unchanged'] . "\n"; - $body .= "- Total d'appareils traités : " . $stats['total'] . "\n\n"; - - if ($stats['added'] > 0) { - $body .= "Les nouveaux appareils ont été automatiquement ajoutés à la base de données.\n"; - } - - if ($stats['disabled'] > 0) { - $body .= "Certains appareils ont été désactivés car ils ne sont plus certifiés.\n"; - } - - $body .= "\nConsultez les logs pour plus de détails.\n"; - $body .= "\nCordialement,\nLe système GeoSector"; - - // Insérer dans la queue d'emails - $stmt = $db->prepare(" - INSERT INTO email_queue - (to_email, subject, body, status, created_at, attempts) - VALUES - (:to_email, :subject, :body, 'pending', NOW(), 0) - "); - - $stmt->execute([ - 'to_email' => $emailConfig['admin_email'], - 'subject' => $subject, - 'body' => $body - ]); - - echo "[" . date('Y-m-d H:i:s') . "] Notification ajoutée à la queue d'emails\n"; - - } catch (Exception $e) { - // Ne pas faire échouer le script si l'email ne peut pas être envoyé - echo "[" . date('Y-m-d H:i:s') . "] Impossible d'envoyer la notification: " . $e->getMessage() . "\n"; - } -} \ No newline at end of file diff --git a/api/scripts/sql/alter_ope_pass_fk_sector_default_null.sql b/api/scripts/sql/alter_ope_pass_fk_sector_default_null.sql new file mode 100644 index 00000000..08fa5c81 --- /dev/null +++ b/api/scripts/sql/alter_ope_pass_fk_sector_default_null.sql @@ -0,0 +1,19 @@ +-- Migration: Modifier fk_sector pour avoir DEFAULT NULL au lieu de DEFAULT 0 +-- Raison: La FK vers ope_sectors(id) ne permet pas la valeur 0 (aucun secteur avec id=0) +-- Date: 2026-01-16 + +-- 1. D'abord, mettre à NULL les passages qui ont fk_sector = 0 +UPDATE ope_pass SET fk_sector = NULL WHERE fk_sector = 0; + +-- 2. Modifier la colonne pour avoir DEFAULT NULL +ALTER TABLE ope_pass MODIFY COLUMN fk_sector INT UNSIGNED DEFAULT NULL; + +-- Vérification +SELECT + COLUMN_NAME, + COLUMN_DEFAULT, + IS_NULLABLE +FROM INFORMATION_SCHEMA.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() +AND TABLE_NAME = 'ope_pass' +AND COLUMN_NAME = 'fk_sector'; diff --git a/api/scripts/sql/create_event_stats_daily.sql b/api/scripts/sql/create_event_stats_daily.sql new file mode 100644 index 00000000..2ca6314f --- /dev/null +++ b/api/scripts/sql/create_event_stats_daily.sql @@ -0,0 +1,57 @@ +-- ============================================================ +-- Table event_stats_daily - Statistiques d'événements agrégées +-- Version: 1.0 +-- Date: 2025-12-22 +-- ============================================================ +-- +-- Usage: Exécuter dans les 3 environnements (DEV, REC, PROD) +-- mysql -u user -p database < create_event_stats_daily.sql +-- +-- Description: +-- Stocke les statistiques quotidiennes agrégées depuis les +-- fichiers JSONL (/logs/events/YYYY-MM-DD.jsonl) +-- Alimentée par le CRON aggregate_event_stats.php (1x/nuit) +-- +-- ============================================================ + +CREATE TABLE IF NOT EXISTS event_stats_daily ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + + -- Clés d'agrégation + stat_date DATE NOT NULL COMMENT 'Date des statistiques', + entity_id INT UNSIGNED NULL COMMENT 'ID entité (NULL = stats globales super-admin)', + event_type VARCHAR(50) NOT NULL COMMENT 'Type événement (login_success, passage_created, etc.)', + + -- Compteurs + count INT UNSIGNED DEFAULT 0 COMMENT 'Nombre d\'occurrences', + sum_amount DECIMAL(12,2) DEFAULT 0.00 COMMENT 'Somme des montants (passages/paiements)', + unique_users INT UNSIGNED DEFAULT 0 COMMENT 'Nombre d\'utilisateurs distincts', + + -- Métadonnées agrégées (JSON) + metadata JSON NULL COMMENT 'Données agrégées: top secteurs, erreurs fréquentes, etc.', + + -- Timestamps + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + -- Contraintes + UNIQUE KEY uk_date_entity_event (stat_date, entity_id, event_type), + INDEX idx_entity_date (entity_id, stat_date), + INDEX idx_date (stat_date), + INDEX idx_event_type (event_type) + +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci +COMMENT='Statistiques quotidiennes agrégées des événements (EventLogService)'; + +-- ============================================================ +-- Exemples de données attendues +-- ============================================================ +-- +-- | stat_date | entity_id | event_type | count | sum_amount | unique_users | +-- |------------|-----------|------------------|-------|------------|--------------| +-- | 2025-12-22 | 5 | login_success | 45 | 0.00 | 12 | +-- | 2025-12-22 | 5 | passage_created | 128 | 2450.00 | 8 | +-- | 2025-12-22 | 5 | stripe_payment_success | 12 | 890.00 | 6 | +-- | 2025-12-22 | NULL | login_success | 320 | 0.00 | 85 | <- Global +-- +-- ============================================================ diff --git a/api/scripts/sql/drop_stripe_devices_table.sql b/api/scripts/sql/drop_stripe_devices_table.sql new file mode 100644 index 00000000..64d56ad8 --- /dev/null +++ b/api/scripts/sql/drop_stripe_devices_table.sql @@ -0,0 +1,5 @@ +-- Suppression de la table stripe_android_certified_devices +-- Cette table n'est plus utilisée : la vérification de compatibilité Tap to Pay +-- se fait maintenant directement côté client via le SDK Stripe Terminal + +DROP TABLE IF EXISTS stripe_android_certified_devices; diff --git a/api/src/Controllers/EventStatsController.php b/api/src/Controllers/EventStatsController.php new file mode 100644 index 00000000..a350e9c6 --- /dev/null +++ b/api/src/Controllers/EventStatsController.php @@ -0,0 +1,484 @@ +db = Database::getInstance(); + $this->statsService = new EventStatsService(); + } + + /** + * GET /api/events/stats/summary + * Récupère le résumé des stats pour une date + * + * Query params: + * - date: Date (YYYY-MM-DD), défaut = aujourd'hui + */ + public function summary(): void + { + try { + if (!$this->checkAccess()) { + return; + } + + $entityId = $this->getEntityIdForQuery(); + $date = $_GET['date'] ?? date('Y-m-d'); + + // Validation de la date + if (!$this->isValidDate($date)) { + Response::json([ + 'status' => 'error', + 'message' => 'Format de date invalide (attendu: YYYY-MM-DD)', + ], 400); + return; + } + + $summary = $this->statsService->getSummary($entityId, $date); + + $this->jsonWithCache([ + 'status' => 'success', + 'data' => $summary, + ], true); + } catch (Exception $e) { + LogService::error('Erreur lors de la récupération du résumé des stats', [ + 'error' => $e->getMessage(), + ]); + Response::json([ + 'status' => 'error', + 'message' => 'Erreur lors de la récupération des statistiques', + ], 500); + } + } + + /** + * GET /api/events/stats/daily + * Récupère les stats journalières sur une plage de dates + * + * Query params: + * - from: Date début (YYYY-MM-DD), requis + * - to: Date fin (YYYY-MM-DD), requis + * - events: Types d'événements (comma-separated), optionnel + */ + public function daily(): void + { + try { + if (!$this->checkAccess()) { + return; + } + + $entityId = $this->getEntityIdForQuery(); + $from = $_GET['from'] ?? null; + $to = $_GET['to'] ?? null; + $eventTypes = isset($_GET['events']) ? explode(',', $_GET['events']) : []; + + // Validation des dates + if (!$from || !$to) { + Response::json([ + 'status' => 'error', + 'message' => 'Les paramètres from et to sont requis', + ], 400); + return; + } + + if (!$this->isValidDate($from) || !$this->isValidDate($to)) { + Response::json([ + 'status' => 'error', + 'message' => 'Format de date invalide (attendu: YYYY-MM-DD)', + ], 400); + return; + } + + if ($from > $to) { + Response::json([ + 'status' => 'error', + 'message' => 'La date de début doit être antérieure à la date de fin', + ], 400); + return; + } + + // Limiter la plage à 90 jours + $daysDiff = (strtotime($to) - strtotime($from)) / 86400; + if ($daysDiff > 90) { + Response::json([ + 'status' => 'error', + 'message' => 'La plage de dates ne peut pas dépasser 90 jours', + ], 400); + return; + } + + $daily = $this->statsService->getDaily($entityId, $from, $to, $eventTypes); + + $this->jsonWithCache([ + 'status' => 'success', + 'data' => [ + 'from' => $from, + 'to' => $to, + 'days' => $daily, + ], + ], true); + } catch (Exception $e) { + LogService::error('Erreur lors de la récupération des stats journalières', [ + 'error' => $e->getMessage(), + ]); + Response::json([ + 'status' => 'error', + 'message' => 'Erreur lors de la récupération des statistiques', + ], 500); + } + } + + /** + * GET /api/events/stats/weekly + * Récupère les stats hebdomadaires sur une plage de dates + * + * Query params: + * - from: Date début (YYYY-MM-DD), requis + * - to: Date fin (YYYY-MM-DD), requis + * - events: Types d'événements (comma-separated), optionnel + */ + public function weekly(): void + { + try { + if (!$this->checkAccess()) { + return; + } + + $entityId = $this->getEntityIdForQuery(); + $from = $_GET['from'] ?? null; + $to = $_GET['to'] ?? null; + $eventTypes = isset($_GET['events']) ? explode(',', $_GET['events']) : []; + + // Validation des dates + if (!$from || !$to) { + Response::json([ + 'status' => 'error', + 'message' => 'Les paramètres from et to sont requis', + ], 400); + return; + } + + if (!$this->isValidDate($from) || !$this->isValidDate($to)) { + Response::json([ + 'status' => 'error', + 'message' => 'Format de date invalide (attendu: YYYY-MM-DD)', + ], 400); + return; + } + + // Limiter la plage à 1 an + $daysDiff = (strtotime($to) - strtotime($from)) / 86400; + if ($daysDiff > 365) { + Response::json([ + 'status' => 'error', + 'message' => 'La plage de dates ne peut pas dépasser 1 an', + ], 400); + return; + } + + $weekly = $this->statsService->getWeekly($entityId, $from, $to, $eventTypes); + + Response::json([ + 'status' => 'success', + 'data' => [ + 'from' => $from, + 'to' => $to, + 'weeks' => $weekly, + ], + ]); + } catch (Exception $e) { + LogService::error('Erreur lors de la récupération des stats hebdomadaires', [ + 'error' => $e->getMessage(), + ]); + Response::json([ + 'status' => 'error', + 'message' => 'Erreur lors de la récupération des statistiques', + ], 500); + } + } + + /** + * GET /api/events/stats/monthly + * Récupère les stats mensuelles pour une année + * + * Query params: + * - year: Année (YYYY), défaut = année courante + * - events: Types d'événements (comma-separated), optionnel + */ + public function monthly(): void + { + try { + if (!$this->checkAccess()) { + return; + } + + $entityId = $this->getEntityIdForQuery(); + $year = isset($_GET['year']) ? (int) $_GET['year'] : (int) date('Y'); + $eventTypes = isset($_GET['events']) ? explode(',', $_GET['events']) : []; + + // Validation de l'année + if ($year < 2020 || $year > (int) date('Y') + 1) { + Response::json([ + 'status' => 'error', + 'message' => 'Année invalide', + ], 400); + return; + } + + $monthly = $this->statsService->getMonthly($entityId, $year, $eventTypes); + + Response::json([ + 'status' => 'success', + 'data' => [ + 'year' => $year, + 'months' => $monthly, + ], + ]); + } catch (Exception $e) { + LogService::error('Erreur lors de la récupération des stats mensuelles', [ + 'error' => $e->getMessage(), + ]); + Response::json([ + 'status' => 'error', + 'message' => 'Erreur lors de la récupération des statistiques', + ], 500); + } + } + + /** + * GET /api/events/stats/details + * Récupère le détail des événements (lecture JSONL paginée) + * + * Query params: + * - date: Date (YYYY-MM-DD), requis + * - event: Type d'événement, optionnel + * - limit: Nombre max (défaut 50, max 100) + * - offset: Décalage pour pagination (défaut 0) + */ + public function details(): void + { + try { + if (!$this->checkAccess()) { + return; + } + + $entityId = $this->getEntityIdForQuery(); + $date = $_GET['date'] ?? null; + $eventType = $_GET['event'] ?? null; + $limit = isset($_GET['limit']) ? min((int) $_GET['limit'], 100) : 50; + $offset = isset($_GET['offset']) ? max((int) $_GET['offset'], 0) : 0; + + // Validation de la date + if (!$date) { + Response::json([ + 'status' => 'error', + 'message' => 'Le paramètre date est requis', + ], 400); + return; + } + + if (!$this->isValidDate($date)) { + Response::json([ + 'status' => 'error', + 'message' => 'Format de date invalide (attendu: YYYY-MM-DD)', + ], 400); + return; + } + + $details = $this->statsService->getDetails($entityId, $date, $eventType, $limit, $offset); + + Response::json([ + 'status' => 'success', + 'data' => $details, + ]); + } catch (Exception $e) { + LogService::error('Erreur lors de la récupération du détail des événements', [ + 'error' => $e->getMessage(), + ]); + Response::json([ + 'status' => 'error', + 'message' => 'Erreur lors de la récupération des événements', + ], 500); + } + } + + /** + * GET /api/events/stats/types + * Récupère la liste des types d'événements disponibles + */ + public function types(): void + { + try { + if (!$this->checkAccess()) { + return; + } + + $types = $this->statsService->getEventTypes(); + + Response::json([ + 'status' => 'success', + 'data' => $types, + ]); + } catch (Exception $e) { + LogService::error('Erreur lors de la récupération des types d\'événements', [ + 'error' => $e->getMessage(), + ]); + Response::json([ + 'status' => 'error', + 'message' => 'Erreur lors de la récupération des types', + ], 500); + } + } + + // ==================== MÉTHODES PRIVÉES ==================== + + /** + * Vérifie si l'utilisateur a accès aux stats + */ + private function checkAccess(): bool + { + $roleId = Session::getRole(); + + if (!in_array($roleId, self::ALLOWED_ROLES)) { + Response::json([ + 'status' => 'error', + 'message' => 'Accès non autorisé. Rôle Admin ou Super-admin requis.', + ], 403); + return false; + } + + return true; + } + + /** + * Détermine l'entity_id à utiliser pour la requête + * Super-admin (role_id = 1) peut voir toutes les entités (null) + * Admin (role_id = 2) voit uniquement son entité + */ + private function getEntityIdForQuery(): ?int + { + $roleId = Session::getRole(); + + // Super-admin : accès global + if ($roleId === 1) { + // Permettre de filtrer par entité si spécifié + if (isset($_GET['entity_id'])) { + return (int) $_GET['entity_id']; + } + return null; // Stats globales + } + + // Admin : uniquement son entité + return Session::getEntityId(); + } + + /** + * Valide le format d'une date + */ + private function isValidDate(string $date): bool + { + $d = \DateTime::createFromFormat('Y-m-d', $date); + return $d && $d->format('Y-m-d') === $date; + } + + /** + * Envoie une réponse JSON avec support ETag et compression gzip + * + * @param array $data Données à envoyer + * @param bool $useCache Activer le cache ETag + */ + private function jsonWithCache(array $data, bool $useCache = true): void + { + // Nettoyer tout buffer existant + while (ob_get_level() > 0) { + ob_end_clean(); + } + + // Encoder en JSON + $jsonResponse = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + if ($jsonResponse === false) { + Response::json([ + 'status' => 'error', + 'message' => 'Erreur d\'encodage de la réponse', + ], 500); + return; + } + + // Headers CORS + $origin = $_SERVER['HTTP_ORIGIN'] ?? '*'; + header("Access-Control-Allow-Origin: $origin"); + header('Access-Control-Allow-Credentials: true'); + header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); + header('Access-Control-Allow-Headers: Origin, Content-Type, X-Auth-Token, Authorization, X-Requested-With'); + header('Access-Control-Expose-Headers: Content-Length, ETag'); + + // Content-Type + header('Content-Type: application/json; charset=utf-8'); + header('X-Content-Type-Options: nosniff'); + + // ETag pour le cache + if ($useCache) { + $etag = '"' . md5($jsonResponse) . '"'; + header('ETag: ' . $etag); + header('Cache-Control: private, max-age=300'); // 5 minutes + + // Vérifier If-None-Match + $ifNoneMatch = $_SERVER['HTTP_IF_NONE_MATCH'] ?? ''; + if ($ifNoneMatch === $etag) { + http_response_code(304); // Not Modified + exit; + } + } + + // Compression gzip si supportée + $supportsGzip = isset($_SERVER['HTTP_ACCEPT_ENCODING']) + && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false; + + if ($supportsGzip && strlen($jsonResponse) > 1024) { + $compressed = gzencode($jsonResponse, 6); + if ($compressed !== false) { + header('Content-Encoding: gzip'); + header('Content-Length: ' . strlen($compressed)); + http_response_code(200); + echo $compressed; + flush(); + exit; + } + } + + // Réponse non compressée + header('Content-Length: ' . strlen($jsonResponse)); + http_response_code(200); + echo $jsonResponse; + flush(); + exit; + } +} diff --git a/api/src/Controllers/LoginController.php b/api/src/Controllers/LoginController.php index bcf0374e..c9084007 100755 --- a/api/src/Controllers/LoginController.php +++ b/api/src/Controllers/LoginController.php @@ -2086,18 +2086,38 @@ class LoginController { ], 201); } } catch (Exception $e) { - $this->db->rollBack(); - LogService::log('Erreur lors de la création du compte GeoSector', [ + // Vérifier si une transaction est active avant de faire rollback + if ($this->db->inTransaction()) { + $this->db->rollBack(); + } + + // Construire un message d'erreur détaillé pour le logging + $errorDetails = [ 'level' => 'error', - 'error' => $e->getMessage(), - 'email' => $email, - 'amicaleName' => $amicaleName, - 'postalCode' => $postalCode - ]); + 'exception_class' => get_class($e), + 'error_message' => $e->getMessage(), + 'error_code' => $e->getCode(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'email' => $email ?? 'non disponible', + 'amicaleName' => $amicaleName ?? 'non disponible', + 'postalCode' => $postalCode ?? 'non disponible', + 'trace' => $e->getTraceAsString() + ]; + + // Si c'est une PDOException, ajouter les infos SQL + if ($e instanceof PDOException) { + $errorDetails['pdo_error_info'] = $this->db->errorInfo(); + } + + LogService::log('Erreur lors de la création du compte GeoSector', $errorDetails); + + // Retourner un message utilisateur clair (ne pas exposer les détails techniques) + $userMessage = 'Une erreur est survenue lors de la création du compte. Veuillez réessayer ou contacter le support.'; Response::json([ 'status' => 'error', - 'message' => $e->getMessage() + 'message' => $userMessage ], 500); return; } diff --git a/api/src/Controllers/PassageController.php b/api/src/Controllers/PassageController.php index da50e83d..254ae304 100755 --- a/api/src/Controllers/PassageController.php +++ b/api/src/Controllers/PassageController.php @@ -8,6 +8,7 @@ require_once __DIR__ . '/../Services/LogService.php'; require_once __DIR__ . '/../Services/EventLogService.php'; require_once __DIR__ . '/../Services/ApiService.php'; require_once __DIR__ . '/../Services/ReceiptService.php'; +require_once __DIR__ . '/../Services/SectorService.php'; use PDO; use PDOException; @@ -19,6 +20,7 @@ use Session; use App\Services\LogService; use App\Services\EventLogService; use App\Services\ApiService; +use App\Services\SectorService; use Exception; use DateTime; @@ -516,14 +518,26 @@ class PassageController { } // Récupérer ope_users.id pour l'utilisateur du passage - // $data['fk_user'] contient users.id, on doit le convertir en ope_users.id + // $data['fk_user'] peut contenir soit users.id soit ope_users.id $passageUserId = (int)$data['fk_user']; - $stmtOpeUser = $this->db->prepare(' + + // Vérifier d'abord si c'est déjà un ope_users.id valide + $stmtCheckOpeUser = $this->db->prepare(' SELECT id FROM ope_users - WHERE fk_user = ? AND fk_operation = ? + WHERE id = ? AND fk_operation = ? '); - $stmtOpeUser->execute([$passageUserId, $operationId]); - $opeUserId = $stmtOpeUser->fetchColumn(); + $stmtCheckOpeUser->execute([$passageUserId, $operationId]); + $opeUserId = $stmtCheckOpeUser->fetchColumn(); + + if (!$opeUserId) { + // Ce n'est pas un ope_users.id, essayer de le convertir depuis users.id + $stmtOpeUser = $this->db->prepare(' + SELECT id FROM ope_users + WHERE fk_user = ? AND fk_operation = ? + '); + $stmtOpeUser->execute([$passageUserId, $operationId]); + $opeUserId = $stmtOpeUser->fetchColumn(); + } if (!$opeUserId) { Response::json([ @@ -533,6 +547,88 @@ class PassageController { return; } + // Détermination automatique du secteur + $sectorId = null; + $gpsLat = isset($data['gps_lat']) && $data['gps_lat'] !== '' ? (float)$data['gps_lat'] : null; + $gpsLng = isset($data['gps_lng']) && $data['gps_lng'] !== '' ? (float)$data['gps_lng'] : null; + + // 1. Si fk_sector > 0 fourni → l'utiliser directement + if (isset($data['fk_sector']) && (int)$data['fk_sector'] > 0) { + $sectorId = (int)$data['fk_sector']; + LogService::info('[PassageController] Secteur fourni par le client', [ + 'sector_id' => $sectorId + ]); + } + + // 2. Si pas de secteur et GPS valide (différent de 0.0) → recherche par GPS + if ($sectorId === null && $gpsLat !== null && $gpsLng !== null && ($gpsLat != 0.0 || $gpsLng != 0.0)) { + $sectorService = new SectorService(); + $sectorId = $sectorService->findSectorByGps($operationId, $gpsLat, $gpsLng); + LogService::info('[PassageController] Recherche secteur par GPS', [ + 'operation_id' => $operationId, + 'lat' => $gpsLat, + 'lng' => $gpsLng, + 'sector_found' => $sectorId + ]); + } + + // 3. Si toujours pas de secteur et adresse fournie → géocodage + recherche + if ($sectorId === null && !empty($data['numero']) && !empty($data['rue']) && !empty($data['ville'])) { + // Récupérer le code postal de l'entité pour la vérification du département + $stmtEntite = $this->db->prepare(' + SELECT e.code_postal FROM entites e + INNER JOIN operations o ON o.fk_entite = e.id + WHERE o.id = ? + '); + $stmtEntite->execute([$operationId]); + $entiteCp = $stmtEntite->fetchColumn() ?: ''; + + $sectorService = new SectorService(); + $result = $sectorService->findSectorByAddress( + $operationId, + trim($data['numero']), + $data['rue_bis'] ?? '', + trim($data['rue']), + trim($data['ville']), + $entiteCp + ); + + if ($result) { + $sectorId = $result['sector_id']; + // Mettre à jour les coordonnées GPS si le géocodage les a trouvées + if ($result['gps_lat'] && $result['gps_lng']) { + $gpsLat = $result['gps_lat']; + $gpsLng = $result['gps_lng']; + } + LogService::info('[PassageController] Recherche secteur par adresse', [ + 'operation_id' => $operationId, + 'adresse' => $data['numero'] . ' ' . $data['rue'] . ' ' . $data['ville'], + 'sector_found' => $sectorId, + 'gps_geocoded' => ($result['gps_lat'] && $result['gps_lng']) + ]); + } + } + + // 4. Fallback : si toujours pas de secteur, prendre le 1er secteur de l'opération + if ($sectorId === null) { + $stmtFirstSector = $this->db->prepare(' + SELECT id FROM ope_sectors + WHERE fk_operation = ? AND chk_active = 1 + ORDER BY id ASC + LIMIT 1 + '); + $stmtFirstSector->execute([$operationId]); + $firstSectorId = $stmtFirstSector->fetchColumn(); + + if ($firstSectorId) { + $sectorId = (int)$firstSectorId; + LogService::info('[PassageController] Fallback: premier secteur de l\'opération', [ + 'operation_id' => $operationId, + 'sector_id' => $sectorId + ]); + } + } + // Chiffrement des données sensibles $encryptedName = ''; if (isset($data['name']) && !empty(trim($data['name']))) { @@ -549,7 +645,7 @@ class PassageController { // Préparation des données pour l'insertion $insertData = [ 'fk_operation' => $operationId, - 'fk_sector' => isset($data['fk_sector']) ? (int)$data['fk_sector'] : 0, + 'fk_sector' => $sectorId, // Peut être NULL si aucun secteur trouvé 'fk_user' => $opeUserId, 'fk_adresse' => $data['fk_adresse'] ?? '', 'passed_at' => isset($data['passed_at']) ? $data['passed_at'] : null, @@ -562,8 +658,8 @@ class PassageController { 'appt' => $data['appt'] ?? '', 'niveau' => $data['niveau'] ?? '', 'residence' => $data['residence'] ?? '', - 'gps_lat' => $data['gps_lat'] ?? '', - 'gps_lng' => $data['gps_lng'] ?? '', + 'gps_lat' => $gpsLat ?? '', + 'gps_lng' => $gpsLng ?? '', 'encrypted_name' => $encryptedName, 'montant' => isset($data['montant']) ? (float)$data['montant'] : 0.00, 'fk_type_reglement' => isset($data['fk_type_reglement']) ? (int)$data['fk_type_reglement'] : 1, @@ -596,7 +692,7 @@ class PassageController { EventLogService::logPassageCreated( (int)$passageId, $insertData['fk_operation'], - $insertData['fk_sector'], + $insertData['fk_sector'] ?? 0, // 0 si secteur non trouvé $insertData['montant'], (string)$insertData['fk_type_reglement'] ); diff --git a/api/src/Controllers/SectorController.php b/api/src/Controllers/SectorController.php index 0d2711b2..c57238a2 100644 --- a/api/src/Controllers/SectorController.php +++ b/api/src/Controllers/SectorController.php @@ -12,17 +12,15 @@ use App\Services\DepartmentBoundaryService; require_once __DIR__ . '/../Services/EventLogService.php'; require_once __DIR__ . '/../Services/ApiService.php'; -class SectorController +class SectorController { private \PDO $db; - private LogService $logService; private AddressService $addressService; private DepartmentBoundaryService $boundaryService; - + public function __construct() { $this->db = Database::getInstance(); - $this->logService = new LogService(); $this->addressService = new AddressService(); $this->boundaryService = new DepartmentBoundaryService(); } @@ -72,7 +70,7 @@ class SectorController Response::json(['status' => 'success', 'data' => $sectors]); } catch (\Exception $e) { - $this->logService->error('Erreur lors de la récupération des secteurs', [ + LogService::error('Erreur lors de la récupération des secteurs', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); @@ -152,14 +150,14 @@ class SectorController $departmentsTouched = $this->boundaryService->getDepartmentsForSector($coordinates); if (empty($departmentsTouched)) { - $this->logService->warning('Aucun département trouvé pour le secteur', [ + LogService::warning('Aucun département trouvé pour le secteur', [ 'libelle' => $data['libelle'], 'entity_id' => $entityId, 'entity_dept' => $departement ]); } } catch (\Exception $e) { - $this->logService->warning('Impossible de vérifier les limites départementales', [ + LogService::warning('Impossible de vérifier les limites départementales', [ 'error' => $e->getMessage(), 'libelle' => $data['libelle'] ]); @@ -169,7 +167,7 @@ class SectorController try { $addressCount = $this->addressService->countAddressesInPolygon($coordinates, $entityId); } catch (\Exception $e) { - $this->logService->warning('Impossible de récupérer les adresses du secteur', [ + LogService::warning('Impossible de récupérer les adresses du secteur', [ 'error' => $e->getMessage(), 'libelle' => $data['libelle'], 'entity_id' => $entityId @@ -208,7 +206,7 @@ class SectorController $opeUserId = $stmtOpeUser->fetchColumn(); if (!$opeUserId) { - $this->logService->warning('ope_users.id non trouvé pour cette opération', [ + LogService::warning('ope_users.id non trouvé pour cette opération', [ 'ope_users_id' => $memberId, 'operation_id' => $operationId ]); @@ -275,7 +273,7 @@ class SectorController } } catch (\Exception $e) { - $this->logService->warning('Erreur lors de la récupération des passages orphelins', [ + LogService::warning('Erreur lors de la récupération des passages orphelins', [ 'sector_id' => $sectorId, 'error' => $e->getMessage() ]); @@ -335,7 +333,7 @@ class SectorController $firstOpeUserId = $stmtFirstOpeUser->fetchColumn(); if (!$firstOpeUserId) { - $this->logService->warning('Premier ope_users.id non trouvé pour cette opération', [ + LogService::warning('Premier ope_users.id non trouvé pour cette opération', [ 'ope_users_id' => $users[0], 'operation_id' => $operationId ]); @@ -401,7 +399,7 @@ class SectorController // Log pour vérifier l'uniformisation GPS (surtout pour immeubles) if ($fkHabitat == 2 && $nbLog > 1) { - $this->logService->info('[SectorController] Création passages immeuble avec GPS uniformisés', [ + LogService::info('[SectorController] Création passages immeuble avec GPS uniformisés', [ 'address_id' => $address['id'], 'nb_passages' => $nbLog, 'gps_lat' => $gpsLat, @@ -410,7 +408,7 @@ class SectorController ]); } } catch (\Exception $e) { - $this->logService->warning('Erreur lors de la création d\'un passage', [ + LogService::warning('Erreur lors de la création d\'un passage', [ 'address_id' => $address['id'], 'error' => $e->getMessage() ]); @@ -421,7 +419,7 @@ class SectorController } } catch (\Exception $e) { // En cas d'erreur avec les adresses, on ne bloque pas la création du secteur - $this->logService->error('Erreur lors du stockage des adresses du secteur', [ + LogService::error('Erreur lors du stockage des adresses du secteur', [ 'sector_id' => $sectorId, 'error' => $e->getMessage(), 'entity_id' => $entityId @@ -525,7 +523,7 @@ class SectorController $responseData['users_sectors'][] = $userData; } - $this->logService->info('Secteur créé', [ + LogService::info('Secteur créé', [ 'sector_id' => $sectorId, 'libelle' => $sectorData['libelle'], 'entity_id' => $entityId, @@ -567,7 +565,7 @@ class SectorController if ($this->db->inTransaction()) { $this->db->rollBack(); } - $this->logService->error('Erreur lors de la création du secteur', [ + LogService::error('Erreur lors de la création du secteur', [ 'error' => $e->getMessage(), 'data' => $data ?? null ]); @@ -634,7 +632,7 @@ class SectorController // Gestion des membres (reçus comme 'users' depuis Flutter) if (isset($data['users'])) { - $this->logService->info('[UPDATE USERS] Début modification des membres', [ + LogService::info('[UPDATE USERS] Début modification des membres', [ 'sector_id' => $id, 'users_demandes' => $data['users'], 'nb_users' => count($data['users']) @@ -642,27 +640,27 @@ class SectorController // Récupérer l'opération du secteur pour l'INSERT $opQuery = "SELECT fk_operation FROM ope_sectors WHERE id = :sector_id"; - $this->logService->info('[UPDATE USERS] SQL - Récupération fk_operation', [ + LogService::info('[UPDATE USERS] SQL - Récupération fk_operation', [ 'query' => $opQuery, 'params' => ['sector_id' => $id] ]); $opStmt = $this->db->prepare($opQuery); $opStmt->execute(['sector_id' => $id]); $operationId = $opStmt->fetch()['fk_operation']; - $this->logService->info('[UPDATE USERS] fk_operation récupéré', [ + LogService::info('[UPDATE USERS] fk_operation récupéré', [ 'operation_id' => $operationId ]); // Supprimer les affectations existantes $deleteQuery = "DELETE FROM ope_users_sectors WHERE fk_sector = :sector_id"; - $this->logService->info('[UPDATE USERS] SQL - Suppression des anciens membres', [ + LogService::info('[UPDATE USERS] SQL - Suppression des anciens membres', [ 'query' => $deleteQuery, 'params' => ['sector_id' => $id] ]); $deleteStmt = $this->db->prepare($deleteQuery); $deleteStmt->execute(['sector_id' => $id]); $deletedCount = $deleteStmt->rowCount(); - $this->logService->info('[UPDATE USERS] Membres supprimés', [ + LogService::info('[UPDATE USERS] Membres supprimés', [ 'nb_deleted' => $deletedCount ]); @@ -670,7 +668,7 @@ class SectorController if (!empty($data['users'])) { $insertQuery = "INSERT INTO ope_users_sectors (fk_operation, fk_user, fk_sector, created_at, updated_at, fk_user_creat, chk_active) VALUES (:operation_id, :user_id, :sector_id, NOW(), NOW(), :user_creat, 1)"; - $this->logService->info('[UPDATE USERS] SQL - Requête INSERT préparée', [ + LogService::info('[UPDATE USERS] SQL - Requête INSERT préparée', [ 'query' => $insertQuery ]); $insertStmt = $this->db->prepare($insertQuery); @@ -689,7 +687,7 @@ class SectorController $opeUserId = $stmtOpeUser->fetchColumn(); if (!$opeUserId) { - $this->logService->warning('[UPDATE USERS] ope_users.id non trouvé pour cette opération', [ + LogService::warning('[UPDATE USERS] ope_users.id non trouvé pour cette opération', [ 'ope_users_id' => $memberId, 'operation_id' => $operationId ]); @@ -703,17 +701,17 @@ class SectorController 'sector_id' => $id, 'user_creat' => $_SESSION['user_id'] ?? null ]; - $this->logService->info('[UPDATE USERS] SQL - INSERT user', [ + LogService::info('[UPDATE USERS] SQL - INSERT user', [ 'params' => $params ]); $insertStmt->execute($params); $insertedUsers[] = $memberId; - $this->logService->info('[UPDATE USERS] User inséré avec succès', [ + LogService::info('[UPDATE USERS] User inséré avec succès', [ 'user_id' => $memberId ]); } catch (\PDOException $e) { $failedUsers[] = $memberId; - $this->logService->warning('[UPDATE USERS] ERREUR insertion user', [ + LogService::warning('[UPDATE USERS] ERREUR insertion user', [ 'sector_id' => $id, 'user_id' => $memberId, 'error' => $e->getMessage(), @@ -722,7 +720,7 @@ class SectorController } } - $this->logService->info('[UPDATE USERS] Résultat des insertions', [ + LogService::info('[UPDATE USERS] Résultat des insertions', [ 'users_demandes' => $data['users'], 'users_inseres' => $insertedUsers, 'users_echoues' => $failedUsers, @@ -744,7 +742,7 @@ class SectorController $chkAdressesChange = $data['chk_adresses_change'] ?? 1; if (isset($data['sector']) && $chkAdressesChange == 0) { - $this->logService->info('[UPDATE] Modification secteur sans recalcul adresses/passages', [ + LogService::info('[UPDATE] Modification secteur sans recalcul adresses/passages', [ 'sector_id' => $id, 'chk_adresses_change' => $chkAdressesChange ]); @@ -770,7 +768,7 @@ class SectorController } // Récupérer et stocker les nouvelles adresses - $this->logService->info('[UPDATE] Récupération des adresses', [ + LogService::info('[UPDATE] Récupération des adresses', [ 'sector_id' => $id, 'entity_id' => $entityId, 'nb_points' => count($coordinates) @@ -781,7 +779,7 @@ class SectorController // Enrichir les adresses avec les données bâtiments $addresses = $this->addressService->enrichAddressesWithBuildings($addresses, $entityId); - $this->logService->info('[UPDATE] Adresses récupérées', [ + LogService::info('[UPDATE] Adresses récupérées', [ 'sector_id' => $id, 'nb_addresses' => count($addresses) ]); @@ -815,12 +813,12 @@ class SectorController ]); } - $this->logService->info('[UPDATE] Adresses stockées dans sectors_adresses', [ + LogService::info('[UPDATE] Adresses stockées dans sectors_adresses', [ 'sector_id' => $id, 'nb_stored' => count($addresses) ]); } else { - $this->logService->warning('[UPDATE] Aucune adresse trouvée pour le secteur', [ + LogService::warning('[UPDATE] Aucune adresse trouvée pour le secteur', [ 'sector_id' => $id, 'entity_id' => $entityId ]); @@ -828,19 +826,19 @@ class SectorController // Vérifier si c'est un problème de connexion à la base d'adresses if (!$this->addressService->isConnected()) { - $this->logService->warning('[UPDATE] Base d\'adresses non accessible - passages créés sans adresses', [ + LogService::warning('[UPDATE] Base d\'adresses non accessible - passages créés sans adresses', [ 'sector_id' => $id ]); } } catch (\Exception $e) { - $this->logService->error('[UPDATE] Erreur lors de la mise à jour des adresses du secteur', [ + LogService::error('[UPDATE] Erreur lors de la mise à jour des adresses du secteur', [ 'sector_id' => $id, 'error' => $e->getMessage() ]); } // Maintenant que les adresses sont mises à jour, traiter les passages - $this->logService->info('[UPDATE] Début mise à jour des passages', ['sector_id' => $id]); + LogService::info('[UPDATE] Début mise à jour des passages', ['sector_id' => $id]); $passageCounters = $this->updatePassagesForSector($id, $data['sector']); } @@ -934,7 +932,7 @@ class SectorController WHERE ous.fk_sector = :sector_id ORDER BY u.id"; - $this->logService->info('[UPDATE USERS] SQL - Récupération finale des users', [ + LogService::info('[UPDATE USERS] SQL - Récupération finale des users', [ 'query' => $usersQuery, 'params' => ['sector_id' => $id] ]); @@ -944,7 +942,7 @@ class SectorController $usersSectors = $usersStmt->fetchAll(\PDO::FETCH_ASSOC); $userIds = array_column($usersSectors, 'id'); - $this->logService->info('[UPDATE USERS] Users récupérés après commit', [ + LogService::info('[UPDATE USERS] Users récupérés après commit', [ 'sector_id' => $id, 'users_ids' => $userIds, 'nb_users' => count($userIds), @@ -971,7 +969,7 @@ class SectorController $usersDecrypted[] = $userData; } - $this->logService->info('Secteur modifié', [ + LogService::info('Secteur modifié', [ 'sector_id' => $id, 'updates' => array_keys($data), 'passage_counters' => $passageCounters, @@ -999,7 +997,7 @@ class SectorController if ($this->db->inTransaction()) { $this->db->rollBack(); } - $this->logService->error('Erreur lors de la modification du secteur', [ + LogService::error('Erreur lors de la modification du secteur', [ 'sector_id' => $id, 'error' => $e->getMessage() ]); @@ -1065,7 +1063,7 @@ class SectorController ]); } catch (\Exception $e) { - $this->logService->error('Erreur lors de la récupération des adresses du secteur', [ + LogService::error('Erreur lors de la récupération des adresses du secteur', [ 'sector_id' => $id, 'error' => $e->getMessage() ]); @@ -1198,7 +1196,7 @@ class SectorController $passagesDecrypted[] = $passage; } - $this->logService->info('Secteur supprimé', [ + LogService::info('Secteur supprimé', [ 'sector_id' => $id, 'libelle' => $sector['libelle'], 'passages_deleted' => $passagesToDelete, @@ -1216,7 +1214,7 @@ class SectorController } catch (\Exception $e) { $this->db->rollBack(); - $this->logService->error('Erreur lors de la suppression du secteur', [ + LogService::error('Erreur lors de la suppression du secteur', [ 'sector_id' => $id, 'error' => $e->getMessage() ]); @@ -1238,7 +1236,7 @@ class SectorController ]); } catch (\Exception $e) { - $this->logService->error('Erreur lors de la vérification des contours départementaux', [ + LogService::error('Erreur lors de la vérification des contours départementaux', [ 'error' => $e->getMessage() ]); Response::json(['status' => 'error', 'message' => 'Erreur lors de la vérification'], 500); @@ -1298,7 +1296,7 @@ class SectorController ]); } catch (\Exception $e) { - $this->logService->error('Erreur lors de la vérification des limites', [ + LogService::error('Erreur lors de la vérification des limites', [ 'error' => $e->getMessage() ]); Response::json(['status' => 'error', 'message' => 'Erreur lors de la vérification'], 500); @@ -1422,7 +1420,7 @@ class SectorController $addressesStmt->execute(['sector_id' => $sectorId]); $addresses = $addressesStmt->fetchAll(); - $this->logService->info('[updatePassagesForSector] Adresses dans sectors_adresses', [ + LogService::info('[updatePassagesForSector] Adresses dans sectors_adresses', [ 'sector_id' => $sectorId, 'nb_addresses' => count($addresses) ]); @@ -1435,7 +1433,7 @@ class SectorController $firstUserId = $firstUser ? $firstUser['fk_user'] : null; if ($firstUserId && !empty($addresses)) { - $this->logService->info('[updatePassagesForSector] Traitement des passages', [ + LogService::info('[updatePassagesForSector] Traitement des passages', [ 'user_id' => $firstUserId, 'nb_addresses' => count($addresses) ]); @@ -1594,7 +1592,7 @@ class SectorController $insertStmt->execute($insertParams); $counters['passages_created'] = count($toInsert); } catch (\Exception $e) { - $this->logService->error('Erreur lors de l\'insertion multiple des passages', [ + LogService::error('Erreur lors de l\'insertion multiple des passages', [ 'sector_id' => $sectorId, 'error' => $e->getMessage() ]); @@ -1658,12 +1656,12 @@ class SectorController $counters['passages_updated'] = count($toUpdate); // Log pour vérifier l'uniformisation GPS (surtout pour immeubles) - $this->logService->info('[updatePassagesForSector] Passages mis à jour avec GPS uniformisés', [ + LogService::info('[updatePassagesForSector] Passages mis à jour avec GPS uniformisés', [ 'nb_updated' => count($toUpdate), 'sector_id' => $sectorId ]); } catch (\Exception $e) { - $this->logService->error('Erreur lors de la mise à jour multiple des passages', [ + LogService::error('Erreur lors de la mise à jour multiple des passages', [ 'sector_id' => $sectorId, 'error' => $e->getMessage() ]); @@ -1680,7 +1678,7 @@ class SectorController $deleteStmt->execute($toDelete); $counters['passages_deleted'] += count($toDelete); } catch (\Exception $e) { - $this->logService->error('Erreur lors de la suppression multiple des passages', [ + LogService::error('Erreur lors de la suppression multiple des passages', [ 'sector_id' => $sectorId, 'error' => $e->getMessage() ]); @@ -1688,7 +1686,7 @@ class SectorController } } else { - $this->logService->warning('[updatePassagesForSector] Pas de création de passages', [ + LogService::warning('[updatePassagesForSector] Pas de création de passages', [ 'reason' => !$firstUserId ? 'Pas d\'utilisateur affecté' : 'Pas d\'adresses', 'first_user_id' => $firstUserId, 'nb_addresses' => count($addresses) @@ -1697,14 +1695,14 @@ class SectorController // Retourner les compteurs détaillés - $this->logService->info('[updatePassagesForSector] Fin traitement', [ + LogService::info('[updatePassagesForSector] Fin traitement', [ 'sector_id' => $sectorId, 'counters' => $counters ]); return $counters; } catch (\Exception $e) { - $this->logService->error('Erreur lors de la mise à jour des passages', [ + LogService::error('Erreur lors de la mise à jour des passages', [ 'sector_id' => $sectorId, 'error' => $e->getMessage() ]); diff --git a/api/src/Controllers/StripeController.php b/api/src/Controllers/StripeController.php index c2a1a8c8..af418450 100644 --- a/api/src/Controllers/StripeController.php +++ b/api/src/Controllers/StripeController.php @@ -196,7 +196,8 @@ class StripeController extends Controller { SELECT p.*, o.fk_entite, o.id as operation_id FROM ope_pass p JOIN operations o ON p.fk_operation = o.id - WHERE p.id = ? AND p.fk_user = ? + JOIN ope_users ou ON p.fk_user = ou.id + WHERE p.id = ? AND ou.fk_user = ? '); $stmt->execute([$passageId, Session::getUserId()]); $passage = $stmt->fetch(); @@ -468,71 +469,7 @@ class StripeController extends Controller { $this->sendError('Erreur: ' . $e->getMessage()); } } - - /** - * POST /api/stripe/devices/check-tap-to-pay - * Vérifier la compatibilité Tap to Pay d'un appareil - */ - public function checkTapToPayCapability(): void { - try { - $data = $this->getJsonInput(); - - $platform = $data['platform'] ?? ''; - - if ($platform === 'ios') { - // Pour iOS, on vérifie côté client (iPhone XS+ avec iOS 16.4+) - $this->sendSuccess([ - 'message' => 'Vérification iOS à faire côté client', - 'requirements' => 'iPhone XS ou plus récent avec iOS 16.4+', - 'details' => 'iOS 16.4 minimum requis pour le support PIN complet' - ]); - return; - } - - if ($platform === 'android') { - $manufacturer = $data['manufacturer'] ?? ''; - $model = $data['model'] ?? ''; - - if (!$manufacturer || !$model) { - $this->sendError('Manufacturer et model requis pour Android', 400); - return; - } - - $result = $this->stripeService->checkAndroidTapToPayCompatibility($manufacturer, $model); - - if ($result['success']) { - $this->sendSuccess($result); - } else { - $this->sendError($result['message'], 400); - } - } else { - $this->sendError('Platform doit être ios ou android', 400); - } - - } catch (Exception $e) { - $this->sendError('Erreur: ' . $e->getMessage()); - } - } - - /** - * GET /api/stripe/devices/certified-android - * Récupérer la liste des appareils Android certifiés - */ - public function getCertifiedAndroidDevices(): void { - try { - $result = $this->stripeService->getCertifiedAndroidDevices(); - - if ($result['success']) { - $this->sendSuccess(['devices' => $result['devices']]); - } else { - $this->sendError($result['message'], 400); - } - - } catch (Exception $e) { - $this->sendError('Erreur: ' . $e->getMessage()); - } - } - + /** * GET /api/stripe/config * Récupérer la configuration publique Stripe @@ -784,4 +721,117 @@ class StripeController extends Controller { $this->sendError('Erreur lors de la création de la location: ' . $e->getMessage()); } } + + /** + * POST /api/stripe/terminal/connection-token + * Créer un Connection Token pour Stripe Terminal/Tap to Pay + * Requis par le SDK Stripe Terminal pour se connecter aux readers + */ + public function createConnectionToken(): void { + try { + $this->requireAuth(); + + $data = $this->getJsonInput(); + $entiteId = $data['amicale_id'] ?? Session::getEntityId(); + + if (!$entiteId) { + $this->sendError('ID entité requis', 400); + return; + } + + // Vérifier les droits sur cette entité + $userRole = Session::getRole() ?? 0; + if (Session::getEntityId() != $entiteId && $userRole < 3) { + $this->sendError('Non autorisé pour cette entité', 403); + return; + } + + $result = $this->stripeService->createConnectionToken($entiteId); + + if ($result['success']) { + $this->sendSuccess([ + 'secret' => $result['secret'] + ]); + } else { + $this->sendError($result['message'], 400); + } + + } catch (Exception $e) { + $this->sendError('Erreur lors de la création du connection token: ' . $e->getMessage()); + } + } + + /** + * POST /api/stripe/payments/cancel + * Annuler un PaymentIntent Stripe + * + * Payload: + * { + * "payment_intent_id": "pi_3SWvho2378xpV4Rn1J43Ks7M" + * } + */ + public function cancelPayment(): void { + try { + $this->requireAuth(); + + $data = $this->getJsonInput(); + + // Validation + if (!isset($data['payment_intent_id'])) { + $this->sendError('payment_intent_id requis', 400); + return; + } + + $paymentIntentId = $data['payment_intent_id']; + + // Vérifier que le passage existe et appartient à l'utilisateur + $stmt = $this->db->prepare(' + SELECT p.*, o.fk_entite, ou.fk_user as ope_user_id + FROM ope_pass p + JOIN operations o ON p.fk_operation = o.id + JOIN ope_users ou ON p.fk_user = ou.id + WHERE p.stripe_payment_id = ? + '); + $stmt->execute([$paymentIntentId]); + $passage = $stmt->fetch(); + + if (!$passage) { + $this->sendError('Paiement non trouvé', 404); + return; + } + + // Vérifier les droits + $userId = Session::getUserId(); + $userEntityId = Session::getEntityId(); + + if ($passage['ope_user_id'] != $userId && $passage['fk_entite'] != $userEntityId) { + $this->sendError('Non autorisé', 403); + return; + } + + // Annuler le PaymentIntent via StripeService + $result = $this->stripeService->cancelPaymentIntent($paymentIntentId); + + if ($result['success']) { + // Retirer le stripe_payment_id du passage + $stmt = $this->db->prepare(' + UPDATE ope_pass + SET stripe_payment_id = NULL, updated_at = NOW() + WHERE id = ? + '); + $stmt->execute([$passage['id']]); + + $this->sendSuccess([ + 'status' => 'canceled', + 'payment_intent_id' => $paymentIntentId, + 'passage_id' => $passage['id'] + ]); + } else { + $this->sendError($result['message'], 400); + } + + } catch (Exception $e) { + $this->sendError('Erreur: ' . $e->getMessage()); + } + } } \ No newline at end of file diff --git a/api/src/Controllers/StripeWebhookController.php b/api/src/Controllers/StripeWebhookController.php index 0e977bbe..de626d72 100644 --- a/api/src/Controllers/StripeWebhookController.php +++ b/api/src/Controllers/StripeWebhookController.php @@ -202,85 +202,46 @@ class StripeWebhookController extends Controller { * Gérer un paiement réussi */ private function handlePaymentIntentSucceeded($paymentIntent): void { - // Mettre à jour le statut en base - $stmt = $this->db->prepare( - "UPDATE stripe_payment_intents - SET status = :status, updated_at = NOW() - WHERE stripe_payment_intent_id = :pi_id" - ); - $stmt->execute([ - 'status' => 'succeeded', - 'pi_id' => $paymentIntent->id - ]); - - // Enregistrer dans l'historique - $stmt = $this->db->prepare( - "SELECT id FROM stripe_payment_intents WHERE stripe_payment_intent_id = :pi_id" - ); + // Pour Tap to Pay, le paiement est instantané et déjà enregistré dans ope_pass + // Ce webhook sert principalement pour les Payment Links (QR Code) asynchrones + + // Vérifier si le passage existe et mettre à jour si nécessaire + $stmt = $this->db->prepare(" + SELECT id FROM ope_pass + WHERE stripe_payment_id = :pi_id + "); $stmt->execute(['pi_id' => $paymentIntent->id]); - $localPayment = $stmt->fetch(); - - if ($localPayment) { - $stmt = $this->db->prepare( - "INSERT INTO stripe_payment_history - (fk_payment_intent, event_type, event_data, created_at) - VALUES (:fk_pi, 'succeeded', :data, NOW())" - ); - $stmt->execute([ - 'fk_pi' => $localPayment['id'], - 'data' => json_encode([ - 'amount' => $paymentIntent->amount, - 'currency' => $paymentIntent->currency, - 'payment_method' => $paymentIntent->payment_method, - 'charges' => $paymentIntent->charges->data - ]) - ]); + $passage = $stmt->fetch(); + + if ($passage) { + error_log("Payment succeeded: {$paymentIntent->id} for {$paymentIntent->amount} {$paymentIntent->currency} - Passage {$passage['id']}"); + + // TODO: Envoyer un reçu par email + // TODO: Mettre à jour les statistiques en temps réel + } else { + error_log("Payment succeeded: {$paymentIntent->id} but no passage found in ope_pass"); } - - // TODO: Envoyer un reçu par email - // TODO: Mettre à jour les statistiques en temps réel - - error_log("Payment succeeded: {$paymentIntent->id} for {$paymentIntent->amount} {$paymentIntent->currency}"); } /** * Gérer un paiement échoué */ private function handlePaymentIntentFailed($paymentIntent): void { - // Mettre à jour le statut - $stmt = $this->db->prepare( - "UPDATE stripe_payment_intents - SET status = :status, updated_at = NOW() - WHERE stripe_payment_intent_id = :pi_id" - ); - $stmt->execute([ - 'status' => 'failed', - 'pi_id' => $paymentIntent->id - ]); - - // Enregistrer dans l'historique avec la raison de l'échec - $stmt = $this->db->prepare( - "SELECT id FROM stripe_payment_intents WHERE stripe_payment_intent_id = :pi_id" - ); + // Vérifier si le passage existe + $stmt = $this->db->prepare(" + SELECT id FROM ope_pass + WHERE stripe_payment_id = :pi_id + "); $stmt->execute(['pi_id' => $paymentIntent->id]); - $localPayment = $stmt->fetch(); - - if ($localPayment) { - $stmt = $this->db->prepare( - "INSERT INTO stripe_payment_history - (fk_payment_intent, event_type, event_data, created_at) - VALUES (:fk_pi, 'failed', :data, NOW())" - ); - $stmt->execute([ - 'fk_pi' => $localPayment['id'], - 'data' => json_encode([ - 'error' => $paymentIntent->last_payment_error, - 'cancellation_reason' => $paymentIntent->cancellation_reason - ]) - ]); + $passage = $stmt->fetch(); + + if ($passage) { + // Optionnel : Marquer le passage comme échec ou supprimer le stripe_payment_id + // Pour l'instant on log seulement + error_log("Payment failed: {$paymentIntent->id} for passage {$passage['id']}, reason: " . json_encode($paymentIntent->last_payment_error)); + } else { + error_log("Payment failed: {$paymentIntent->id}, reason: " . json_encode($paymentIntent->last_payment_error)); } - - error_log("Payment failed: {$paymentIntent->id}, reason: " . json_encode($paymentIntent->last_payment_error)); } /** @@ -358,17 +319,8 @@ class StripeWebhookController extends Controller { * Gérer une action réussie sur un Terminal reader */ private function handleTerminalReaderActionSucceeded($reader): void { - // Mettre à jour le statut du reader - $stmt = $this->db->prepare( - "UPDATE stripe_terminal_readers - SET status = :status, last_seen_at = NOW() - WHERE stripe_reader_id = :reader_id" - ); - $stmt->execute([ - 'status' => 'online', - 'reader_id' => $reader->id - ]); - + // Note: Pour Tap to Pay, il n'y a pas de readers physiques + // Ce webhook concerne uniquement les terminaux externes (non utilisés pour l'instant) error_log("Terminal reader action succeeded: {$reader->id}"); } @@ -376,17 +328,8 @@ class StripeWebhookController extends Controller { * Gérer une action échouée sur un Terminal reader */ private function handleTerminalReaderActionFailed($reader): void { - // Mettre à jour le statut du reader - $stmt = $this->db->prepare( - "UPDATE stripe_terminal_readers - SET status = :status, last_seen_at = NOW() - WHERE stripe_reader_id = :reader_id" - ); - $stmt->execute([ - 'status' => 'error', - 'reader_id' => $reader->id - ]); - + // Note: Pour Tap to Pay, il n'y a pas de readers physiques + // Ce webhook concerne uniquement les terminaux externes (non utilisés pour l'instant) error_log("Terminal reader action failed: {$reader->id}"); } } \ No newline at end of file diff --git a/api/src/Core/Router.php b/api/src/Core/Router.php index c8984ca3..1d99a5b0 100755 --- a/api/src/Core/Router.php +++ b/api/src/Core/Router.php @@ -135,13 +135,13 @@ class Router { $this->get('stripe/accounts/:entityId/status', ['StripeController', 'getAccountStatus']); $this->post('stripe/accounts/:accountId/onboarding-link', ['StripeController', 'createOnboardingLink']); - // Tap to Pay - Vérification compatibilité et configuration - $this->post('stripe/devices/check-tap-to-pay', ['StripeController', 'checkTapToPayCapability']); - $this->get('stripe/devices/certified-android', ['StripeController', 'getCertifiedAndroidDevices']); + // Tap to Pay - Configuration $this->post('stripe/locations', ['StripeController', 'createLocation']); + $this->post('stripe/terminal/connection-token', ['StripeController', 'createConnectionToken']); // Paiements $this->post('stripe/payments/create-intent', ['StripeController', 'createPaymentIntent']); + $this->post('stripe/payments/cancel', ['StripeController', 'cancelPayment']); $this->post('stripe/payment-links', ['StripeController', 'createPaymentLink']); $this->get('stripe/payments/:paymentIntentId', ['StripeController', 'getPaymentStatus']); @@ -152,6 +152,14 @@ class Router { // Webhook (IMPORTANT: pas d'authentification requise pour les webhooks Stripe) $this->post('stripe/webhooks', ['StripeWebhookController', 'handleWebhook']); + // Routes Statistiques Events (Admin uniquement) + $this->get('events/stats/summary', ['EventStatsController', 'summary']); + $this->get('events/stats/daily', ['EventStatsController', 'daily']); + $this->get('events/stats/weekly', ['EventStatsController', 'weekly']); + $this->get('events/stats/monthly', ['EventStatsController', 'monthly']); + $this->get('events/stats/details', ['EventStatsController', 'details']); + $this->get('events/stats/types', ['EventStatsController', 'types']); + // Routes Migration (Admin uniquement) $this->get('migrations/test-connections', ['MigrationController', 'testConnections']); $this->get('migrations/entities/available', ['MigrationController', 'getAvailableEntities']); diff --git a/api/src/Services/AddressService.php b/api/src/Services/AddressService.php index 535be5c5..349f72f9 100644 --- a/api/src/Services/AddressService.php +++ b/api/src/Services/AddressService.php @@ -21,19 +21,16 @@ class AddressService { private ?PDO $addressesDb = null; private PDO $mainDb; - private $logService; private $buildingService; public function __construct() { - $this->logService = new LogService(); - try { $this->addressesDb = \AddressesDatabase::getInstance(); - $this->logService->info('[AddressService] Connexion à la base d\'adresses réussie'); + LogService::info('[AddressService] Connexion à la base d\'adresses réussie'); } catch (\Exception $e) { // Si la connexion échoue, on continue sans la base d'adresses - $this->logService->error('[AddressService] Connexion à la base d\'adresses impossible', [ + LogService::error('[AddressService] Connexion à la base d\'adresses impossible', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); @@ -94,13 +91,13 @@ class AddressService { // Si pas de connexion à la base d'adresses, retourner un tableau vide if (!$this->addressesDb) { - $this->logService->error('[AddressService] Pas de connexion à la base d\'adresses externe', [ + LogService::error('[AddressService] Pas de connexion à la base d\'adresses externe', [ 'entity_id' => $entityId ]); return []; } - $this->logService->info('[AddressService] Début recherche adresses', [ + LogService::info('[AddressService] Début recherche adresses', [ 'entity_id' => $entityId, 'nb_coordinates' => count($coordinates) ]); @@ -117,11 +114,11 @@ class AddressService // Si aucun département n'est trouvé par analyse spatiale, // chercher d'abord dans le département de l'entité et ses limitrophes $entityDept = $this->getDepartmentForEntity($entityId); - $this->logService->info('[AddressService] Département de l\'entité', [ + LogService::info('[AddressService] Département de l\'entité', [ 'departement' => $entityDept ]); if (!$entityDept) { - $this->logService->error('[AddressService] Impossible de déterminer le département de l\'entité', [ + LogService::error('[AddressService] Impossible de déterminer le département de l\'entité', [ 'entity_id' => $entityId ]); throw new RuntimeException("Impossible de déterminer le département"); @@ -131,7 +128,7 @@ class AddressService $priorityDepts = $boundaryService->getPriorityDepartments($entityDept); // Log pour debug - $this->logService->warning('[AddressService] Aucun département trouvé par analyse spatiale', [ + LogService::warning('[AddressService] Aucun département trouvé par analyse spatiale', [ 'departements_prioritaires' => implode(', ', $priorityDepts) ]); @@ -204,7 +201,7 @@ class AddressService } // Log pour debug - $this->logService->info('[AddressService] Recherche dans table', [ + LogService::info('[AddressService] Recherche dans table', [ 'table' => $tableName, 'departement' => $deptCode, 'nb_adresses' => count($addresses) @@ -212,7 +209,7 @@ class AddressService } catch (PDOException $e) { // Log l'erreur mais continue avec les autres départements - $this->logService->error('[AddressService] Erreur SQL', [ + LogService::error('[AddressService] Erreur SQL', [ 'table' => $tableName, 'departement' => $deptCode, 'error' => $e->getMessage(), @@ -221,7 +218,7 @@ class AddressService } } - $this->logService->info('[AddressService] Fin recherche adresses', [ + LogService::info('[AddressService] Fin recherche adresses', [ 'total_adresses' => count($allAddresses) ]); return $allAddresses; @@ -243,7 +240,7 @@ class AddressService return []; } - $this->logService->info('[AddressService] Début enrichissement avec bâtiments', [ + LogService::info('[AddressService] Début enrichissement avec bâtiments', [ 'entity_id' => $entityId, 'nb_addresses' => count($addresses) ]); @@ -262,7 +259,7 @@ class AddressService } } - $this->logService->info('[AddressService] Fin enrichissement avec bâtiments', [ + LogService::info('[AddressService] Fin enrichissement avec bâtiments', [ 'total_adresses' => count($enrichedAddresses), 'nb_immeubles' => $nbImmeubles, 'nb_maisons' => $nbMaisons @@ -271,7 +268,7 @@ class AddressService return $enrichedAddresses; } catch (\Exception $e) { - $this->logService->error('[AddressService] Erreur lors de l\'enrichissement', [ + LogService::error('[AddressService] Erreur lors de l\'enrichissement', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); diff --git a/api/src/Services/ApiService.php b/api/src/Services/ApiService.php index 03ebdf33..4916015a 100755 --- a/api/src/Services/ApiService.php +++ b/api/src/Services/ApiService.php @@ -231,7 +231,7 @@ class ApiService { * @param int $minLength Longueur minimale du nom d'utilisateur (par défaut 10) * @return string Nom d'utilisateur généré */ - public static function generateUserName(PDO $db, string $name, string $postalCode, string $cityName, int $minLength = 10): string { + public static function generateUserName(\PDO $db, string $name, string $postalCode, string $cityName, int $minLength = 10): string { // Nettoyer et préparer les chaînes $name = preg_replace('/[^a-zA-Z0-9]/', '', strtolower($name)); $postalCode = preg_replace('/[^0-9]/', '', $postalCode); @@ -277,7 +277,7 @@ class ApiService { // Vérifier si le nom d'utilisateur existe déjà $stmt = $db->prepare('SELECT COUNT(*) as count FROM users WHERE encrypted_user_name = ?'); $stmt->execute([$encryptedUsername]); - $result = $stmt->fetch(PDO::FETCH_ASSOC); + $result = $stmt->fetch(\PDO::FETCH_ASSOC); if ($result && $result['count'] == 0) { $isUnique = true; diff --git a/api/src/Services/EmailTemplates.php b/api/src/Services/EmailTemplates.php index 59f00716..636d9c05 100755 --- a/api/src/Services/EmailTemplates.php +++ b/api/src/Services/EmailTemplates.php @@ -14,9 +14,9 @@ class EmailTemplates { Votre compte a été créé avec succès sur GeoSector.

Identifiant : $username
Mot de passe : $password

- Vous pouvez vous connecter dès maintenant sur app3.geosector.fr

À très bientôt,
- L'équipe GeoSector"; + L'équipe GeoSector
+ Support : support@geosector.fr"; } /** @@ -80,9 +80,9 @@ class EmailTemplates { Bonjour $name,

Vous avez demandé la réinitialisation de votre mot de passe sur GeoSector.

Nouveau mot de passe : $password

- Vous pouvez vous connecter avec ce nouveau mot de passe sur app3.geosector.fr

À très bientôt,
- L'équipe GeoSector"; + L'équipe GeoSector
+ Support : support@geosector.fr"; } /** diff --git a/api/src/Services/EventLogService.php b/api/src/Services/EventLogService.php index fdce06a6..8a432a5b 100644 --- a/api/src/Services/EventLogService.php +++ b/api/src/Services/EventLogService.php @@ -305,6 +305,141 @@ class EventLogService ]); } + // ==================== MÉTHODES STRIPE ==================== + + /** + * Log la création d'un PaymentIntent + * + * @param string $paymentIntentId ID Stripe du PaymentIntent + * @param int $passageId ID du passage + * @param int $amount Montant en centimes + * @param string $method Méthode (tap_to_pay, qr_code, web) + */ + public static function logStripePaymentCreated( + string $paymentIntentId, + int $passageId, + int $amount, + string $method + ): void { + $entityId = Session::getEntityId(); + self::writeEvent('stripe_payment_created', [ + 'payment_intent_id' => $paymentIntentId, + 'passage_id' => $passageId, + 'entity_id' => $entityId, + 'amount' => $amount, + 'method' => $method + ]); + } + + /** + * Log un paiement Stripe réussi + * + * @param string $paymentIntentId ID Stripe du PaymentIntent + * @param int $passageId ID du passage + * @param int $amount Montant en centimes + * @param string $method Méthode (tap_to_pay, qr_code, web) + */ + public static function logStripePaymentSuccess( + string $paymentIntentId, + int $passageId, + int $amount, + string $method + ): void { + $entityId = Session::getEntityId(); + self::writeEvent('stripe_payment_success', [ + 'payment_intent_id' => $paymentIntentId, + 'passage_id' => $passageId, + 'entity_id' => $entityId, + 'amount' => $amount, + 'method' => $method + ]); + } + + /** + * Log un paiement Stripe échoué + * + * @param string|null $paymentIntentId ID Stripe (peut être null si création échouée) + * @param int|null $passageId ID du passage (peut être null) + * @param int|null $amount Montant en centimes (peut être null) + * @param string $method Méthode tentée + * @param string $errorCode Code d'erreur + * @param string $errorMessage Message d'erreur + */ + public static function logStripePaymentFailed( + ?string $paymentIntentId, + ?int $passageId, + ?int $amount, + string $method, + string $errorCode, + string $errorMessage + ): void { + $entityId = Session::getEntityId(); + $data = [ + 'entity_id' => $entityId, + 'method' => $method, + 'error_code' => $errorCode, + 'error_message' => $errorMessage + ]; + + if ($paymentIntentId !== null) { + $data['payment_intent_id'] = $paymentIntentId; + } + if ($passageId !== null) { + $data['passage_id'] = $passageId; + } + if ($amount !== null) { + $data['amount'] = $amount; + } + + self::writeEvent('stripe_payment_failed', $data); + } + + /** + * Log l'annulation d'un paiement Stripe + * + * @param string $paymentIntentId ID Stripe du PaymentIntent + * @param int|null $passageId ID du passage (peut être null) + * @param string $reason Raison (user_cancelled, timeout, error, etc.) + */ + public static function logStripePaymentCancelled( + string $paymentIntentId, + ?int $passageId, + string $reason + ): void { + $entityId = Session::getEntityId(); + $data = [ + 'payment_intent_id' => $paymentIntentId, + 'entity_id' => $entityId, + 'reason' => $reason + ]; + + if ($passageId !== null) { + $data['passage_id'] = $passageId; + } + + self::writeEvent('stripe_payment_cancelled', $data); + } + + /** + * Log une erreur du Terminal Tap to Pay + * + * @param string $errorCode Code d'erreur (cardReadTimedOut, device_not_compatible, etc.) + * @param string $errorMessage Message d'erreur + * @param array $metadata Métadonnées supplémentaires (device_model, is_simulated, etc.) + */ + public static function logStripeTerminalError( + string $errorCode, + string $errorMessage, + array $metadata = [] + ): void { + $entityId = Session::getEntityId(); + self::writeEvent('stripe_terminal_error', array_merge([ + 'entity_id' => $entityId, + 'error_code' => $errorCode, + 'error_message' => $errorMessage + ], $metadata)); + } + // ==================== MÉTHODES OPÉRATIONS ==================== /** diff --git a/api/src/Services/EventStatsService.php b/api/src/Services/EventStatsService.php new file mode 100644 index 00000000..3cb69fa1 --- /dev/null +++ b/api/src/Services/EventStatsService.php @@ -0,0 +1,535 @@ +db = Database::getInstance(); + } + + // ==================== MÉTHODES PUBLIQUES ==================== + + /** + * Récupère le résumé des stats pour une date donnée + * + * @param int|null $entityId ID entité (null = toutes entités pour super-admin) + * @param string|null $date Date (YYYY-MM-DD), défaut = aujourd'hui + * @return array Stats résumées par catégorie + */ + public function getSummary(?int $entityId, ?string $date = null): array + { + $date = $date ?? date('Y-m-d'); + + $sql = " + SELECT event_type, count, sum_amount, unique_users, metadata + FROM event_stats_daily + WHERE stat_date = :date + "; + $params = ['date' => $date]; + + if ($entityId !== null) { + $sql .= " AND entity_id = :entity_id"; + $params['entity_id'] = $entityId; + } else { + $sql .= " AND entity_id IS NULL"; + } + + $stmt = $this->db->prepare($sql); + $stmt->execute($params); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return $this->formatSummary($date, $rows); + } + + /** + * Récupère les stats journalières sur une plage de dates + * + * @param int|null $entityId ID entité (null = toutes entités) + * @param string $from Date début (YYYY-MM-DD) + * @param string $to Date fin (YYYY-MM-DD) + * @param array $eventTypes Filtrer par types d'événements (optionnel) + * @return array Stats par jour + */ + public function getDaily(?int $entityId, string $from, string $to, array $eventTypes = []): array + { + $sql = " + SELECT stat_date, event_type, count, sum_amount, unique_users + FROM event_stats_daily + WHERE stat_date BETWEEN :from AND :to + "; + $params = ['from' => $from, 'to' => $to]; + + if ($entityId !== null) { + $sql .= " AND entity_id = :entity_id"; + $params['entity_id'] = $entityId; + } else { + $sql .= " AND entity_id IS NULL"; + } + + if (!empty($eventTypes)) { + $placeholders = []; + foreach ($eventTypes as $i => $type) { + $key = "event_type_{$i}"; + $placeholders[] = ":{$key}"; + $params[$key] = $type; + } + $sql .= " AND event_type IN (" . implode(', ', $placeholders) . ")"; + } + + $sql .= " ORDER BY stat_date ASC, event_type ASC"; + + $stmt = $this->db->prepare($sql); + $stmt->execute($params); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + + return $this->formatDaily($rows); + } + + /** + * Récupère les stats hebdomadaires (calculées depuis daily) + * + * @param int|null $entityId ID entité + * @param string $from Date début + * @param string $to Date fin + * @param array $eventTypes Filtrer par types d'événements + * @return array Stats par semaine + */ + public function getWeekly(?int $entityId, string $from, string $to, array $eventTypes = []): array + { + $daily = $this->getDaily($entityId, $from, $to, $eventTypes); + + $weekly = []; + + foreach ($daily as $day) { + $date = new \DateTime($day['date']); + $weekStart = clone $date; + $weekStart->modify('monday this week'); + $weekKey = $weekStart->format('Y-m-d'); + + if (!isset($weekly[$weekKey])) { + $weekly[$weekKey] = [ + 'week_start' => $weekKey, + 'week_number' => (int) $date->format('W'), + 'year' => (int) $date->format('Y'), + 'events' => [], + 'totals' => [ + 'count' => 0, + 'sum_amount' => 0.0, + ], + ]; + } + + // Agréger les événements + foreach ($day['events'] as $eventType => $stats) { + if (!isset($weekly[$weekKey]['events'][$eventType])) { + $weekly[$weekKey]['events'][$eventType] = [ + 'count' => 0, + 'sum_amount' => 0.0, + 'unique_users' => 0, + ]; + } + $weekly[$weekKey]['events'][$eventType]['count'] += $stats['count']; + $weekly[$weekKey]['events'][$eventType]['sum_amount'] += $stats['sum_amount']; + $weekly[$weekKey]['events'][$eventType]['unique_users'] += $stats['unique_users']; + } + + $weekly[$weekKey]['totals']['count'] += $day['totals']['count']; + $weekly[$weekKey]['totals']['sum_amount'] += $day['totals']['sum_amount']; + } + + return array_values($weekly); + } + + /** + * Récupère les stats mensuelles (calculées depuis daily) + * + * @param int|null $entityId ID entité + * @param int $year Année + * @param array $eventTypes Filtrer par types d'événements + * @return array Stats par mois + */ + public function getMonthly(?int $entityId, int $year, array $eventTypes = []): array + { + $from = "{$year}-01-01"; + $to = "{$year}-12-31"; + + $daily = $this->getDaily($entityId, $from, $to, $eventTypes); + + $monthly = []; + + foreach ($daily as $day) { + $monthKey = substr($day['date'], 0, 7); // YYYY-MM + + if (!isset($monthly[$monthKey])) { + $monthly[$monthKey] = [ + 'month' => $monthKey, + 'year' => $year, + 'month_number' => (int) substr($monthKey, 5, 2), + 'events' => [], + 'totals' => [ + 'count' => 0, + 'sum_amount' => 0.0, + ], + ]; + } + + // Agréger les événements + foreach ($day['events'] as $eventType => $stats) { + if (!isset($monthly[$monthKey]['events'][$eventType])) { + $monthly[$monthKey]['events'][$eventType] = [ + 'count' => 0, + 'sum_amount' => 0.0, + 'unique_users' => 0, + ]; + } + $monthly[$monthKey]['events'][$eventType]['count'] += $stats['count']; + $monthly[$monthKey]['events'][$eventType]['sum_amount'] += $stats['sum_amount']; + $monthly[$monthKey]['events'][$eventType]['unique_users'] += $stats['unique_users']; + } + + $monthly[$monthKey]['totals']['count'] += $day['totals']['count']; + $monthly[$monthKey]['totals']['sum_amount'] += $day['totals']['sum_amount']; + } + + return array_values($monthly); + } + + /** + * Récupère le détail des événements depuis le fichier JSONL + * + * @param int|null $entityId ID entité (null = tous) + * @param string $date Date (YYYY-MM-DD) + * @param string|null $eventType Filtrer par type d'événement + * @param int $limit Nombre max de résultats + * @param int $offset Décalage pour pagination + * @return array Événements détaillés avec pagination + */ + public function getDetails( + ?int $entityId, + string $date, + ?string $eventType = null, + int $limit = 50, + int $offset = 0 + ): array { + $limit = min($limit, self::MAX_DETAILS_LIMIT); + + $filePath = self::EVENT_LOG_DIR . '/' . $date . '.jsonl'; + + if (!file_exists($filePath)) { + return [ + 'date' => $date, + 'events' => [], + 'pagination' => [ + 'total' => 0, + 'limit' => $limit, + 'offset' => $offset, + 'has_more' => false, + ], + ]; + } + + $events = []; + $total = 0; + $currentIndex = 0; + + $handle = fopen($filePath, 'r'); + if (!$handle) { + return [ + 'date' => $date, + 'events' => [], + 'pagination' => [ + 'total' => 0, + 'limit' => $limit, + 'offset' => $offset, + 'has_more' => false, + ], + 'error' => 'Impossible de lire le fichier', + ]; + } + + while (($line = fgets($handle)) !== false) { + $line = trim($line); + if (empty($line)) { + continue; + } + + $event = json_decode($line, true); + if (!$event || !isset($event['event'])) { + continue; + } + + // Filtrer par entity_id + if ($entityId !== null) { + $eventEntityId = $event['entity_id'] ?? null; + if ($eventEntityId !== $entityId) { + continue; + } + } + + // Filtrer par event_type + if ($eventType !== null && ($event['event'] ?? '') !== $eventType) { + continue; + } + + $total++; + + // Pagination + if ($currentIndex >= $offset && count($events) < $limit) { + $events[] = $this->sanitizeEventForOutput($event); + } + + $currentIndex++; + } + + fclose($handle); + + return [ + 'date' => $date, + 'events' => $events, + 'pagination' => [ + 'total' => $total, + 'limit' => $limit, + 'offset' => $offset, + 'has_more' => ($offset + $limit) < $total, + ], + ]; + } + + /** + * Récupère les types d'événements disponibles + * + * @return array Liste des types d'événements + */ + public function getEventTypes(): array + { + return [ + 'auth' => ['login_success', 'login_failed', 'logout'], + 'passages' => ['passage_created', 'passage_updated', 'passage_deleted'], + 'sectors' => ['sector_created', 'sector_updated', 'sector_deleted'], + 'users' => ['user_created', 'user_updated', 'user_deleted'], + 'entities' => ['entity_created', 'entity_updated', 'entity_deleted'], + 'operations' => ['operation_created', 'operation_updated', 'operation_deleted'], + 'stripe' => [ + 'stripe_payment_created', + 'stripe_payment_success', + 'stripe_payment_failed', + 'stripe_payment_cancelled', + 'stripe_terminal_error', + ], + ]; + } + + /** + * Vérifie si des stats existent pour une date + * + * @param string $date Date à vérifier + * @return bool + */ + public function hasStatsForDate(string $date): bool + { + $stmt = $this->db->prepare(" + SELECT COUNT(*) FROM event_stats_daily WHERE stat_date = :date + "); + $stmt->execute(['date' => $date]); + return (int) $stmt->fetchColumn() > 0; + } + + // ==================== MÉTHODES PRIVÉES ==================== + + /** + * Formate le résumé des stats par catégorie + */ + private function formatSummary(string $date, array $rows): array + { + $summary = [ + 'date' => $date, + 'stats' => [ + 'auth' => ['success' => 0, 'failed' => 0, 'logout' => 0], + 'passages' => ['created' => 0, 'updated' => 0, 'deleted' => 0, 'amount' => 0.0], + 'users' => ['created' => 0, 'updated' => 0, 'deleted' => 0], + 'sectors' => ['created' => 0, 'updated' => 0, 'deleted' => 0], + 'entities' => ['created' => 0, 'updated' => 0, 'deleted' => 0], + 'operations' => ['created' => 0, 'updated' => 0, 'deleted' => 0], + 'stripe' => ['created' => 0, 'success' => 0, 'failed' => 0, 'cancelled' => 0, 'amount' => 0.0], + ], + 'totals' => [ + 'events' => 0, + 'unique_users' => 0, + ], + ]; + + $uniqueUsersSet = []; + + foreach ($rows as $row) { + $eventType = $row['event_type']; + $count = (int) $row['count']; + $amount = (float) $row['sum_amount']; + $uniqueUsers = (int) $row['unique_users']; + + $summary['totals']['events'] += $count; + $uniqueUsersSet[$eventType] = $uniqueUsers; + + // Mapper vers les catégories + switch ($eventType) { + case 'login_success': + $summary['stats']['auth']['success'] = $count; + break; + case 'login_failed': + $summary['stats']['auth']['failed'] = $count; + break; + case 'logout': + $summary['stats']['auth']['logout'] = $count; + break; + case 'passage_created': + $summary['stats']['passages']['created'] = $count; + $summary['stats']['passages']['amount'] += $amount; + break; + case 'passage_updated': + $summary['stats']['passages']['updated'] = $count; + break; + case 'passage_deleted': + $summary['stats']['passages']['deleted'] = $count; + break; + case 'user_created': + $summary['stats']['users']['created'] = $count; + break; + case 'user_updated': + $summary['stats']['users']['updated'] = $count; + break; + case 'user_deleted': + $summary['stats']['users']['deleted'] = $count; + break; + case 'sector_created': + $summary['stats']['sectors']['created'] = $count; + break; + case 'sector_updated': + $summary['stats']['sectors']['updated'] = $count; + break; + case 'sector_deleted': + $summary['stats']['sectors']['deleted'] = $count; + break; + case 'entity_created': + $summary['stats']['entities']['created'] = $count; + break; + case 'entity_updated': + $summary['stats']['entities']['updated'] = $count; + break; + case 'entity_deleted': + $summary['stats']['entities']['deleted'] = $count; + break; + case 'operation_created': + $summary['stats']['operations']['created'] = $count; + break; + case 'operation_updated': + $summary['stats']['operations']['updated'] = $count; + break; + case 'operation_deleted': + $summary['stats']['operations']['deleted'] = $count; + break; + case 'stripe_payment_created': + $summary['stats']['stripe']['created'] = $count; + break; + case 'stripe_payment_success': + $summary['stats']['stripe']['success'] = $count; + $summary['stats']['stripe']['amount'] += $amount; + break; + case 'stripe_payment_failed': + $summary['stats']['stripe']['failed'] = $count; + break; + case 'stripe_payment_cancelled': + $summary['stats']['stripe']['cancelled'] = $count; + break; + } + } + + // Estimation des utilisateurs uniques (max des catégories car overlap possible) + $summary['totals']['unique_users'] = !empty($uniqueUsersSet) ? max($uniqueUsersSet) : 0; + + return $summary; + } + + /** + * Formate les stats journalières + */ + private function formatDaily(array $rows): array + { + $daily = []; + + foreach ($rows as $row) { + $date = $row['stat_date']; + + if (!isset($daily[$date])) { + $daily[$date] = [ + 'date' => $date, + 'events' => [], + 'totals' => [ + 'count' => 0, + 'sum_amount' => 0.0, + ], + ]; + } + + $eventType = $row['event_type']; + $count = (int) $row['count']; + $amount = (float) $row['sum_amount']; + + $daily[$date]['events'][$eventType] = [ + 'count' => $count, + 'sum_amount' => $amount, + 'unique_users' => (int) $row['unique_users'], + ]; + + $daily[$date]['totals']['count'] += $count; + $daily[$date]['totals']['sum_amount'] += $amount; + } + + return array_values($daily); + } + + /** + * Nettoie un événement pour l'affichage (supprime données sensibles) + */ + private function sanitizeEventForOutput(array $event): array + { + // Supprimer l'IP complète, garder seulement les 2 premiers octets + if (isset($event['ip'])) { + $parts = explode('.', $event['ip']); + if (count($parts) === 4) { + $event['ip'] = $parts[0] . '.' . $parts[1] . '.x.x'; + } + } + + // Supprimer le user_agent complet + unset($event['user_agent']); + + // Supprimer les données chiffrées si présentes + unset($event['encrypted_name']); + unset($event['encrypted_email']); + + return $event; + } +} diff --git a/api/src/Services/ExportService.php b/api/src/Services/ExportService.php index f035e5a7..7a6f3b32 100755 --- a/api/src/Services/ExportService.php +++ b/api/src/Services/ExportService.php @@ -58,7 +58,7 @@ class ExportService { $filepath = $exportDir . '/' . $filename; // Créer le spreadsheet - $spreadsheet = new PhpOffice\PhpSpreadsheet\Spreadsheet(); + $spreadsheet = new Spreadsheet(); // Insérer les données $this->createPassagesSheet($spreadsheet, $operationId, $userId); @@ -283,11 +283,11 @@ class ExportService { $dateEve = $passage['passed_at'] ? date('d/m/Y', strtotime($passage['passed_at'])) : ''; $heureEve = $passage['passed_at'] ? date('H:i', strtotime($passage['passed_at'])) : ''; - // Déchiffrer les données - $donateur = ApiService::decryptData($passage['encrypted_name']); + // Déchiffrer les données (avec vérification null) + $donateur = !empty($passage['encrypted_name']) ? ApiService::decryptData($passage['encrypted_name']) : ''; $email = !empty($passage['encrypted_email']) ? ApiService::decryptSearchableData($passage['encrypted_email']) : ''; $phone = !empty($passage['encrypted_phone']) ? ApiService::decryptData($passage['encrypted_phone']) : ''; - $userName = ApiService::decryptData($passage['user_name']); + $userName = !empty($passage['user_name']) ? ApiService::decryptData($passage['user_name']) : ''; // Type de passage $typeLabels = [ @@ -382,7 +382,7 @@ class ExportService { foreach ($users as $user) { $rowData = [ $user['id'], - ApiService::decryptData($user['encrypted_name']), + !empty($user['encrypted_name']) ? ApiService::decryptData($user['encrypted_name']) : '', $user['first_name'], !empty($user['encrypted_email']) ? ApiService::decryptSearchableData($user['encrypted_email']) : '', !empty($user['encrypted_phone']) ? ApiService::decryptData($user['encrypted_phone']) : '', @@ -480,7 +480,7 @@ class ExportService { $row = 2; foreach ($userSectors as $us) { - $userName = ApiService::decryptData($us['user_name']); + $userName = !empty($us['user_name']) ? ApiService::decryptData($us['user_name']) : ''; $fullUserName = $us['first_name'] ? $us['first_name'] . ' ' . $userName : $userName; $rowData = [ @@ -690,11 +690,11 @@ class ExportService { $dateEve = $p["passed_at"] ? date("d/m/Y", strtotime($p["passed_at"])) : ""; $heureEve = $p["passed_at"] ? date("H:i", strtotime($p["passed_at"])) : ""; - // Déchiffrer les données - $donateur = ApiService::decryptData($p["encrypted_name"]); + // Déchiffrer les données (avec vérification null) + $donateur = !empty($p["encrypted_name"]) ? ApiService::decryptData($p["encrypted_name"]) : ""; $email = !empty($p["encrypted_email"]) ? ApiService::decryptSearchableData($p["encrypted_email"]) : ""; $phone = !empty($p["encrypted_phone"]) ? ApiService::decryptData($p["encrypted_phone"]) : ""; - $userName = ApiService::decryptData($p["user_name"]); + $userName = !empty($p["user_name"]) ? ApiService::decryptData($p["user_name"]) : ""; // Nettoyer les données (comme dans l'ancienne version) $nom = str_replace("/", "-", $userName); diff --git a/api/src/Services/LogService.php b/api/src/Services/LogService.php index f953e5fe..993dd923 100755 --- a/api/src/Services/LogService.php +++ b/api/src/Services/LogService.php @@ -8,6 +8,12 @@ use AppConfig; use ClientDetector; class LogService { + /** @var int Permissions du dossier */ + private const DIR_PERMISSIONS = 0750; + + /** @var int Permissions des fichiers */ + private const FILE_PERMISSIONS = 0640; + public static function log(string $message, array $metadata = []): void { // Obtenir les informations client via ClientDetector $clientInfo = ClientDetector::getClientInfo(); @@ -67,12 +73,10 @@ class LogService { // Créer le dossier logs s'il n'existe pas if (!is_dir($logDir)) { - if (!mkdir($logDir, 0777, true)) { + if (!mkdir($logDir, self::DIR_PERMISSIONS, true)) { error_log("Impossible de créer le dossier de logs: {$logDir}"); return; // Sortir de la fonction si on ne peut pas créer le dossier } - // S'assurer que les permissions sont correctes - chmod($logDir, 0777); } // Vérifier si le dossier est accessible en écriture @@ -139,26 +143,29 @@ class LogService { $message ]) . "\n"; - // Écrire dans le fichier avec gestion d'erreur - if (file_put_contents($filename, $logLine, FILE_APPEND) === false) { + // Écrire dans le fichier avec gestion d'erreur et verrouillage + if (file_put_contents($filename, $logLine, FILE_APPEND | LOCK_EX) === false) { error_log("Impossible d'écrire dans le fichier de logs: {$filename}"); + } else { + // Appliquer les permissions au fichier + @chmod($filename, self::FILE_PERMISSIONS); } } catch (\Exception $e) { error_log("Erreur lors de l'écriture des logs: " . $e->getMessage()); } } - public function info(string $message, array $metadata = []): void { + public static function info(string $message, array $metadata = []): void { $metadata['level'] = 'info'; self::log($message, $metadata); } - public function warning(string $message, array $metadata = []): void { + public static function warning(string $message, array $metadata = []): void { $metadata['level'] = 'warning'; self::log($message, $metadata); } - public function error(string $message, array $metadata = []): void { + public static function error(string $message, array $metadata = []): void { $metadata['level'] = 'error'; self::log($message, $metadata); } diff --git a/api/src/Services/SectorService.php b/api/src/Services/SectorService.php new file mode 100644 index 00000000..7f0dd283 --- /dev/null +++ b/api/src/Services/SectorService.php @@ -0,0 +1,292 @@ +db = Database::getInstance(); + } + + /** + * Géocode une adresse via api-adresse.data.gouv.fr + * + * @param string $num Numéro de rue + * @param string $bis Complément (bis, ter, etc.) + * @param string $rue Nom de la rue + * @param string $ville Nom de la ville + * @param string $cp Code postal (pour vérifier le département) + * @return array|null [lat, lng] ou null si non trouvé ou score trop faible + */ + public function geocodeAddress(string $num, string $bis, string $rue, string $ville, string $cp): ?array + { + try { + // Construire l'URL de l'API + $query = trim($num . $bis) . ' ' . $rue . ' ' . $ville; + $url = 'https://api-adresse.data.gouv.fr/search/?q=' . urlencode($query); + + LogService::info('[SectorService] Géocodage adresse', [ + 'url' => $url, + 'adresse' => $query + ]); + + // Appel à l'API + $ch = curl_init(); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + $json = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200 || empty($json)) { + LogService::warning('[SectorService] Erreur API géocodage', [ + 'http_code' => $httpCode, + 'adresse' => $query + ]); + return null; + } + + $data = json_decode($json); + + if (empty($data->features)) { + LogService::info('[SectorService] Aucun résultat de géocodage', [ + 'adresse' => $query + ]); + return null; + } + + $score = $data->features[0]->properties->score ?? 0; + + // Vérifier le score (> 0.7 = 70% de confiance) + if (floatval($score) <= 0.7) { + LogService::info('[SectorService] Score géocodage trop faible', [ + 'score' => $score, + 'adresse' => $query + ]); + return null; + } + + // Vérifier le département + $cpTrouve = $data->features[0]->properties->postcode ?? ''; + $deptTrouve = substr($cpTrouve, 0, 2); + + $cpAmicale = $cp; + if (strlen($cpAmicale) == 4) { + $cpAmicale = '0' . $cpAmicale; + } + $deptAmicale = substr($cpAmicale, 0, 2); + + if ($deptTrouve !== $deptAmicale) { + LogService::warning('[SectorService] Département différent', [ + 'dept_trouve' => $deptTrouve, + 'dept_attendu' => $deptAmicale, + 'adresse' => $query + ]); + return null; + } + + // Extraire les coordonnées [lng, lat] -> [lat, lng] + $coordinates = $data->features[0]->geometry->coordinates; + $lat = (float)$coordinates[1]; + $lng = (float)$coordinates[0]; + + LogService::info('[SectorService] Géocodage réussi', [ + 'lat' => $lat, + 'lng' => $lng, + 'score' => $score, + 'adresse' => $query + ]); + + return ['lat' => $lat, 'lng' => $lng]; + + } catch (\Exception $e) { + LogService::error('[SectorService] Erreur géocodage', [ + 'error' => $e->getMessage(), + 'adresse' => "$num$bis $rue $ville" + ]); + return null; + } + } + + /** + * Trouve le secteur contenant une position GPS pour une opération donnée + * + * @param int $operationId ID de l'opération + * @param float $lat Latitude + * @param float $lng Longitude + * @return int|null ID du secteur trouvé ou null + */ + public function findSectorByGps(int $operationId, float $lat, float $lng): ?int + { + try { + // Récupérer tous les secteurs de l'opération avec leur polygone + $query = "SELECT id, sector FROM ope_sectors + WHERE fk_operation = :operation_id + AND chk_active = 1 + AND sector IS NOT NULL + AND sector != ''"; + + $stmt = $this->db->prepare($query); + $stmt->execute(['operation_id' => $operationId]); + $sectors = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (empty($sectors)) { + LogService::info('[SectorService] Aucun secteur trouvé pour l\'opération', [ + 'operation_id' => $operationId + ]); + return null; + } + + // Tester chaque secteur + foreach ($sectors as $sector) { + $polygon = $this->parseSectorPolygon($sector['sector']); + + if (empty($polygon)) { + continue; + } + + if ($this->isPointInsidePolygon($lat, $lng, $polygon)) { + LogService::info('[SectorService] Secteur trouvé par GPS', [ + 'sector_id' => $sector['id'], + 'operation_id' => $operationId, + 'lat' => $lat, + 'lng' => $lng + ]); + return (int)$sector['id']; + } + } + + LogService::info('[SectorService] Aucun secteur ne contient ce point GPS', [ + 'operation_id' => $operationId, + 'lat' => $lat, + 'lng' => $lng, + 'nb_sectors_tested' => count($sectors) + ]); + + return null; + + } catch (\Exception $e) { + LogService::error('[SectorService] Erreur findSectorByGps', [ + 'error' => $e->getMessage(), + 'operation_id' => $operationId, + 'lat' => $lat, + 'lng' => $lng + ]); + return null; + } + } + + /** + * Trouve le secteur pour une adresse (géocodage + recherche GPS) + * + * @param int $operationId ID de l'opération + * @param string $num Numéro de rue + * @param string $bis Complément + * @param string $rue Nom de la rue + * @param string $ville Nom de la ville + * @param string $cp Code postal + * @return array|null ['sector_id' => int, 'gps_lat' => float, 'gps_lng' => float] ou null + */ + public function findSectorByAddress(int $operationId, string $num, string $bis, string $rue, string $ville, string $cp): ?array + { + // Étape 1 : Géocoder l'adresse + $coords = $this->geocodeAddress($num, $bis, $rue, $ville, $cp); + + if (!$coords) { + return null; + } + + // Étape 2 : Chercher le secteur avec les coordonnées obtenues + $sectorId = $this->findSectorByGps($operationId, $coords['lat'], $coords['lng']); + + if (!$sectorId) { + // Retourner quand même les coordonnées GPS trouvées (utiles pour mettre à jour le passage) + return [ + 'sector_id' => null, + 'gps_lat' => $coords['lat'], + 'gps_lng' => $coords['lng'] + ]; + } + + return [ + 'sector_id' => $sectorId, + 'gps_lat' => $coords['lat'], + 'gps_lng' => $coords['lng'] + ]; + } + + /** + * Parse le format de polygone stocké en base (lat/lng#lat/lng#...) + * + * @param string $sectorString Format "lat/lng#lat/lng#..." + * @return array Array de ['lat' => float, 'lng' => float] + */ + private function parseSectorPolygon(string $sectorString): array + { + $polygon = []; + $points = explode('#', rtrim($sectorString, '#')); + + foreach ($points as $point) { + if (!empty($point) && strpos($point, '/') !== false) { + list($lat, $lng) = explode('/', $point); + $polygon[] = [ + 'lat' => (float)$lat, + 'lng' => (float)$lng + ]; + } + } + + return $polygon; + } + + /** + * Vérifie si un point est à l'intérieur d'un polygone + * Utilise l'algorithme de ray casting + * + * @param float $lat Latitude du point + * @param float $lng Longitude du point + * @param array $polygon Array de ['lat' => float, 'lng' => float] + * @return bool + */ + private function isPointInsidePolygon(float $lat, float $lng, array $polygon): bool + { + $x = $lat; + $y = $lng; + $inside = false; + $count = count($polygon); + + for ($i = 0, $j = $count - 1; $i < $count; $j = $i++) { + $xi = $polygon[$i]['lat']; + $yi = $polygon[$i]['lng']; + $xj = $polygon[$j]['lat']; + $yj = $polygon[$j]['lng']; + + $intersect = (($yi > $y) != ($yj > $y)) + && ($x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi); + + if ($intersect) { + $inside = !$inside; + } + } + + return $inside; + } +} diff --git a/api/src/Services/StripeService.php b/api/src/Services/StripeService.php index 79f3ce2e..46b1da6a 100644 --- a/api/src/Services/StripeService.php +++ b/api/src/Services/StripeService.php @@ -465,64 +465,68 @@ class StripeService { $entiteId = $params['fk_entite'] ?? 0; $userId = $params['fk_user'] ?? 0; $metadata = $params['metadata'] ?? []; - + $paymentMethodTypes = $params['payment_method_types'] ?? ['card_present']; + if ($amount < 100) { throw new Exception("Le montant minimum est de 1€"); } - + // Récupérer le compte Stripe $stmt = $this->db->prepare( "SELECT * FROM stripe_accounts WHERE fk_entite = :fk_entite" ); $stmt->execute(['fk_entite' => $entiteId]); $account = $stmt->fetch(PDO::FETCH_ASSOC); - + if (!$account) { throw new Exception("Compte Stripe non trouvé"); } - - // Pas de commission plateforme - 100% pour l'amicale - - // Créer le PaymentIntent sans commission - $paymentIntent = $this->stripe->paymentIntents->create([ + + // Déterminer le mode : Tap to Pay (card_present) ou Payment Link (card) + $isTapToPay = in_array('card_present', $paymentMethodTypes); + + // Configuration du PaymentIntent selon le mode + $paymentIntentData = [ 'amount' => $amount, 'currency' => 'eur', - 'payment_method_types' => ['card_present'], + 'payment_method_types' => $paymentMethodTypes, 'capture_method' => 'automatic', - // Pas d'application_fee_amount - tout va à l'amicale - 'transfer_data' => [ - 'destination' => $account['stripe_account_id'], - ], 'metadata' => array_merge($metadata, [ 'entite_id' => $entiteId, 'user_id' => $userId, 'calendrier_annee' => date('Y'), ]), - ]); - - // Sauvegarder en base - $stmt = $this->db->prepare( - "INSERT INTO stripe_payment_intents - (stripe_payment_intent_id, fk_entite, fk_user, amount, currency, status, application_fee, metadata, created_at) - VALUES (:pi_id, :fk_entite, :fk_user, :amount, :currency, :status, :app_fee, :metadata, NOW())" + ]; + + // Options Stripe (avec ou sans stripe_account) + $stripeOptions = []; + + if ($isTapToPay) { + // TAP TO PAY : Paiement direct sur le compte connecté + // Le PaymentIntent est créé sur le compte de l'amicale + $stripeOptions['stripe_account'] = $account['stripe_account_id']; + } else { + // PAYMENT LINK / WEB : Paiement via la plateforme avec transfert + // Le PaymentIntent est créé sur la plateforme et transféré + $paymentIntentData['transfer_data'] = [ + 'destination' => $account['stripe_account_id'], + ]; + } + + // Créer le PaymentIntent + $paymentIntent = $this->stripe->paymentIntents->create( + $paymentIntentData, + $stripeOptions ); - $stmt->execute([ - 'pi_id' => $paymentIntent->id, - 'fk_entite' => $entiteId, - 'fk_user' => $userId, - 'amount' => $amount, - 'currency' => 'eur', - 'status' => $paymentIntent->status, - 'app_fee' => 0, // Pas de commission - 'metadata' => json_encode($metadata) - ]); - + + // Note : Le payment_intent_id est sauvegardé dans ope_pass.stripe_payment_id par le controller + return [ 'success' => true, 'client_secret' => $paymentIntent->client_secret, 'payment_intent_id' => $paymentIntent->id, 'amount' => $amount, - 'application_fee' => 0 // Pas de commission + 'mode' => $isTapToPay ? 'tap_to_pay' : 'payment_link' ]; } catch (Exception $e) { @@ -532,76 +536,7 @@ class StripeService { ]; } } - - /** - * Vérifier la compatibilité Tap to Pay d'un appareil Android - */ - public function checkAndroidTapToPayCompatibility(string $manufacturer, string $model): array { - try { - $stmt = $this->db->prepare( - "SELECT * FROM stripe_android_certified_devices - WHERE manufacturer = :manufacturer - AND model = :model - AND tap_to_pay_certified = 1 - AND country = 'FR'" - ); - $stmt->execute([ - 'manufacturer' => $manufacturer, - 'model' => $model - ]); - - $device = $stmt->fetch(PDO::FETCH_ASSOC); - - if ($device) { - return [ - 'success' => true, - 'tap_to_pay_supported' => true, - 'message' => 'Tap to Pay disponible sur cet appareil', - 'min_android_version' => $device['min_android_version'] - ]; - } - - return [ - 'success' => true, - 'tap_to_pay_supported' => false, - 'message' => 'Appareil non certifié pour Tap to Pay en France', - 'alternative' => 'Utilisez un iPhone XS ou plus récent avec iOS 16.4+' - ]; - - } catch (Exception $e) { - return [ - 'success' => false, - 'message' => 'Erreur: ' . $e->getMessage() - ]; - } - } - - /** - * Récupérer les appareils Android certifiés - */ - public function getCertifiedAndroidDevices(): array { - try { - $stmt = $this->db->prepare( - "SELECT manufacturer, model, model_identifier, min_android_version - FROM stripe_android_certified_devices - WHERE tap_to_pay_certified = 1 AND country = 'FR' - ORDER BY manufacturer, model" - ); - $stmt->execute(); - - return [ - 'success' => true, - 'devices' => $stmt->fetchAll(PDO::FETCH_ASSOC) - ]; - - } catch (Exception $e) { - return [ - 'success' => false, - 'message' => 'Erreur: ' . $e->getMessage() - ]; - } - } - + /** * Créer un Payment Link Stripe pour paiement par QR Code * @@ -747,6 +682,47 @@ class StripeService { } } + /** + * Annuler un PaymentIntent Stripe + * + * @param string $paymentIntentId L'ID du PaymentIntent à annuler + * @return array ['success' => bool, 'status' => string|null, 'message' => string|null] + */ + public function cancelPaymentIntent(string $paymentIntentId): array { + try { + // Annuler le PaymentIntent via l'API Stripe + $paymentIntent = $this->stripe->paymentIntents->cancel($paymentIntentId); + + LogService::log('PaymentIntent annulé', [ + 'payment_intent_id' => $paymentIntentId, + 'status' => $paymentIntent->status + ]); + + return [ + 'success' => true, + 'status' => $paymentIntent->status, + 'payment_intent_id' => $paymentIntentId + ]; + + } catch (ApiErrorException $e) { + LogService::log('Erreur annulation PaymentIntent Stripe', [ + 'level' => 'error', + 'payment_intent_id' => $paymentIntentId, + 'error' => $e->getMessage() + ]); + + return [ + 'success' => false, + 'message' => 'Erreur Stripe: ' . $e->getMessage() + ]; + } catch (Exception $e) { + return [ + 'success' => false, + 'message' => 'Erreur: ' . $e->getMessage() + ]; + } + } + /** * Obtenir le mode actuel (test ou live) */ diff --git a/app/.dart_tool/package_config.json b/app/.dart_tool/package_config.json index af0269aa..3989fe6a 100644 --- a/app/.dart_tool/package_config.json +++ b/app/.dart_tool/package_config.json @@ -3,1147 +3,1159 @@ "packages": [ { "name": "_fe_analyzer_shared", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-72.0.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/_fe_analyzer_shared-72.0.0", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "_macros", - "rootUri": "file:///opt/flutter/bin/cache/dart-sdk/pkg/_macros", + "rootUri": "file:///home/pierre/.local/flutter/bin/cache/dart-sdk/pkg/_macros", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "analyzer", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/analyzer-6.7.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/analyzer-6.7.0", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "archive", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/archive-4.0.7", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/archive-4.0.7", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "args", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/args-2.7.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/args-2.7.0", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "async", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/async-2.11.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/async-2.11.0", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "battery_plus", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "battery_plus_platform_interface", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/battery_plus_platform_interface-2.0.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus_platform_interface-2.0.1", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "boolean_selector", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/boolean_selector-2.1.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/boolean_selector-2.1.1", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "build", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/build-2.4.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/build-2.4.1", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "build_config", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/build_config-1.1.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/build_config-1.1.1", "packageUri": "lib/", "languageVersion": "2.14" }, { "name": "build_daemon", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/build_daemon-4.0.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/build_daemon-4.0.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "build_resolvers", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/build_resolvers-2.4.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/build_resolvers-2.4.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "build_runner", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/build_runner-2.4.13", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/build_runner-2.4.13", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "build_runner_core", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/build_runner_core-7.3.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/build_runner_core-7.3.2", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "built_collection", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/built_collection-5.1.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/built_collection-5.1.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "built_value", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/built_value-8.12.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/built_value-8.12.3", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "characters", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/characters-1.3.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/characters-1.3.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "charcode", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/charcode-1.4.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/charcode-1.4.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "checked_yaml", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/checked_yaml-2.0.3", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/checked_yaml-2.0.3", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "class_to_string", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/class_to_string-1.1.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/class_to_string-1.1.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "cli_util", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/cli_util-0.4.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/cli_util-0.4.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "clock", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/clock-1.1.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/clock-1.1.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "code_builder", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/code_builder-4.10.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/code_builder-4.10.1", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "collection", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/collection-1.18.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/collection-1.18.0", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "connectivity_plus", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "connectivity_plus_platform_interface", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "convert", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/convert-3.1.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/convert-3.1.2", "packageUri": "lib/", "languageVersion": "3.4" }, + { + "name": "cookie_jar", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/cookie_jar-4.0.8", + "packageUri": "lib/", + "languageVersion": "2.15" + }, { "name": "cross_file", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/cross_file-0.3.4+2", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "crypto", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/crypto-3.0.7", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/crypto-3.0.7", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "csslib", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/csslib-1.0.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/csslib-1.0.2", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "cupertino_icons", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/cupertino_icons-1.0.8", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/cupertino_icons-1.0.8", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "dart_earcut", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dart_earcut-1.2.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dart_earcut-1.2.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "dart_style", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dart_style-2.3.7", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dart_style-2.3.7", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "dbus", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dbus-0.7.11", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dbus-0.7.11", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "device_info_plus", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "device_info_plus_platform_interface", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/device_info_plus_platform_interface-7.0.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus_platform_interface-7.0.2", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "dio", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dio-5.9.0", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "dio_cache_interceptor", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dio_cache_interceptor-3.5.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dio_cache_interceptor-3.5.1", "packageUri": "lib/", "languageVersion": "2.14" }, { "name": "dio_cache_interceptor_hive_store", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dio_cache_interceptor_hive_store-3.2.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dio_cache_interceptor_hive_store-3.2.2", "packageUri": "lib/", "languageVersion": "2.14" }, + { + "name": "dio_cookie_manager", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dio_cookie_manager-3.3.0", + "packageUri": "lib/", + "languageVersion": "2.18" + }, { "name": "dio_web_adapter", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dio_web_adapter-2.1.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/dio_web_adapter-2.1.1", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "fake_async", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/fake_async-1.3.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/fake_async-1.3.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "ffi", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/ffi-2.1.3", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/ffi-2.1.3", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "file", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/file-7.0.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file-7.0.1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "file_selector_linux", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_linux-0.9.3+2", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "file_selector_macos", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_macos-0.9.4+2", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "file_selector_platform_interface", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_platform_interface-2.6.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_platform_interface-2.6.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "file_selector_windows", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+4", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_windows-0.9.3+4", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "fixnum", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/fixnum-1.1.1", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "flutter", - "rootUri": "file:///opt/flutter/packages/flutter", + "rootUri": "file:///home/pierre/.local/flutter/packages/flutter", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "flutter_launcher_icons", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_launcher_icons-0.14.4", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_launcher_icons-0.14.4", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "flutter_lints", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_lints-5.0.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_lints-5.0.0", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "flutter_local_notifications", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.5.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications-19.5.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "flutter_local_notifications_linux", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-6.0.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_linux-6.0.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "flutter_local_notifications_platform_interface", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_platform_interface-9.1.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_platform_interface-9.1.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "flutter_local_notifications_windows", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_windows-1.0.3", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_windows-1.0.3", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "flutter_localizations", - "rootUri": "file:///opt/flutter/packages/flutter_localizations", + "rootUri": "file:///home/pierre/.local/flutter/packages/flutter_localizations", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "flutter_map", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-7.0.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_map-7.0.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "flutter_map_cache", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map_cache-1.5.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_map_cache-1.5.1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "flutter_plugin_android_lifecycle", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.26", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.26", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "flutter_stripe", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_stripe-11.5.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_stripe-11.5.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "flutter_svg", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.1.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_svg-2.1.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "flutter_test", - "rootUri": "file:///opt/flutter/packages/flutter_test", + "rootUri": "file:///home/pierre/.local/flutter/packages/flutter_test", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "flutter_web_plugins", - "rootUri": "file:///opt/flutter/packages/flutter_web_plugins", + "rootUri": "file:///home/pierre/.local/flutter/packages/flutter_web_plugins", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "freezed_annotation", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/freezed_annotation-2.4.4", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/freezed_annotation-2.4.4", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "frontend_server_client", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/frontend_server_client-4.0.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "geolocator", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator-13.0.4", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator-13.0.4", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "geolocator_android", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_android-4.6.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_android-4.6.1", "packageUri": "lib/", "languageVersion": "2.15" }, { "name": "geolocator_apple", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_apple-2.3.13", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "geolocator_platform_interface", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_platform_interface-4.2.6", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_platform_interface-4.2.6", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "geolocator_web", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_web-4.1.3", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "geolocator_windows", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_windows-0.2.5", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "glob", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/glob-2.1.3", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/glob-2.1.3", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "go_router", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/go_router-15.1.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/go_router-15.1.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "google_fonts", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/google_fonts-6.3.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/google_fonts-6.3.0", "packageUri": "lib/", "languageVersion": "2.14" }, { "name": "graphs", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/graphs-2.3.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/graphs-2.3.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "hive", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/hive-2.2.3", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/hive-2.2.3", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "hive_flutter", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/hive_flutter-1.1.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/hive_flutter-1.1.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "hive_generator", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/hive_generator-2.0.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/hive_generator-2.0.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "html", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/html-0.15.6", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/html-0.15.6", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "http", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/http-1.6.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "http_multi_server", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/http_multi_server-3.2.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/http_multi_server-3.2.2", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "http_parser", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.0.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/http_parser-4.0.2", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "image", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image-4.5.4", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image-4.7.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "image_picker", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-0.8.9", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker-0.8.9", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "image_picker_android", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.12+21", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_android-0.8.12+21", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "image_picker_for_web", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-2.1.12", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_for_web-2.1.12", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "image_picker_ios", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_ios-0.8.12+2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "image_picker_linux", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_linux-0.2.1+2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "image_picker_macos", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_macos-0.2.1+2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "image_picker_platform_interface", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_platform_interface-2.10.1", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "image_picker_windows", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_windows-0.2.1+1", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "intl", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.19.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/intl-0.19.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "io", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/io-1.0.5", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/io-1.0.5", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "js", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/js-0.7.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/js-0.7.1", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "json_annotation", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/json_annotation-4.9.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/json_annotation-4.9.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "latlong2", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/latlong2-0.9.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/latlong2-0.9.1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "leak_tracker", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/leak_tracker-10.0.5", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/leak_tracker-10.0.5", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "leak_tracker_flutter_testing", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.5", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/leak_tracker_flutter_testing-3.0.5", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "leak_tracker_testing", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/leak_tracker_testing-3.0.1", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "lints", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/lints-5.0.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/lints-5.0.0", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "lists", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/lists-1.0.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/lists-1.0.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "logger", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/logger-2.6.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/logger-2.6.2", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "logging", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/logging-1.3.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/logging-1.3.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "macros", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/macros-0.1.2-main.4", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/macros-0.1.2-main.4", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "matcher", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/matcher-0.12.16+1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/matcher-0.12.16+1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "material_color_utilities", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/material_color_utilities-0.11.1", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "mek_data_class", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/mek_data_class-1.4.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/mek_data_class-1.4.1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "mek_stripe_terminal", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/mek_stripe_terminal-4.6.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/mek_stripe_terminal-4.6.1", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "meta", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/meta-1.15.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/meta-1.15.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "mgrs_dart", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/mgrs_dart-2.0.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "mime", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/mime-2.0.0", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "nfc_manager", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/nfc_manager-3.3.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/nfc_manager-3.3.0", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "nm", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/nm-0.5.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/nm-0.5.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "one_for_all", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/one_for_all-1.1.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/one_for_all-1.1.1", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "package_config", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/package_config-2.2.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/package_config-2.2.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "path", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path-1.9.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path-1.9.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "path_parsing", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_parsing-1.1.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_parsing-1.1.0", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "path_provider", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider-2.1.5", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider-2.1.5", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "path_provider_android", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_android-2.2.15", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_android-2.2.15", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "path_provider_foundation", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_foundation-2.4.1", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "path_provider_linux", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_linux-2.2.1", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "path_provider_platform_interface", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_platform_interface-2.1.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_platform_interface-2.1.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "path_provider_windows", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_windows-2.3.0", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "permission_handler", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/permission_handler-12.0.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler-12.0.1", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "permission_handler_android", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/permission_handler_android-13.0.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_android-13.0.1", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "permission_handler_apple", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.7", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_apple-9.4.7", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "permission_handler_html", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/permission_handler_html-0.1.3+5", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_html-0.1.3+5", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "permission_handler_platform_interface", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/permission_handler_platform_interface-4.3.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_platform_interface-4.3.0", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "permission_handler_windows", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_windows-0.2.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "petitparser", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/petitparser-6.0.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/petitparser-6.0.2", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "platform", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/platform-3.1.6", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/platform-3.1.6", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "plugin_platform_interface", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/plugin_platform_interface-2.1.8", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/plugin_platform_interface-2.1.8", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "polylabel", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/polylabel-1.0.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/polylabel-1.0.1", "packageUri": "lib/", "languageVersion": "2.13" }, { "name": "pool", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/pool-1.5.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/pool-1.5.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "posix", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/posix-6.0.3", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/posix-6.0.3", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "proj4dart", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/proj4dart-2.1.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/proj4dart-2.1.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "pub_semver", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/pub_semver-2.2.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/pub_semver-2.2.0", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "pubspec_parse", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/pubspec_parse-1.4.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/pubspec_parse-1.4.0", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "qr", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/qr-3.0.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/qr-3.0.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "qr_flutter", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/qr_flutter-4.1.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/qr_flutter-4.1.0", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "recase", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/recase-4.1.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/recase-4.1.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "retry", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/retry-3.1.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/retry-3.1.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "shelf", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/shelf-1.4.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/shelf-1.4.1", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "shelf_web_socket", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/shelf_web_socket-2.0.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/shelf_web_socket-2.0.1", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "sky_engine", - "rootUri": "file:///opt/flutter/bin/cache/pkg/sky_engine", + "rootUri": "file:///home/pierre/.local/flutter/bin/cache/pkg/sky_engine", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "source_gen", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/source_gen-1.5.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/source_gen-1.5.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "source_helper", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/source_helper-1.3.5", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/source_helper-1.3.5", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "source_span", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/source_span-1.10.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/source_span-1.10.0", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "stack_trace", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/stack_trace-1.11.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stack_trace-1.11.1", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "stream_channel", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/stream_channel-2.1.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stream_channel-2.1.2", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "stream_transform", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/stream_transform-2.1.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stream_transform-2.1.1", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "string_scanner", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.2.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/string_scanner-1.2.0", "packageUri": "lib/", "languageVersion": "2.18" }, { "name": "stripe_android", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/stripe_android-11.5.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stripe_android-11.5.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "stripe_ios", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/stripe_ios-11.5.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stripe_ios-11.5.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "stripe_platform_interface", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/stripe_platform_interface-11.5.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stripe_platform_interface-11.5.0", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "syncfusion_flutter_charts", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-27.2.5", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/syncfusion_flutter_charts-27.2.5", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "syncfusion_flutter_core", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-27.2.5", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/syncfusion_flutter_core-27.2.5", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "term_glyph", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/term_glyph-1.2.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "test_api", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/test_api-0.7.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/test_api-0.7.2", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "timezone", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/timezone-0.10.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/timezone-0.10.1", "packageUri": "lib/", "languageVersion": "2.19" }, { "name": "timing", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/timing-1.0.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/timing-1.0.2", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "typed_data", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/typed_data-1.4.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/typed_data-1.4.0", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "unicode", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/unicode-0.3.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/unicode-0.3.1", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "universal_html", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/universal_html-2.2.4", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/universal_html-2.2.4", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "universal_io", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/universal_io-2.2.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/universal_io-2.2.2", "packageUri": "lib/", "languageVersion": "2.17" }, { "name": "upower", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/upower-0.7.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/upower-0.7.0", "packageUri": "lib/", "languageVersion": "2.14" }, { "name": "url_launcher", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher-6.3.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher-6.3.1", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "url_launcher_android", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_android-6.3.14", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "url_launcher_ios", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.3", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_ios-6.3.3", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "url_launcher_linux", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_linux-3.2.1", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "url_launcher_macos", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_macos-3.2.2", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "url_launcher_platform_interface", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_platform_interface-2.3.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_platform_interface-2.3.2", "packageUri": "lib/", "languageVersion": "3.1" }, { "name": "url_launcher_web", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_web-2.3.3", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "url_launcher_windows", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.4", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_windows-3.1.4", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "uuid", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/uuid-4.5.2", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/uuid-4.5.2", "packageUri": "lib/", "languageVersion": "3.0" }, { "name": "vector_graphics", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics-1.1.18", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/vector_graphics-1.1.18", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "vector_graphics_codec", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_codec-1.1.13", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/vector_graphics_codec-1.1.13", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "vector_graphics_compiler", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vector_graphics_compiler-1.1.16", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/vector_graphics_compiler-1.1.16", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "vector_math", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/vector_math-2.1.4", "packageUri": "lib/", "languageVersion": "2.14" }, { "name": "vm_service", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/vm_service-14.2.5", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/vm_service-14.2.5", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "watcher", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/watcher-1.1.4", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/watcher-1.2.1", "packageUri": "lib/", - "languageVersion": "3.1" + "languageVersion": "3.4" }, { "name": "web", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/web-1.1.1", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "web_socket", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/web_socket-1.0.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/web_socket-1.0.1", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "web_socket_channel", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/web_socket_channel-3.0.3", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/web_socket_channel-3.0.3", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "win32", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/win32-5.10.1", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/win32-5.10.1", "packageUri": "lib/", "languageVersion": "3.5" }, { "name": "win32_registry", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/win32_registry-1.1.5", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/win32_registry-1.1.5", "packageUri": "lib/", "languageVersion": "3.4" }, { "name": "wkt_parser", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/wkt_parser-2.0.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/wkt_parser-2.0.0", "packageUri": "lib/", "languageVersion": "2.12" }, { "name": "xdg_directories", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/xdg_directories-1.1.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/xdg_directories-1.1.0", "packageUri": "lib/", "languageVersion": "3.3" }, { "name": "xml", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/xml-6.5.0", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/xml-6.5.0", "packageUri": "lib/", "languageVersion": "3.2" }, { "name": "yaml", - "rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/yaml-3.1.3", + "rootUri": "file:///home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/yaml-3.1.3", "packageUri": "lib/", "languageVersion": "3.4" }, @@ -1154,10 +1166,10 @@ "languageVersion": "3.0" } ], - "generated": "2025-11-09T17:48:24.059730Z", + "generated": "2026-01-16T12:37:51.884211Z", "generator": "pub", "generatorVersion": "3.5.4", - "flutterRoot": "file:///opt/flutter", + "flutterRoot": "file:///home/pierre/.local/flutter", "flutterVersion": "3.24.5", - "pubCache": "file:///home/pierre/.pub-cache" + "pubCache": "file:///home/pierre/dev/geosector/app/.pub-cache-local" } diff --git a/app/.flutter-plugins-dependencies b/app/.flutter-plugins-dependencies index 4030753a..13adc821 100644 --- a/app/.flutter-plugins-dependencies +++ b/app/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"battery_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.5.0/","native_build":true,"dependencies":[]},{"name":"geolocator_apple","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"image_picker_ios","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/","native_build":true,"dependencies":[]},{"name":"mek_stripe_terminal","path":"/home/pierre/.pub-cache/hosted/pub.dev/mek_stripe_terminal-4.6.1/","native_build":true,"dependencies":[]},{"name":"nfc_manager","path":"/home/pierre/.pub-cache/hosted/pub.dev/nfc_manager-3.3.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"permission_handler_apple","path":"/home/pierre/.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.7/","native_build":true,"dependencies":[]},{"name":"stripe_ios","path":"/home/pierre/.pub-cache/hosted/pub.dev/stripe_ios-11.5.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.3/","native_build":true,"dependencies":[]}],"android":[{"name":"battery_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/","native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.5.0/","native_build":true,"dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.26/","native_build":true,"dependencies":[]},{"name":"geolocator_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_android-4.6.1/","native_build":true,"dependencies":[]},{"name":"image_picker_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.12+21/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"mek_stripe_terminal","path":"/home/pierre/.pub-cache/hosted/pub.dev/mek_stripe_terminal-4.6.1/","native_build":true,"dependencies":[]},{"name":"nfc_manager","path":"/home/pierre/.pub-cache/hosted/pub.dev/nfc_manager-3.3.0/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_android-2.2.15/","native_build":true,"dependencies":[]},{"name":"permission_handler_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/permission_handler_android-13.0.1/","native_build":true,"dependencies":[]},{"name":"stripe_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/stripe_android-11.5.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"battery_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[]},{"name":"file_selector_macos","path":"/home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+2/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications-19.5.0/","native_build":true,"dependencies":[]},{"name":"geolocator_apple","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"image_picker_macos","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+2/","native_build":false,"dependencies":["file_selector_macos"]},{"name":"path_provider_foundation","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.2/","native_build":true,"dependencies":[]}],"linux":[{"name":"battery_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/","native_build":false,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/","native_build":false,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":false,"dependencies":[]},{"name":"file_selector_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_linux-6.0.0/","native_build":false,"dependencies":[]},{"name":"image_picker_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+2/","native_build":false,"dependencies":["file_selector_linux"]},{"name":"path_provider_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"url_launcher_linux","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"battery_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/","native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":false,"dependencies":[]},{"name":"file_selector_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+4/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/flutter_local_notifications_windows-1.0.3/","native_build":true,"dependencies":[]},{"name":"geolocator_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[]},{"name":"image_picker_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/","native_build":false,"dependencies":["file_selector_windows"]},{"name":"path_provider_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"permission_handler_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1/","native_build":true,"dependencies":[]},{"name":"url_launcher_windows","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.4/","native_build":true,"dependencies":[]}],"web":[{"name":"battery_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/battery_plus-6.0.3/","dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus-6.0.5/","dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","dependencies":[]},{"name":"geolocator_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/geolocator_web-4.1.3/","dependencies":[]},{"name":"image_picker_for_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-2.1.12/","dependencies":[]},{"name":"permission_handler_html","path":"/home/pierre/.pub-cache/hosted/pub.dev/permission_handler_html-0.1.3+5/","dependencies":[]},{"name":"url_launcher_web","path":"/home/pierre/.pub-cache/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"battery_plus","dependencies":[]},{"name":"connectivity_plus","dependencies":[]},{"name":"device_info_plus","dependencies":[]},{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux","flutter_local_notifications_windows"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"flutter_local_notifications_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_stripe","dependencies":["stripe_android","stripe_ios"]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios","image_picker_linux","image_picker_macos","image_picker_windows"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]},{"name":"image_picker_linux","dependencies":["file_selector_linux"]},{"name":"image_picker_macos","dependencies":["file_selector_macos"]},{"name":"image_picker_windows","dependencies":["file_selector_windows"]},{"name":"mek_stripe_terminal","dependencies":[]},{"name":"nfc_manager","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"stripe_android","dependencies":[]},{"name":"stripe_ios","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2025-11-09 18:48:24.197820","version":"3.24.5","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"battery_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications-19.5.0/","native_build":true,"dependencies":[]},{"name":"geolocator_apple","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"image_picker_ios","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_ios-0.8.12+2/","native_build":true,"dependencies":[]},{"name":"mek_stripe_terminal","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/mek_stripe_terminal-4.6.1/","native_build":true,"dependencies":[]},{"name":"nfc_manager","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/nfc_manager-3.3.0/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"permission_handler_apple","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_apple-9.4.7/","native_build":true,"dependencies":[]},{"name":"stripe_ios","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stripe_ios-11.5.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_ios","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_ios-6.3.3/","native_build":true,"dependencies":[]}],"android":[{"name":"battery_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/","native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications-19.5.0/","native_build":true,"dependencies":[]},{"name":"flutter_plugin_android_lifecycle","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.26/","native_build":true,"dependencies":[]},{"name":"geolocator_android","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_android-4.6.1/","native_build":true,"dependencies":[]},{"name":"image_picker_android","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_android-0.8.12+21/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"mek_stripe_terminal","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/mek_stripe_terminal-4.6.1/","native_build":true,"dependencies":[]},{"name":"nfc_manager","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/nfc_manager-3.3.0/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_android-2.2.15/","native_build":true,"dependencies":[]},{"name":"permission_handler_android","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_android-13.0.1/","native_build":true,"dependencies":[]},{"name":"stripe_android","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/stripe_android-11.5.0/","native_build":true,"dependencies":[]},{"name":"url_launcher_android","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_android-6.3.14/","native_build":true,"dependencies":[]}],"macos":[{"name":"battery_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[]},{"name":"file_selector_macos","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_macos-0.9.4+2/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications-19.5.0/","native_build":true,"dependencies":[]},{"name":"geolocator_apple","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_apple-2.3.13/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"image_picker_macos","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_macos-0.2.1+2/","native_build":false,"dependencies":["file_selector_macos"]},{"name":"path_provider_foundation","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"url_launcher_macos","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_macos-3.2.2/","native_build":true,"dependencies":[]}],"linux":[{"name":"battery_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/","native_build":false,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/","native_build":false,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/","native_build":false,"dependencies":[]},{"name":"file_selector_linux","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_linux-0.9.3+2/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications_linux","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_linux-6.0.0/","native_build":false,"dependencies":[]},{"name":"image_picker_linux","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_linux-0.2.1+2/","native_build":false,"dependencies":["file_selector_linux"]},{"name":"path_provider_linux","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"url_launcher_linux","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_linux-3.2.1/","native_build":true,"dependencies":[]}],"windows":[{"name":"battery_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/","native_build":true,"dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/","native_build":true,"dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/","native_build":false,"dependencies":[]},{"name":"file_selector_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/file_selector_windows-0.9.3+4/","native_build":true,"dependencies":[]},{"name":"flutter_local_notifications_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/flutter_local_notifications_windows-1.0.3/","native_build":true,"dependencies":[]},{"name":"geolocator_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_windows-0.2.5/","native_build":true,"dependencies":[]},{"name":"image_picker_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_windows-0.2.1+1/","native_build":false,"dependencies":["file_selector_windows"]},{"name":"path_provider_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[]},{"name":"permission_handler_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_windows-0.2.1/","native_build":true,"dependencies":[]},{"name":"url_launcher_windows","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_windows-3.1.4/","native_build":true,"dependencies":[]}],"web":[{"name":"battery_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/battery_plus-6.0.3/","dependencies":[]},{"name":"connectivity_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/connectivity_plus-6.0.5/","dependencies":[]},{"name":"device_info_plus","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/device_info_plus-11.3.0/","dependencies":[]},{"name":"geolocator_web","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/geolocator_web-4.1.3/","dependencies":[]},{"name":"image_picker_for_web","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/image_picker_for_web-2.1.12/","dependencies":[]},{"name":"permission_handler_html","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/permission_handler_html-0.1.3+5/","dependencies":[]},{"name":"url_launcher_web","path":"/home/pierre/dev/geosector/app/.pub-cache-local/hosted/pub.dev/url_launcher_web-2.3.3/","dependencies":[]}]},"dependencyGraph":[{"name":"battery_plus","dependencies":[]},{"name":"connectivity_plus","dependencies":[]},{"name":"device_info_plus","dependencies":[]},{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"flutter_local_notifications","dependencies":["flutter_local_notifications_linux","flutter_local_notifications_windows"]},{"name":"flutter_local_notifications_linux","dependencies":[]},{"name":"flutter_local_notifications_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_stripe","dependencies":["stripe_android","stripe_ios"]},{"name":"geolocator","dependencies":["geolocator_android","geolocator_apple","geolocator_web","geolocator_windows"]},{"name":"geolocator_android","dependencies":[]},{"name":"geolocator_apple","dependencies":[]},{"name":"geolocator_web","dependencies":[]},{"name":"geolocator_windows","dependencies":[]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios","image_picker_linux","image_picker_macos","image_picker_windows"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]},{"name":"image_picker_linux","dependencies":["file_selector_linux"]},{"name":"image_picker_macos","dependencies":["file_selector_macos"]},{"name":"image_picker_windows","dependencies":["file_selector_windows"]},{"name":"mek_stripe_terminal","dependencies":[]},{"name":"nfc_manager","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"stripe_android","dependencies":[]},{"name":"stripe_ios","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-01-16 13:38:00.688799","version":"3.24.5","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/app/IOS-BUILD-GUIDE.md b/app/IOS-BUILD-GUIDE.md index 3ed21ce7..9543d19e 100644 --- a/app/IOS-BUILD-GUIDE.md +++ b/app/IOS-BUILD-GUIDE.md @@ -1,208 +1,143 @@ # 🍎 Guide de Build iOS - GEOSECTOR -**Date de création** : 21/10/2025 -**Version actuelle** : 3.4.2 (Build 342) +**Dernière mise à jour** : 16/11/2025 +**Version système** : Workflow automatisé depuis Debian --- -## 📋 **Prérequis** +## 📋 Prérequis -### Sur le Mac mini -- ✅ macOS installé -- ✅ Xcode installé avec Command Line Tools -- ✅ Flutter installé (3.24.5 LTS recommandé) -- ✅ CocoaPods installé (`sudo gem install cocoapods`) -- ✅ Certificats Apple configurés (Team ID: 6WT84NWCTC) +### Mac mini (192.168.1.34) +- ✅ Xcode + Command Line Tools +- ✅ Flutter 3.24.5 LTS +- ✅ CocoaPods installé +- ✅ Certificats Apple (Team: **6WT84NWCTC**) -### Sur Debian -- ✅ Accès SSH au Mac mini (192.168.1.34) -- ✅ rsync installé +### PC Debian (développement) +- ✅ Accès SSH au Mac mini +- ✅ Fichier `../VERSION` à jour --- -## 🚀 **Procédure complète** +## 🚀 Build iOS - Workflow complet -### **Étape 1 : Transfert depuis Debian vers Mac mini** +### **Commande unique depuis Debian** ```bash -# Sur votre machine Debian cd /home/pierre/dev/geosector/app - -# Lancer le transfert -./transfer-to-mac.sh +./ios.sh ``` **Ce que fait le script** : -1. Détecte automatiquement la version (ex: 342) -2. Crée le dossier `app_342` sur le Mac mini -3. Transfert tous les fichiers nécessaires (lib, ios, pubspec.yaml, etc.) -4. Exclut les dossiers inutiles (build, .dart_tool, Pods, etc.) - -**Durée** : 2-5 minutes (selon la connexion réseau) - -**Note** : Vous devrez saisir le mot de passe du Mac mini +1. ✅ Lit `../VERSION` (ex: 3.5.3) +2. ✅ Met à jour `pubspec.yaml` (3.5.3+353) +3. ✅ Teste connexion Mac mini +4. ✅ Transfert rsync → `/Users/pierre/dev/geosector/app_353/` +5. 🔀 **Choix A** : Lance build SSH automatique +6. 🔀 **Choix B** : Instructions manuelles --- -### **Étape 2 : Connexion au Mac mini** +### **Option A : Build automatique (recommandé)** + +Sélectionner **A** dans le menu : +- SSH automatique vers Mac mini +- Lance `ios-build-mac.sh` +- Ouvre Xcode pour l'archive + +### **Option B : Build manuel** ```bash -# Depuis Debian ssh pierre@192.168.1.34 - -# Aller dans le dossier transféré -cd /Users/pierre/dev/geosector/app_342 -``` - ---- - -### **Étape 3 : Lancer le build iOS** - -```bash -# Sur le Mac mini +cd /Users/pierre/dev/geosector/app_353 ./ios-build-mac.sh ``` -**Ce que fait le script** : -1. ✅ Nettoie le projet (`flutter clean`) -2. ✅ Récupère les dépendances (`flutter pub get`) -3. ✅ Installe les pods (`pod install`) -4. ✅ Compile en release (`flutter build ios --release`) -5. ✅ Ouvre Xcode pour l'archive (signature manuelle plus fiable) +--- -**Durée de préparation** : 5-10 minutes +## 📦 Archive et Upload (Xcode) -**Résultat** : Xcode s'ouvre, prêt pour Product > Archive +**Xcode s'ouvre automatiquement** après le build ✅ + +1. ⏳ Attendre chargement Xcode +2. ✅ Vérifier **Signing & Capabilities** + - Team : `6WT84NWCTC` + - "Automatically manage signing" : ✅ +3. 🧹 **Product > Clean Build Folder** (⌘⇧K) +4. 📦 **Product > Archive** (⏳ 5-10 min) +5. 📤 **Organizer** → **Distribute App** +6. ☁️ **App Store Connect** → **Upload** +7. ✅ **Upload** (⏳ 2-5 min) --- -### **Étape 4 : Créer l'archive et upload vers App Store Connect** +## 📱 TestFlight (App Store Connect) -**Xcode est ouvert automatiquement** ✅ +https://appstoreconnect.apple.com -Dans Xcode : -1. ⏳ Attendre le chargement (quelques secondes) -2. ✅ Vérifier **Signing & Capabilities** : Team = 6WT84NWCTC, "Automatically manage signing" coché -3. 🧹 **Product > Clean Build Folder** (Cmd+Shift+K) -4. 📦 **Product > Archive** -5. ⏳ Attendre l'archive (5-10 minutes) -6. 📤 **Organizer** s'ouvre → Clic **Distribute App** -7. ☁️ Choisir **App Store Connect** -8. ✅ **Upload** → Automatique -9. 🚀 **Next** jusqu'à validation finale - -**⚠️ Ne PAS utiliser xcodebuild en ligne de commande** : erreurs de signature (errSecInternalComponent). Xcode GUI obligatoire. +1. **Apps** > **GeoSector** > **TestFlight** +2. ⏳ Attendre traitement (5-15 min) +3. Build **353 (3.5.3)** apparaît +4. **Conformité export** : + - Utilise chiffrement ? → **Oui** + - Algorithmes exempts ? → **Aucun des algorithmes mentionnés** +5. **Testeurs internes** → Ajouter ton Apple ID +6. 📧 Invitation TestFlight envoyée --- -## 📁 **Structure des dossiers sur Mac mini** +## ✅ Checklist rapide -``` -/Users/pierre/dev/geosector/ -├── app_342/ # Version 3.4.2 (Build 342) -│ ├── ios/ -│ ├── lib/ -│ ├── pubspec.yaml -│ ├── ios-build-mac.sh # Script de build -│ └── build/ -│ └── Runner.xcarchive # Archive générée -├── app_341/ # Version précédente (si existe) -└── app_343/ # Version future -``` - -**Avantage** : Garder plusieurs versions côte à côte pour tests/rollback +- [ ] Mettre à jour `../VERSION` (ex: 3.5.4) +- [ ] Lancer `./ios.sh` depuis Debian +- [ ] Archive créée dans Xcode +- [ ] Upload vers App Store Connect +- [ ] Conformité export renseignée +- [ ] Testeur interne ajouté +- [ ] App installée via TestFlight --- -## 🔧 **Résolution de problèmes** +## 🔧 Résolution problèmes -### **Erreur : "Flutter not found"** +### Erreur SSH "Too many authentication failures" +✅ **Corrigé** : Le script force l'authentification par mot de passe -```bash -# Vérifier que Flutter est dans le PATH -echo $PATH | grep flutter - -# Ajouter Flutter au PATH (dans ~/.zshrc ou ~/.bash_profile) -export PATH="$PATH:/opt/flutter/bin" -source ~/.zshrc +### Erreur de signature Xcode +``` +Signing & Capabilities > Team = 6WT84NWCTC +"Automatically manage signing" ✅ ``` -### **Erreur : "xcodebuild not found"** - +### Pod install échoue ```bash -# Installer Xcode Command Line Tools -xcode-select --install -``` - -### **Erreur lors de pod install** - -```bash -# Sur le Mac mini cd ios rm -rf Pods Podfile.lock pod install --repo-update -cd .. -``` - -### **Erreur de signature** - -1. Ouvrir Xcode : `open ios/Runner.xcworkspace` -2. Sélectionner le target "Runner" -3. Onglet "Signing & Capabilities" -4. Vérifier Team ID : `6WT84NWCTC` -5. Cocher "Automatically manage signing" - -### **Archive créée mais vide** - -Vérifier que la compilation iOS a réussi : -```bash -flutter build ios --release --no-codesign --verbose ``` --- -## 📊 **Checklist de validation** +## 🎯 Workflow version complète -- [ ] Version/Build incrémenté dans `pubspec.yaml` -- [ ] Compilation iOS réussie -- [ ] Archive validée dans Xcode Organizer -- [ ] Build uploadé vers App Store Connect -- [ ] **TestFlight** : Ajouter build au groupe "Testeurs externes" -- [ ] Renseigner "Infos sur l'exportation de conformité" : - - **App utilise chiffrement ?** → Oui - - **Algorithmes exempts listés ?** → **Aucun des algorithmes mentionnés ci-dessus** - - (App utilise HTTPS standard iOS uniquement) -- [ ] Soumettre build pour révision TestFlight -- [ ] *(Optionnel)* Captures/Release notes pour production App Store - ---- - -## 🎯 **Workflow complet** - -```bash -# 1. Debian → Transfert -cd /home/pierre/dev/geosector/app -./transfer-to-mac.sh - -# 2. Mac mini → Build + Archive -ssh pierre@192.168.1.34 -cd /Users/pierre/dev/geosector/app_342 -./ios-build-mac.sh -# Xcode s'ouvre → Product > Clean + Archive - -# 3. Upload → TestFlight -# Organizer > Distribute App > App Store Connect > Upload -# App Store Connect > TestFlight > Conformité export +```mermaid +Debian (dev) → Mac mini (build) → App Store Connect → TestFlight → iPhone + │ │ │ │ │ + ↓ ↓ ↓ ↓ ↓ + ios.sh build iOS Upload Traitement Install + + Archive (5-15 min) ``` +**Temps total** : 20-30 minutes (build + upload + traitement Apple) + --- -## 📞 **Support** +## 📞 Liens utiles -- **Documentation Apple** : https://developer.apple.com - **App Store Connect** : https://appstoreconnect.apple.com +- **TestFlight** : App dans l'App Store - **Flutter iOS** : https://docs.flutter.dev/deployment/ios --- -✅ **Prêt pour la production !** 🚀 +✅ **Prêt pour TestFlight !** 🚀 diff --git a/app/android.sh b/app/android.sh index d8d3e1d1..3bcaa58f 100755 --- a/app/android.sh +++ b/app/android.sh @@ -57,42 +57,181 @@ if ! command -v flutter &> /dev/null; then exit 1 fi -# Récupérer la version depuis pubspec.yaml -VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //' | sed 's/+/-/') -if [ -z "$VERSION" ]; then - print_error "Impossible de récupérer la version depuis pubspec.yaml" +# Étape 0 : Synchroniser la version depuis ../VERSION +print_message "Étape 0/5 : Synchronisation de la version..." +echo + +VERSION_FILE="../VERSION" +if [ ! -f "$VERSION_FILE" ]; then + print_error "Fichier VERSION introuvable : $VERSION_FILE" exit 1 fi -# Extraire le version code -VERSION_CODE=$(echo $VERSION | cut -d'-' -f2) +# Lire la version depuis le fichier (enlever espaces/retours à la ligne) +VERSION_NUMBER=$(cat "$VERSION_FILE" | tr -d '\n\r ' | tr -d '[:space:]') +if [ -z "$VERSION_NUMBER" ]; then + print_error "Le fichier VERSION est vide" + exit 1 +fi + +print_message "Version lue depuis $VERSION_FILE : $VERSION_NUMBER" + +# Calculer le versionCode (supprimer les points) +VERSION_CODE=$(echo $VERSION_NUMBER | tr -d '.') if [ -z "$VERSION_CODE" ]; then - print_error "Impossible d'extraire le version code" + print_error "Impossible de calculer le versionCode" exit 1 fi -print_message "Version détectée : $VERSION" +print_message "Version code calculé : $VERSION_CODE" + +# Mettre à jour pubspec.yaml +print_message "Mise à jour de pubspec.yaml..." +sed -i.bak "s/^version:.*/version: $VERSION_NUMBER+$VERSION_CODE/" pubspec.yaml + +# Vérifier que la mise à jour a réussi +UPDATED_VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //') +if [ "$UPDATED_VERSION" != "$VERSION_NUMBER+$VERSION_CODE" ]; then + print_error "Échec de la mise à jour de pubspec.yaml" + print_error "Attendu : $VERSION_NUMBER+$VERSION_CODE" + print_error "Obtenu : $UPDATED_VERSION" + exit 1 +fi + +print_success "pubspec.yaml mis à jour : version: $VERSION_NUMBER+$VERSION_CODE" +print_message "build.gradle.kts se synchronisera automatiquement via Flutter Gradle Plugin" +echo + +# Récupérer la version finale pour l'affichage +VERSION="$VERSION_NUMBER-$VERSION_CODE" +print_message "Version finale : $VERSION" print_message "Version code : $VERSION_CODE" echo -# Vérifier la présence du keystore -if [ ! -f "android/app/geosector2025.jks" ]; then - print_error "Fichier keystore introuvable : android/app/geosector2025.jks" - exit 1 -fi - -# Vérifier la présence du fichier key.properties -if [ ! -f "android/key.properties" ]; then - print_error "Fichier key.properties introuvable" - print_error "Ce fichier est nécessaire pour signer l'application" - exit 1 -fi - -print_success "Configuration de signature vérifiée" +# Demander le mode Debug ou Release +print_message "=========================================" +print_message " MODE DE BUILD" +print_message "=========================================" +echo +print_message "Choisissez le mode de build :" +echo +print_message " ${YELLOW}[D]${NC} Debug" +print_message " ✓ Installation rapide via ADB" +print_message " ✓ Hot reload possible" +print_message " ✓ Logs complets" +print_message " ⚠ Tap to Pay simulé uniquement" +print_message " ⚠ Performance non optimisée" +echo +print_message " ${GREEN}[R]${NC} Release (recommandé)" +print_message " ✓ APK/AAB optimisé" +print_message " ✓ Tap to Pay réel en production" +print_message " ✓ Performance maximale" +echo +read -p "Votre choix (D/R) [défaut: R] : " -n 1 -r BUILD_TYPE +echo echo +# Définir le flag de build et le suffixe pour les noms de fichiers +BUILD_MODE_FLAG="--release" +MODE_SUFFIX="release" +SKIP_R8_CHOICE=false + +if [[ $BUILD_TYPE =~ ^[Dd]$ ]]; then + BUILD_MODE_FLAG="--debug" + MODE_SUFFIX="debug" + SKIP_R8_CHOICE=true + print_success "Mode Debug sélectionné" + echo + print_warning "Attention : Tap to Pay ne fonctionnera qu'en mode simulé" + echo + + # En mode debug, pas de choix R8 ni de vérification keystore + USE_R8=false + COPY_DEBUG_FILES=false +else + print_success "Mode Release sélectionné" + echo +fi + +# Demander le mode R8 SEULEMENT si Release +if [ "$SKIP_R8_CHOICE" = false ]; then + print_message "=========================================" + print_message " OPTIMISATION RELEASE" + print_message "=========================================" + echo + print_message "Choisissez le niveau d'optimisation :" + echo + print_message " ${GREEN}[A]${NC} Production - R8/ProGuard activé" + print_message " ✓ Taille réduite (~30-40%)" + print_message " ✓ Code obscurci (sécurité)" + print_message " ✓ Génère mapping.txt pour débogage" + print_message " ✓ Génère symboles natifs" + echo + print_message " ${YELLOW}[B]${NC} Test interne - Sans R8/ProGuard (défaut)" + print_message " ✓ Build plus rapide" + print_message " ✓ Pas d'obscurcissement (débogage facile)" + print_message " ⚠ Taille plus importante" + print_message " ⚠ Avertissements Google Play Console" + echo + read -p "Votre choix (A/B) [défaut: B] : " -n 1 -r BUILD_MODE + echo + echo + + # Définir les variables selon le choix + USE_R8=false + COPY_DEBUG_FILES=false + + if [[ $BUILD_MODE =~ ^[Aa]$ ]]; then + USE_R8=true + COPY_DEBUG_FILES=true + print_success "Mode Production sélectionné - R8/ProGuard activé" + else + print_success "Mode Test interne sélectionné - R8/ProGuard désactivé" + fi + echo +fi + +# Vérifier la présence du keystore SEULEMENT si Release +if [ "$SKIP_R8_CHOICE" = false ]; then + if [ ! -f "android/app/geosector2025.jks" ]; then + print_error "Fichier keystore introuvable : android/app/geosector2025.jks" + exit 1 + fi + + # Vérifier la présence du fichier key.properties + if [ ! -f "android/key.properties" ]; then + print_error "Fichier key.properties introuvable" + print_error "Ce fichier est nécessaire pour signer l'application" + exit 1 + fi + + print_success "Configuration de signature vérifiée" + echo +fi + +# Activer R8 si demandé (modification temporaire du build.gradle.kts) +GRADLE_FILE="android/app/build.gradle.kts" +GRADLE_BACKUP="android/app/build.gradle.kts.backup" + +if [ "$USE_R8" = true ]; then + print_message "Activation de R8/ProGuard dans build.gradle.kts..." + + # Créer une sauvegarde + cp "$GRADLE_FILE" "$GRADLE_BACKUP" + + # Activer minifyEnabled et shrinkResources + sed -i.tmp 's/isMinifyEnabled = false/isMinifyEnabled = true/' "$GRADLE_FILE" + sed -i.tmp 's/isShrinkResources = false/isShrinkResources = true/' "$GRADLE_FILE" + + # Nettoyer les fichiers temporaires de sed + rm -f "${GRADLE_FILE}.tmp" + + print_success "R8/ProGuard activé temporairement" + echo +fi + # Étape 1 : Nettoyer le projet -print_message "Étape 1/4 : Nettoyage du projet..." +print_message "Étape 1/5 : Nettoyage du projet..." flutter clean if [ $? -eq 0 ]; then print_success "Projet nettoyé" @@ -103,7 +242,7 @@ fi echo # Étape 2 : Récupérer les dépendances -print_message "Étape 2/4 : Récupération des dépendances..." +print_message "Étape 2/5 : Récupération des dépendances..." flutter pub get if [ $? -eq 0 ]; then print_success "Dépendances récupérées" @@ -114,7 +253,7 @@ fi echo # Étape 3 : Analyser le code (optionnel mais recommandé) -print_message "Étape 3/4 : Analyse du code Dart..." +print_message "Étape 3/5 : Analyse du code Dart..." flutter analyze --no-fatal-infos --no-fatal-warnings || { print_warning "Des avertissements ont été détectés dans le code" read -p "Voulez-vous continuer malgré les avertissements ? (y/n) " -n 1 -r @@ -128,9 +267,9 @@ print_success "Analyse du code terminée" echo # Étape 4 : Générer le bundle -print_message "Étape 4/4 : Génération du bundle Android..." +print_message "Étape 4/5 : Génération du bundle Android..." print_message "Cette opération peut prendre plusieurs minutes..." -flutter build appbundle --release +flutter build appbundle $BUILD_MODE_FLAG if [ $? -eq 0 ]; then print_success "Bundle généré avec succès" else @@ -139,21 +278,29 @@ else fi echo +# Restaurer le build.gradle.kts original si modifié +if [ "$USE_R8" = true ] && [ -f "$GRADLE_BACKUP" ]; then + print_message "Restauration du build.gradle.kts original..." + mv "$GRADLE_BACKUP" "$GRADLE_FILE" + print_success "Fichier restauré" + echo +fi + # Vérifier que le bundle a été créé -BUNDLE_PATH="build/app/outputs/bundle/release/app-release.aab" +BUNDLE_PATH="build/app/outputs/bundle/$MODE_SUFFIX/app-$MODE_SUFFIX.aab" if [ ! -f "$BUNDLE_PATH" ]; then print_error "Bundle introuvable à l'emplacement attendu : $BUNDLE_PATH" exit 1 fi # Copier le bundle à la racine avec le nouveau nom -FINAL_NAME="geosector-$VERSION_CODE.aab" +FINAL_NAME="geosector-$VERSION_CODE-$MODE_SUFFIX.aab" print_message "Copie du bundle vers : $FINAL_NAME" cp "$BUNDLE_PATH" "$FINAL_NAME" if [ -f "$FINAL_NAME" ]; then print_success "Bundle copié avec succès" - + # Afficher la taille du fichier FILE_SIZE=$(du -h "$FINAL_NAME" | cut -f1) print_message "Taille du bundle : $FILE_SIZE" @@ -162,6 +309,47 @@ else exit 1 fi +# Copier les fichiers de débogage si Option A sélectionnée +if [ "$COPY_DEBUG_FILES" = true ]; then + echo + print_message "Copie des fichiers de débogage pour Google Play Console..." + + # Créer un dossier de release + RELEASE_DIR="release-$VERSION_CODE" + mkdir -p "$RELEASE_DIR" + + # Copier le bundle + cp "$FINAL_NAME" "$RELEASE_DIR/" + + # Copier le fichier mapping.txt (R8/ProGuard) + MAPPING_FILE="build/app/outputs/mapping/release/mapping.txt" + if [ -f "$MAPPING_FILE" ]; then + cp "$MAPPING_FILE" "$RELEASE_DIR/mapping.txt" + print_success "Fichier mapping.txt copié" + else + print_warning "Fichier mapping.txt introuvable (peut être normal)" + fi + + # Copier les symboles natifs + SYMBOLS_ZIP="build/app/intermediates/merged_native_libs/release/out/lib" + if [ -d "$SYMBOLS_ZIP" ]; then + # Créer une archive des symboles + cd build/app/intermediates/merged_native_libs/release/out + zip -r "../../../../../../$RELEASE_DIR/native-symbols.zip" lib/ + cd - > /dev/null + print_success "Symboles natifs archivés" + else + print_warning "Symboles natifs introuvables (peut être normal)" + fi + + print_success "Fichiers de débogage copiés dans : $RELEASE_DIR/" + echo + print_message "Pour uploader sur Google Play Console :" + print_message "1. Bundle : $RELEASE_DIR/$FINAL_NAME" + print_message "2. Mapping : $RELEASE_DIR/mapping.txt" + print_message "3. Symboles : $RELEASE_DIR/native-symbols.zip" +fi + echo print_message "=========================================" print_success " GÉNÉRATION TERMINÉE AVEC SUCCÈS !" @@ -170,11 +358,37 @@ echo print_message "Bundle généré : ${GREEN}$FINAL_NAME${NC}" print_message "Version : $VERSION" print_message "Chemin : $(pwd)/$FINAL_NAME" -echo -print_message "Prochaines étapes :" -print_message "1. Tester le bundle sur un appareil Android" -print_message "2. Uploader sur Google Play Console" -print_message "3. Soumettre pour review" + +if [ "$BUILD_MODE_FLAG" = "--debug" ]; then + echo + print_message "Mode : ${YELLOW}Debug${NC}" + print_message "⚠ Tap to Pay simulé uniquement" + print_message "✓ Logs complets disponibles" + echo + print_message "Prochaines étapes :" + print_message "1. Installer l'APK sur l'appareil (proposé ci-dessous)" + print_message "2. Tester l'application avec adb logcat" + print_message "3. Pour Tap to Pay réel, relancer en mode Release" +elif [ "$USE_R8" = true ]; then + echo + print_message "Mode : ${GREEN}Release - Production (R8/ProGuard activé)${NC}" + print_message "Dossier release : ${GREEN}$RELEASE_DIR/${NC}" + echo + print_message "Prochaines étapes :" + print_message "1. Tester le bundle sur un appareil Android" + print_message "2. Uploader le bundle sur Google Play Console" + print_message "3. Uploader mapping.txt et native-symbols.zip" + print_message "4. Soumettre pour review" +else + echo + print_message "Mode : ${GREEN}Release${NC} - ${YELLOW}Test interne (R8/ProGuard désactivé)${NC}" + print_warning "Avertissements attendus sur Google Play Console" + echo + print_message "Prochaines étapes :" + print_message "1. Tester le bundle sur un appareil Android" + print_message "2. Uploader sur Google Play Console (test interne)" + print_message "3. Pour production, relancer avec Option A" +fi echo # Optionnel : Générer aussi l'APK @@ -182,18 +396,48 @@ read -p "Voulez-vous aussi générer l'APK pour des tests ? (y/n) " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then print_message "Génération de l'APK..." - flutter build apk --release - + flutter build apk $BUILD_MODE_FLAG + if [ $? -eq 0 ]; then - APK_PATH="build/app/outputs/flutter-apk/app-release.apk" + APK_PATH="build/app/outputs/flutter-apk/app-$MODE_SUFFIX.apk" if [ -f "$APK_PATH" ]; then - APK_NAME="geosector-$VERSION_CODE.apk" + APK_NAME="geosector-$VERSION_CODE-$MODE_SUFFIX.apk" cp "$APK_PATH" "$APK_NAME" print_success "APK généré : $APK_NAME" - + # Afficher la taille de l'APK APK_SIZE=$(du -h "$APK_NAME" | cut -f1) print_message "Taille de l'APK : $APK_SIZE" + + # Si mode Debug, proposer installation automatique + if [ "$BUILD_MODE_FLAG" = "--debug" ]; then + echo + read -p "Installer l'APK debug sur l'appareil connecté ? (y/n) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + print_message "Installation sur l'appareil..." + adb install -r "$APK_NAME" + + if [ $? -eq 0 ]; then + print_success "APK installé avec succès" + + # Proposer de lancer l'app + read -p "Lancer l'application ? (y/n) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + adb shell am start -n fr.geosector.app3/.MainActivity + if [ $? -eq 0 ]; then + print_success "Application lancée" + else + print_warning "Impossible de lancer l'application" + fi + fi + else + print_error "Échec de l'installation" + print_message "Vérifiez qu'un appareil est bien connecté : adb devices" + fi + fi + fi fi else print_warning "Échec de la génération de l'APK (le bundle a été créé avec succès)" diff --git a/app/android/app/build.gradle.kts b/app/android/app/build.gradle.kts index 7b396535..492712d9 100755 --- a/app/android/app/build.gradle.kts +++ b/app/android/app/build.gradle.kts @@ -55,9 +55,13 @@ android { buildTypes { release { - // Optimisations sans ProGuard pour éviter les problèmes - isMinifyEnabled = false - isShrinkResources = false + // Optimisations R8/ProGuard avec règles personnalisées + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) // Configuration de signature if (keystorePropertiesFile.exists()) { diff --git a/app/android/app/build.gradle.kts.backup b/app/android/app/build.gradle.kts.backup new file mode 100755 index 00000000..7b396535 --- /dev/null +++ b/app/android/app/build.gradle.kts.backup @@ -0,0 +1,92 @@ +import java.util.Properties +import java.io.FileInputStream + +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +// Charger les propriétés de signature +val keystorePropertiesFile = rootProject.file("key.properties") +val keystoreProperties = Properties() +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) +} + +android { + namespace = "fr.geosector.app3" + compileSdk = 35 // Requis par plusieurs plugins (flutter_local_notifications, stripe, etc.) + ndkVersion = "27.0.12077973" + + compileOptions { + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + kotlinOptions { + jvmTarget = "21" + } + + defaultConfig { + // Application ID for Google Play Store + applicationId = "fr.geosector.app3" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + // Minimum SDK 28 requis pour Stripe Tap to Pay + minSdk = 28 + targetSdk = 35 // API 35 requise par Google Play (Oct 2024+) + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + signingConfigs { + if (keystorePropertiesFile.exists()) { + create("release") { + keyAlias = keystoreProperties["keyAlias"] as String + keyPassword = keystoreProperties["keyPassword"] as String + storeFile = file(keystoreProperties["storeFile"] as String) + storePassword = keystoreProperties["storePassword"] as String + } + } + } + + buildTypes { + release { + // Optimisations sans ProGuard pour éviter les problèmes + isMinifyEnabled = false + isShrinkResources = false + + // Configuration de signature + if (keystorePropertiesFile.exists()) { + signingConfig = signingConfigs.getByName("release") + } else { + signingConfig = signingConfigs.getByName("debug") + } + } + + debug { + // Mode debug pour le développement + isDebuggable = true + applicationIdSuffix = ".debug" + versionNameSuffix = "-DEBUG" + } + } + + // Résolution des conflits de fichiers dupliqués (Stripe + BouncyCastle) + packaging { + resources { + pickFirst("org/bouncycastle/**") + } + } +} + +flutter { + source = "../.." +} + +dependencies { + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4") +} diff --git a/app/android/app/proguard-rules.pro b/app/android/app/proguard-rules.pro new file mode 100644 index 00000000..3680037a --- /dev/null +++ b/app/android/app/proguard-rules.pro @@ -0,0 +1,57 @@ +# Règles ProGuard/R8 pour GEOSECTOR +# ===================================== + +## Règles générées automatiquement par R8 (classes manquantes) +## Ces classes Java ne sont pas disponibles sur Android mais ne sont pas utilisées +-dontwarn java.beans.ConstructorProperties +-dontwarn java.beans.Transient +-dontwarn org.slf4j.impl.StaticLoggerBinder +-dontwarn org.slf4j.impl.StaticMDCBinder + +## Règles pour Google Play Core (composants différés - non utilisés) +-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication +-dontwarn com.google.android.play.core.splitinstall.SplitInstallException +-dontwarn com.google.android.play.core.splitinstall.SplitInstallManager +-dontwarn com.google.android.play.core.splitinstall.SplitInstallManagerFactory +-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest$Builder +-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest +-dontwarn com.google.android.play.core.splitinstall.SplitInstallSessionState +-dontwarn com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener +-dontwarn com.google.android.play.core.tasks.OnFailureListener +-dontwarn com.google.android.play.core.tasks.OnSuccessListener +-dontwarn com.google.android.play.core.tasks.Task + +## Règles pour Stripe SDK +-keep class com.stripe.** { *; } +-keepclassmembers class com.stripe.** { *; } + +## Règles pour Jackson (utilisé par Stripe) +-keep class com.fasterxml.jackson.** { *; } +-keepclassmembers class com.fasterxml.jackson.** { *; } +-dontwarn com.fasterxml.jackson.databind.** + +## Règles pour les modèles de données (Hive) +-keep class fr.geosector.app3.** { *; } +-keepclassmembers class fr.geosector.app3.** { *; } + +## Règles pour les réflexions Flutter +-keep class io.flutter.app.** { *; } +-keep class io.flutter.plugin.** { *; } +-keep class io.flutter.util.** { *; } +-keep class io.flutter.view.** { *; } +-keep class io.flutter.** { *; } +-keep class io.flutter.plugins.** { *; } + +## Règles pour les annotations +-keepattributes *Annotation* +-keepattributes Signature +-keepattributes Exceptions +-keepattributes InnerClasses +-keepattributes EnclosingMethod + +## Optimisation +-optimizationpasses 5 +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-dontpreverify +-verbose diff --git a/app/assets/images/geosector-1024x500.png b/app/assets/images/geosector-1024x500.png new file mode 100644 index 00000000..26eda9d5 Binary files /dev/null and b/app/assets/images/geosector-1024x500.png differ diff --git a/app/assets/images/geosector-admin-amicale-1800x1800.png b/app/assets/images/geosector-admin-amicale-1800x1800.png new file mode 100644 index 00000000..fdea9b15 Binary files /dev/null and b/app/assets/images/geosector-admin-amicale-1800x1800.png differ diff --git a/app/assets/images/geosector-admin-tbord-1800x1800.png b/app/assets/images/geosector-admin-tbord-1800x1800.png new file mode 100644 index 00000000..6b872fc0 Binary files /dev/null and b/app/assets/images/geosector-admin-tbord-1800x1800.png differ diff --git a/app/assets/images/geosector-user-carte-1800x1800.png b/app/assets/images/geosector-user-carte-1800x1800.png new file mode 100644 index 00000000..f699dfa2 Binary files /dev/null and b/app/assets/images/geosector-user-carte-1800x1800.png differ diff --git a/app/assets/images/geosector-user-histo-1800x1800.png b/app/assets/images/geosector-user-histo-1800x1800.png new file mode 100644 index 00000000..16e8a509 Binary files /dev/null and b/app/assets/images/geosector-user-histo-1800x1800.png differ diff --git a/app/assets/images/geosector-user-login-1800x1800.png b/app/assets/images/geosector-user-login-1800x1800.png new file mode 100644 index 00000000..2dee7e95 Binary files /dev/null and b/app/assets/images/geosector-user-login-1800x1800.png differ diff --git a/app/assets/images/geosector-user-stripe-1800x1800.png b/app/assets/images/geosector-user-stripe-1800x1800.png new file mode 100644 index 00000000..90c7b41f Binary files /dev/null and b/app/assets/images/geosector-user-stripe-1800x1800.png differ diff --git a/app/assets/images/geosector-user-tbord-1800x1800.png b/app/assets/images/geosector-user-tbord-1800x1800.png new file mode 100644 index 00000000..16d1ec17 Binary files /dev/null and b/app/assets/images/geosector-user-tbord-1800x1800.png differ diff --git a/app/deploy-app.sh b/app/deploy-app.sh index f0f1c13b..e08b5b2d 100755 --- a/app/deploy-app.sh +++ b/app/deploy-app.sh @@ -41,7 +41,7 @@ FINAL_OWNER="nginx" FINAL_GROUP="nginx" # Configuration de sauvegarde -BACKUP_DIR="/data/backup/geosector" +BACKUP_DIR="/home/pierre/samba/back/geosector/app/" # Couleurs pour les messages GREEN='\033[0;32m' @@ -549,4 +549,4 @@ echo_info "[$(date '+%H:%M:%S.%3N')] Fin du script" echo_step "⏱️ TEMPS TOTAL D'EXÉCUTION: ${TOTAL_TIME} ms ($((TOTAL_TIME/1000)) secondes)" # Journaliser le déploiement -echo "$(date '+%Y-%m-%d %H:%M:%S') - Flutter app deployed to ${ENV_NAME} (${DEST_CONTAINER}) - Total: ${TOTAL_TIME}ms" >> ~/.geo_deploy_history \ No newline at end of file +echo "$(date '+%Y-%m-%d %H:%M:%S') - Flutter app deployed to ${ENV_NAME} (${DEST_CONTAINER}) - Total: ${TOTAL_TIME}ms" >> ~/.geo_deploy_history diff --git a/app/deploy-ios-full-auto.sh b/app/deploy-ios-full-auto.sh new file mode 100755 index 00000000..ce3aeb2c --- /dev/null +++ b/app/deploy-ios-full-auto.sh @@ -0,0 +1,498 @@ +#!/bin/bash + +# Script de déploiement iOS automatisé pour GEOSECTOR +# Version: 1.0 +# Date: 2025-12-05 +# Auteur: Pierre (avec l'aide de Claude) +# +# Usage: +# ./deploy-ios-full-auto.sh # Utilise ../VERSION +# ./deploy-ios-full-auto.sh 3.6.0 # Version spécifique +# ./deploy-ios-full-auto.sh 3.6.0 --skip-build # Skip Flutter build si déjà fait + +set -euo pipefail + +# ===================================== +# Configuration +# ===================================== + +# Couleurs pour les messages +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Configuration Mac mini +MAC_MINI_HOST="minipi4" # Nom défini dans ~/.ssh/config +MAC_BASE_DIR="/Users/pierre/dev/geosector" + +# Timestamp pour logs et archives +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +LOG_FILE="./logs/deploy-ios-${TIMESTAMP}.log" +mkdir -p ./logs + +# Variables globales pour le rapport +STEP_START_TIME=0 +TOTAL_START_TIME=$(date +%s) +ERRORS_COUNT=0 +WARNINGS_COUNT=0 + +# ===================================== +# Fonctions utilitaires +# ===================================== + +log() { + echo -e "$1" | tee -a "${LOG_FILE}" +} + +log_step() { + STEP_START_TIME=$(date +%s) + log "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + log "${CYAN}▶ $1${NC}" + log "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +log_substep() { + log "${MAGENTA} ➜ $1${NC}" +} + +log_info() { + log "${BLUE}ℹ ${NC}$1" +} + +log_success() { + local elapsed=$(($(date +%s) - STEP_START_TIME)) + log "${GREEN}✓${NC} $1 ${CYAN}(${elapsed}s)${NC}" +} + +log_warning() { + ((WARNINGS_COUNT++)) + log "${YELLOW}⚠ ${NC}$1" +} + +log_error() { + ((ERRORS_COUNT++)) + log "${RED}✗${NC} $1" +} + +log_fatal() { + log_error "$1" + log "\n${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + log "${RED}DÉPLOIEMENT ÉCHOUÉ${NC}" + log "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + log_error "Consultez le log: ${LOG_FILE}" + exit 1 +} + +# Fonction pour exécuter une commande et capturer les erreurs +safe_exec() { + local cmd="$1" + local error_msg="$2" + + if ! eval "$cmd" >> "${LOG_FILE}" 2>&1; then + log_fatal "$error_msg" + fi +} + +# Fonction pour exécuter une commande SSH avec gestion d'erreurs +ssh_exec() { + local cmd="$1" + local error_msg="$2" + + if ! ssh "$MAC_MINI_HOST" "$cmd" >> "${LOG_FILE}" 2>&1; then + log_fatal "$error_msg" + fi +} + +# ===================================== +# En-tête +# ===================================== + +clear +log "${BLUE}╔════════════════════════════════════════════════════════╗${NC}" +log "${BLUE}║ ║${NC}" +log "${BLUE}║ ${GREEN}🍎 DÉPLOIEMENT iOS AUTOMATISÉ${BLUE} ║${NC}" +log "${BLUE}║ ${CYAN}GEOSECTOR - Full Automation${BLUE} ║${NC}" +log "${BLUE}║ ║${NC}" +log "${BLUE}╚════════════════════════════════════════════════════════╝${NC}" +log "" +log_info "Démarrage: $(date '+%Y-%m-%d %H:%M:%S')" +log_info "Log file: ${LOG_FILE}" +log "" + +# ===================================== +# Étape 1 : Gestion de la version +# ===================================== + +log_step "ÉTAPE 1/8 : Gestion de la version" + +# Déterminer la version à utiliser +if [ "${1:-}" != "" ] && [[ ! "${1}" =~ ^-- ]]; then + VERSION="$1" + log_info "Version fournie en argument: ${VERSION}" +else + # Lire depuis ../VERSION + if [ ! -f ../VERSION ]; then + log_fatal "Fichier ../VERSION introuvable et aucune version fournie" + fi + VERSION=$(cat ../VERSION | tr -d '\n\r ' | tr -d '[:space:]') + log_info "Version lue depuis ../VERSION: ${VERSION}" +fi + +# Vérifier le format de version +if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + log_fatal "Format de version invalide: ${VERSION} (attendu: x.x.x)" +fi + +# Calculer le build number +BUILD_NUMBER=$(echo $VERSION | tr -d '.') +FULL_VERSION="${VERSION}+${BUILD_NUMBER}" + +log_success "Version configurée" +log_info " Version name: ${GREEN}${VERSION}${NC}" +log_info " Build number: ${GREEN}${BUILD_NUMBER}${NC}" +log_info " Full version: ${GREEN}${FULL_VERSION}${NC}" + +# ===================================== +# Étape 2 : Mise à jour pubspec.yaml +# ===================================== + +log_step "ÉTAPE 2/8 : Mise à jour pubspec.yaml" + +# Backup du pubspec.yaml +cp pubspec.yaml pubspec.yaml.backup + +# Mise à jour de la version +sed -i "s/^version: .*/version: $FULL_VERSION/" pubspec.yaml + +# Vérifier la mise à jour +UPDATED_VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //' | tr -d ' ') +if [ "$UPDATED_VERSION" != "$FULL_VERSION" ]; then + log_fatal "Échec de la mise à jour de pubspec.yaml (attendu: $FULL_VERSION, obtenu: $UPDATED_VERSION)" +fi + +log_success "pubspec.yaml mis à jour" + +# ===================================== +# Étape 3 : Préparation du projet +# ===================================== + +SKIP_BUILD=false +if [[ "${2:-}" == "--skip-build" ]]; then + SKIP_BUILD=true + log_warning "Mode --skip-build activé, Flutter build sera ignoré" +fi + +if [ "$SKIP_BUILD" = false ]; then + log_step "ÉTAPE 3/8 : Préparation du projet Flutter" + + log_substep "Configuration du cache local" + export PUB_CACHE="$PWD/.pub-cache-local" + export GRADLE_USER_HOME="$PWD/.gradle-local" + mkdir -p "$PUB_CACHE" "$GRADLE_USER_HOME" + log_info " Cache Pub: $PUB_CACHE" + log_info " Cache Gradle: $GRADLE_USER_HOME" + + log_substep "Nettoyage du projet" + safe_exec "flutter clean" "Échec du nettoyage Flutter" + + log_substep "Récupération des dépendances" + safe_exec "flutter pub get" "Échec de flutter pub get" + + log_substep "Application du patch nfc_manager" + safe_exec "./fastlane/scripts/commun/fix-nfc-manager.sh" "Échec du patch nfc_manager" + + log_substep "Application du patch permission_handler (si nécessaire)" + if [ -f "./fastlane/scripts/commun/fix-permission-handler.sh" ]; then + safe_exec "./fastlane/scripts/commun/fix-permission-handler.sh" "Échec du patch permission_handler" + fi + + log_substep "Génération des fichiers Hive" + safe_exec "dart run build_runner build --delete-conflicting-outputs" "Échec de la génération de code" + + log_success "Projet préparé (dépendances + patchs + génération de code)" + log_info " ⚠️ Build iOS sera fait sur le Mac mini via Fastlane" +else + log_step "ÉTAPE 3/8 : Préparation du projet (BUILD SKIPPED)" + + log_substep "Configuration du cache local uniquement" + export PUB_CACHE="$PWD/.pub-cache-local" + export GRADLE_USER_HOME="$PWD/.gradle-local" + + if [ ! -d "$PUB_CACHE" ]; then + log_warning "Cache local introuvable, le build pourrait échouer sur le Mac mini" + fi + + log_success "Cache configuré (build Flutter ignoré)" +fi + +# ===================================== +# Étape 4 : Vérification de la connexion Mac mini +# ===================================== + +log_step "ÉTAPE 4/8 : Connexion au Mac mini" + +log_substep "Test de connexion SSH à ${MAC_MINI_HOST}" + +if ! ssh "$MAC_MINI_HOST" "echo 'Connection OK'" >> "${LOG_FILE}" 2>&1; then + log_fatal "Impossible de se connecter au Mac mini (${MAC_MINI_HOST})" +fi + +log_success "Connexion SSH établie" + +# Vérifier l'environnement Mac +log_substep "Vérification de l'environnement Mac" +MAC_INFO=$(ssh "$MAC_MINI_HOST" "sw_vers -productVersion && xcodebuild -version | head -1 && flutter --version | head -1" 2>/dev/null || echo "N/A") +log_info "$(echo "$MAC_INFO" | head -1 | xargs -I {} echo " macOS: {}")" +log_info "$(echo "$MAC_INFO" | sed -n '2p' | xargs -I {} echo " Xcode: {}")" +log_info "$(echo "$MAC_INFO" | sed -n '3p' | xargs -I {} echo " Flutter: {}")" + +# ===================================== +# Étape 5 : Transfert vers Mac mini +# ===================================== + +log_step "ÉTAPE 5/8 : Transfert du projet vers Mac mini" + +DEST_DIR="${MAC_BASE_DIR}/app_${BUILD_NUMBER}" + +log_substep "Création du dossier de destination: ${DEST_DIR}" +ssh_exec "mkdir -p ${DEST_DIR}" "Impossible de créer le dossier ${DEST_DIR} sur le Mac mini" + +log_substep "Transfert rsync (peut prendre 2-5 minutes)" +TRANSFER_START=$(date +%s) + +rsync -avz --progress \ + --exclude='build/' \ + --exclude='.dart_tool/' \ + --exclude='ios/Pods/' \ + --exclude='ios/.symlinks/' \ + --exclude='macos/Pods/' \ + --exclude='linux/flutter/ephemeral/' \ + --exclude='windows/flutter/ephemeral/' \ + --exclude='android/build/' \ + --exclude='*.aab' \ + --exclude='*.apk' \ + --exclude='logs/' \ + --exclude='*.log' \ + ./ "${MAC_MINI_HOST}:${DEST_DIR}/" >> "${LOG_FILE}" 2>&1 || log_fatal "Échec du transfert rsync" + +TRANSFER_TIME=$(($(date +%s) - TRANSFER_START)) + +log_success "Transfert terminé" +log_info " Destination: ${DEST_DIR}" +log_info " Durée: ${TRANSFER_TIME}s" + +# ===================================== +# Étape 6 : Build et Archive avec Fastlane +# ===================================== + +log_step "ÉTAPE 6/8 : Build et Archive iOS avec Fastlane" + +log_info "Cette étape peut prendre 15-25 minutes" +log_info "Fastlane va :" +log_info " 1. Nettoyer les artefacts" +log_info " 2. Installer les CocoaPods" +log_info " 3. Analyser le code" +log_info " 4. Build Flutter iOS" +log_info " 5. Archive Xcode (gym)" +log_info " 6. Export IPA" +log_info "" +log_substep "Lancement de: cd ${DEST_DIR} && fastlane ios build" + +FASTLANE_START=$(date +%s) + +# Créer un fichier temporaire pour capturer la sortie Fastlane +FASTLANE_LOG="/tmp/fastlane-ios-${TIMESTAMP}.log" + +# Exécuter Fastlane en temps réel avec affichage des étapes importantes +ssh -t "$MAC_MINI_HOST" "cd ${DEST_DIR} && /opt/homebrew/bin/fastlane ios build" 2>&1 | tee -a "${LOG_FILE}" | tee "${FASTLANE_LOG}" | while IFS= read -r line; do + # Afficher les lignes importantes + if echo "$line" | grep -qE "(🧹|📦|🔧|🔍|🏗️|✓|✗|Error|error:|ERROR|Build succeeded|Build failed)"; then + echo -e "${CYAN} ${line}${NC}" + fi +done + +# Vérifier le code de retour de Fastlane +FASTLANE_EXIT_CODE=${PIPESTATUS[0]} +FASTLANE_TIME=$(($(date +%s) - FASTLANE_START)) + +if [ $FASTLANE_EXIT_CODE -ne 0 ]; then + log_error "Fastlane a échoué (code: ${FASTLANE_EXIT_CODE})" + log_error "Analyse des erreurs..." + + # Extraire les erreurs du log Fastlane + if [ -f "${FASTLANE_LOG}" ]; then + log "" + log "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + log "${RED}ERREURS DÉTECTÉES :${NC}" + log "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + grep -i "error:\|Error:\|ERROR:\|❌\|✗" "${FASTLANE_LOG}" | head -20 | while IFS= read -r error_line; do + log "${RED} ${error_line}${NC}" + done + log "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + fi + + log_fatal "Build iOS échoué via Fastlane. Consultez ${FASTLANE_LOG} pour plus de détails." +fi + +log_success "Build et Archive iOS réussis" +log_info " Durée totale Fastlane: ${FASTLANE_TIME}s ($((FASTLANE_TIME/60))m $((FASTLANE_TIME%60))s)" + +# Vérifier que l'IPA existe +log_substep "Vérification de l'IPA généré" +IPA_EXISTS=$(ssh "$MAC_MINI_HOST" "test -f ${DEST_DIR}/build/ios/ipa/Runner.ipa && echo 'YES' || echo 'NO'") + +if [ "$IPA_EXISTS" != "YES" ]; then + log_fatal "IPA non trouvé dans ${DEST_DIR}/build/ios/ipa/Runner.ipa" +fi + +IPA_SIZE=$(ssh "$MAC_MINI_HOST" "du -h ${DEST_DIR}/build/ios/ipa/Runner.ipa | cut -f1") +log_info " IPA trouvé: ${GREEN}${IPA_SIZE}${NC}" + +# ===================================== +# Étape 7 : Upload vers TestFlight (optionnel) +# ===================================== + +log_step "ÉTAPE 7/8 : Upload vers TestFlight" + +log "" +log_info "${YELLOW}Voulez-vous uploader l'IPA vers TestFlight maintenant ?${NC}" +log_info " [Y] Oui - Upload automatique via fastlane ios upload" +log_info " [N] Non - Je ferai l'upload manuellement plus tard" +log "" + +read -p "$(echo -e ${CYAN}Votre choix [Y/n]: ${NC})" -n 1 -r UPLOAD_CHOICE +echo +log "" + +if [[ $UPLOAD_CHOICE =~ ^[Yy]$ ]] || [ -z "$UPLOAD_CHOICE" ]; then + log_substep "Lancement de: fastlane ios upload" + log_info "Upload vers TestFlight (peut prendre 5-10 minutes)" + + UPLOAD_START=$(date +%s) + + ssh -t "$MAC_MINI_HOST" "cd ${DEST_DIR} && /opt/homebrew/bin/fastlane ios upload" 2>&1 | tee -a "${LOG_FILE}" + UPLOAD_EXIT_CODE=${PIPESTATUS[0]} + UPLOAD_TIME=$(($(date +%s) - UPLOAD_START)) + + if [ $UPLOAD_EXIT_CODE -ne 0 ]; then + log_error "Upload TestFlight échoué (code: ${UPLOAD_EXIT_CODE})" + log_warning "L'IPA est disponible sur le Mac mini, vous pouvez réessayer manuellement" + log_info " Commande: ssh $MAC_USER@$MAC_MINI_IP \"cd ${DEST_DIR} && fastlane ios upload\"" + else + log_success "Upload TestFlight réussi" + log_info " Durée: ${UPLOAD_TIME}s" + log_info " URL: ${CYAN}https://appstoreconnect.apple.com${NC}" + fi +else + log_info "Upload ignoré. Pour uploader manuellement plus tard :" + log_info " ${CYAN}ssh $MAC_MINI_HOST \"cd ${DEST_DIR} && /opt/homebrew/bin/fastlane ios upload\"${NC}" +fi + +# ===================================== +# Étape 8 : Nettoyage et archivage +# ===================================== + +log_step "ÉTAPE 8/8 : Nettoyage et archivage" + +log_substep "Voulez-vous archiver le dossier de build ?" +log_info " [Y] Oui - Créer une archive ${DEST_DIR}.tar.gz" +log_info " [N] Non - Garder le dossier tel quel (défaut)" +log "" + +read -p "$(echo -e ${CYAN}Votre choix [y/N]: ${NC})" -n 1 -r ARCHIVE_CHOICE +echo +log "" + +if [[ $ARCHIVE_CHOICE =~ ^[Yy]$ ]]; then + log_substep "Création de l'archive..." + ssh_exec "cd ${MAC_BASE_DIR} && tar -czf app_${BUILD_NUMBER}.tar.gz app_${BUILD_NUMBER}" \ + "Échec de la création de l'archive" + + ARCHIVE_SIZE=$(ssh "$MAC_MINI_HOST" "du -h ${MAC_BASE_DIR}/app_${BUILD_NUMBER}.tar.gz | cut -f1") + log_success "Archive créée" + log_info " Archive: ${MAC_BASE_DIR}/app_${BUILD_NUMBER}.tar.gz (${ARCHIVE_SIZE})" + + log_substep "Suppression du dossier de build" + ssh_exec "rm -rf ${DEST_DIR}" "Échec de la suppression du dossier" + log_success "Dossier de build supprimé" +else + log_info "Dossier conservé: ${DEST_DIR}" +fi + +# Restaurer le pubspec.yaml original (optionnel) +log_substep "Restauration de pubspec.yaml local" +mv pubspec.yaml.backup pubspec.yaml +log_info " pubspec.yaml local restauré à son état initial" + +# ===================================== +# Rapport final +# ===================================== + +TOTAL_TIME=$(($(date +%s) - TOTAL_START_TIME)) +TOTAL_MINUTES=$((TOTAL_TIME / 60)) +TOTAL_SECONDS=$((TOTAL_TIME % 60)) + +log "" +log "${GREEN}╔════════════════════════════════════════════════════════╗${NC}" +log "${GREEN}║ ║${NC}" +log "${GREEN}║ ✓ DÉPLOIEMENT iOS TERMINÉ AVEC SUCCÈS ║${NC}" +log "${GREEN}║ ║${NC}" +log "${GREEN}╚════════════════════════════════════════════════════════╝${NC}" +log "" +log "${CYAN}📊 RAPPORT DE DÉPLOIEMENT${NC}" +log "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +log "" +log " ${BLUE}Version déployée:${NC} ${GREEN}${VERSION} (Build ${BUILD_NUMBER})${NC}" +log " ${BLUE}Destination:${NC} ${DEST_DIR}" +log " ${BLUE}IPA généré:${NC} ${GREEN}${IPA_SIZE}${NC}" +log " ${BLUE}Durée totale:${NC} ${GREEN}${TOTAL_MINUTES}m ${TOTAL_SECONDS}s${NC}" +log "" +if [ $WARNINGS_COUNT -gt 0 ]; then + log " ${YELLOW}⚠ Avertissements:${NC} ${WARNINGS_COUNT}" +fi +if [ $ERRORS_COUNT -gt 0 ]; then + log " ${RED}✗ Erreurs:${NC} ${ERRORS_COUNT}" +fi +log "" +log "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +log "" +log "${BLUE}📱 PROCHAINES ÉTAPES${NC}" +log "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +log "" + +if [[ $UPLOAD_CHOICE =~ ^[Yy]$ ]] || [ -z "$UPLOAD_CHOICE" ]; then + if [ $UPLOAD_EXIT_CODE -eq 0 ]; then + log " 1. ${GREEN}✓${NC} IPA uploadé sur TestFlight" + log " 2. Accéder à App Store Connect:" + log " ${CYAN}https://appstoreconnect.apple.com${NC}" + log " 3. Attendre le traitement Apple (5-15 min)" + log " 4. Configurer la conformité export si demandée" + log " 5. Ajouter des testeurs internes" + log " 6. Installer via TestFlight sur iPhone" + else + log " 1. ${YELLOW}⚠${NC} Upload TestFlight a échoué" + log " 2. Réessayer manuellement:" + log " ${CYAN}ssh $MAC_USER@$MAC_MINI_IP \"cd ${DEST_DIR} && fastlane ios upload\"${NC}" + fi +else + log " 1. L'IPA est prêt sur le Mac mini" + log " 2. Pour uploader vers TestFlight:" + log " ${CYAN}ssh $MAC_USER@$MAC_MINI_IP \"cd ${DEST_DIR} && fastlane ios upload\"${NC}" + log " 3. Ou distribuer l'IPA manuellement via Xcode" +fi + +log "" +log "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +log "" +log " ${BLUE}Log complet:${NC} ${LOG_FILE}" +log " ${BLUE}Fin:${NC} $(date '+%Y-%m-%d %H:%M:%S')" +log "" + +# Nettoyer le log Fastlane temporaire +rm -f "${FASTLANE_LOG}" + +exit 0 diff --git a/app/docs/FLOW-STRIPE.md b/app/docs/FLOW-STRIPE.md index 1aa142a7..7b7269d3 100644 --- a/app/docs/FLOW-STRIPE.md +++ b/app/docs/FLOW-STRIPE.md @@ -278,55 +278,102 @@ Log::info('Stripe account activated', [ ## 📱 FLOW TAP TO PAY (Application Flutter) +### 🎯 Architecture technique + +Le flow Tap to Pay repose sur trois composants principaux : + +1. **DeviceInfoService** - Vérification de compatibilité hardware (Android SDK ≥ 28, NFC disponible) +2. **StripeTapToPayService** - Gestion du SDK Stripe Terminal et des paiements +3. **Backend API** - Endpoints PHP pour les tokens de connexion et PaymentIntents + ### 🔄 Diagramme de séquence complet ``` -┌─────────────┐ ┌─────────────┐ ┌──────────┐ ┌─────────┐ -│ App Flutter │ │ API PHP │ │ Stripe │ │ Carte │ -└──────┬──────┘ └──────┬──────┘ └────┬─────┘ └────┬────┘ - │ │ │ │ - [1] │ Validation form │ │ │ - │ + montant CB │ │ │ - │ │ │ │ - [2] │ POST/PUT passage │ │ │ - │──────────────────>│ │ │ - │ │ │ │ - [3] │<──────────────────│ │ │ - │ Passage ID: 456 │ │ │ - │ │ │ │ - [4] │ POST create-intent│ │ │ - │──────────────────>│ (avec passage_id: 456) │ - │ │ │ │ - [5] │ │ Create PaymentIntent │ - │ │─────────────────>│ │ - │ │ │ │ - [6] │ │<─────────────────│ │ - │ │ pi_xxx + secret │ │ - │ │ │ │ - [7] │<──────────────────│ │ │ - │ PaymentIntent ID │ │ │ - │ │ │ │ - [8] │ SDK Terminal Init │ │ │ - │ "Approchez carte" │ │ │ - │ │ │ │ - [9] │<──────────────────────────────────────────────────────│ - │ NFC : Lecture carte sans contact │ - │ │ │ │ - [10] │ Process Payment │ │ │ - │───────────────────────────────────>│ │ - │ │ │ │ - [11] │<───────────────────────────────────│ │ - │ Payment Success │ │ - │ │ │ │ - [12] │ POST confirm │ │ │ - │──────────────────>│ │ │ - │ │ │ │ - [13] │ PUT passage/456 │ │ │ - │──────────────────>│ (ajout stripe_payment_id) │ - │ │ │ │ - [14] │<──────────────────│ │ │ - │ Passage updated │ │ │ - │ │ │ │ +┌─────────────┐ ┌─────────────────┐ ┌──────────┐ ┌─────────┐ ┌──────────────┐ +│ App Flutter │ │ DeviceInfo │ │ API │ │ Stripe │ │ SDK Terminal │ +│ │ │ Service │ │ PHP │ │ │ │ │ +└──────┬──────┘ └────────┬────────┘ └────┬─────┘ └────┬─────┘ └──────┬───────┘ + │ │ │ │ │ + [1] │ Login utilisateur │ │ │ │ + │────────────────────>│ │ │ │ + │ │ │ │ │ + [2] │ │ checkStripeCertification() │ │ + │ │ • Android SDK ≥ 28 │ │ + │ │ • NFC disponible │ │ + │ │ │ │ │ + [3] │<────────────────────│ │ │ │ + │ ✅ Compatible │ │ │ │ + │ │ │ │ │ + [4] │ Validation form │ │ │ │ + │ + montant CB │ │ │ │ + │ │ │ │ │ + [5] │ POST/PUT passage │ │ │ │ + │────────────────────────────────────────>│ │ │ + │ │ │ │ │ + [6] │<────────────────────────────────────────│ │ │ + │ Passage ID: 456 │ │ │ │ + │ │ │ │ │ + [7] │ initialize() │ │ │ │ + │────────────────────────────────────────────────────────────────────────────>│ + │ │ │ │ │ + [8] │ │ │ │ Terminal.initTerminal() + │ │ │ │ │ (fetchToken callback) + │ │ │ │ │ + [9] │ │ │ POST /terminal/connection-token │ + │────────────────────────────────────────>│ │ │ + │ {amicale_id, stripe_account, location_id} │ │ + │ │ │ │ │ + [10] │ │ │ CreateConnectionToken │ + │ │ │───────────────>│ │ + │ │ │ │ │ + [11] │ │ │<───────────────│ │ + │ │ │ {secret: "..."}│ │ + │ │ │ │ │ + [12] │<────────────────────────────────────────│ │ │ + │ Connection Token │ │ │ │ + │ │ │ │ │ + [13] │────────────────────────────────────────────────────────────────────────────>│ + │ Token delivered to SDK │ │ ✅ SDK Ready │ + │ │ │ │ │ + [14] │ createPaymentIntent() │ │ │ + │────────────────────────────────────────>│ │ │ + │ {amount, passage_id, amicale_id} │ │ │ + │ │ │ │ │ + [15] │ │ │ Create PaymentIntent │ + │ │ │───────────────>│ │ + │ │ │ │ │ + [16] │ │ │<───────────────│ │ + │ │ │ pi_xxx + secret│ │ + │ │ │ │ │ + [17] │<────────────────────────────────────────│ │ │ + │ PaymentIntent ID │ │ │ │ + │ │ │ │ │ + [18] │ collectPayment() │ │ │ │ + │────────────────────────────────────────────────────────────────────────────>│ + │ │ │ │ │ + [19] │ │ │ │ discoverReaders() + │ │ │ │ + connectReader() + │ │ │ │ │ + [20] │ │ │ │ collectPaymentMethod() + │ 💳 "Présentez la carte" │ │ ⏳ Attente NFC │ + │ │ │ │ │ + [21] │<──────────────────────────────────────────────────────────── 📱 TAP CARTE │ + │ │ │ │ │ + [22] │ confirmPayment() │ │ │ │ + │────────────────────────────────────────────────────────────────────────────>│ + │ │ │ │ │ + [23] │ │ │ │ confirmPaymentIntent() + │ │ │ │ │ + [24] │ │ │ │ ✅ Succeeded │ + │<────────────────────────────────────────────────────────────────────────────│ + │ Payment Success │ │ │ │ + │ │ │ │ │ + [25] │ PUT passage/456 │ │ │ │ + │────────────────────────────────────────>│ │ │ + │ {stripe_payment_id: "pi_xxx"} │ │ │ + │ │ │ │ │ + [26] │<────────────────────────────────────────│ │ │ + │ ✅ Passage updated │ │ │ │ ``` ### 🎮 Gestion du Terminal de Paiement @@ -378,6 +425,59 @@ Le terminal de paiement reste affiché jusqu'à la réponse définitive de Strip - **Persistence** : Le terminal reste ouvert jusqu'à réponse définitive de Stripe - **Gestion d'erreur** : Possibilité de réessayer sans perdre le contexte +### 🔑 Connection Token - Flow détaillé + +Le Connection Token est crucial pour le SDK Stripe Terminal. Il est récupéré via un callback au moment de l'initialisation. + +**Code côté App (stripe_tap_to_pay_service.dart:87-89) :** +```dart +await Terminal.initTerminal( + fetchToken: _fetchConnectionToken, // Callback appelé automatiquement +); +``` + +**Callback de récupération (lignes 137-161) :** +```dart +Future _fetchConnectionToken() async { + debugPrint('🔑 Récupération du token de connexion Stripe...'); + + final response = await ApiService.instance.post( + '/stripe/terminal/connection-token', + data: { + 'amicale_id': CurrentAmicaleService.instance.amicaleId, + 'stripe_account': _stripeAccountId, + 'location_id': _locationId, + } + ); + + final token = response.data['secret']; + if (token == null || token.isEmpty) { + throw Exception('Token de connexion invalide'); + } + + debugPrint('✅ Token de connexion récupéré'); + return token; +} +``` + +**Backend PHP :** +```php +// POST /stripe/terminal/connection-token +$token = \Stripe\Terminal\ConnectionToken::create([], [ + 'stripe_account' => $amicale->stripe_id, +]); + +return response()->json([ + 'secret' => $token->secret, +]); +``` + +**Points importants :** +- ✅ Le token est **temporaire** (valide quelques minutes) +- ✅ Un nouveau token est créé à **chaque initialisation** du SDK +- ✅ Le token est spécifique au **compte Stripe Connect** de l'amicale +- ✅ Utilisé pour **authentifier** le Terminal SDK auprès de Stripe + ### 📋 Détail des étapes #### Étape 1 : VALIDATION DU FORMULAIRE @@ -1085,28 +1185,168 @@ POST/PUT /api/passages ## 🔄 GESTION DES ERREURS -### 📱 Erreurs Tap to Pay +### 📱 Erreurs Tap to Pay - Messages utilisateur clairs -| Code erreur | Description | Action utilisateur | -|-------------|-------------|-------------------| -| `device_not_compatible` | iPhone non compatible | Afficher message explicatif | -| `nfc_disabled` | NFC désactivé | Demander activation dans réglages | -| `card_declined` | Carte refusée | Essayer autre carte | -| `insufficient_funds` | Solde insuffisant | Essayer autre carte | -| `network_error` | Erreur réseau | Réessayer ou mode offline | -| `timeout` | Timeout lecture carte | Rapprocher carte et réessayer | +L'application détecte automatiquement le type d'erreur et affiche un message adapté : -### 🔄 Flow de retry +#### Gestion intelligente des erreurs (passage_form_dialog.dart) + +```dart +catch (e) { + final errorMsg = e.toString().toLowerCase(); + String userMessage; + bool shouldCancelPayment = true; + + if (errorMsg.contains('canceled') || errorMsg.contains('cancelled')) { + // Annulation volontaire + userMessage = 'Paiement annulé'; + + } else if (errorMsg.contains('cardreadtimedout') || errorMsg.contains('timed out')) { + // Timeout NFC avec conseils + userMessage = 'Lecture de la carte impossible.\n\n' + 'Conseils :\n' + '• Maintenez la carte contre le dos du téléphone\n' + '• Ne bougez pas jusqu\'à confirmation\n' + '• Retirez la coque si nécessaire\n' + '• Essayez différentes positions sur le téléphone'; + + } else if (errorMsg.contains('already') && errorMsg.contains('payment')) { + // PaymentIntent existe déjà + userMessage = 'Un paiement est déjà en cours pour ce passage.\n' + 'Veuillez réessayer dans quelques instants.'; + shouldCancelPayment = false; + } + + // Annulation automatique du PaymentIntent pour permettre nouvelle tentative + if (shouldCancelPayment && _paymentIntentId != null) { + await StripeTapToPayService.instance.cancelPayment(_paymentIntentId!); + } +} +``` + +#### Table des erreurs et actions + +| Type erreur | Message utilisateur | Action automatique | +|-------------|--------------------|--------------------| +| `canceled` / `cancelled` | "Paiement annulé" | Annulation PaymentIntent ✅ | +| `cardReadTimedOut` | Message avec 4 conseils NFC | Annulation PaymentIntent ✅ | +| `already payment` | "Paiement déjà en cours" | Pas d'annulation ⏳ | +| `device_not_compatible` | "Appareil non compatible" | Annulation PaymentIntent ✅ | +| `nfc_disabled` | "NFC désactivé" | Annulation PaymentIntent ✅ | +| Autre erreur | Message technique complet | Annulation PaymentIntent ✅ | + +### ⚠️ Contraintes NFC - Tap to Pay vs Google Pay + +**Différence fondamentale :** +- **Google Pay (émission)** : Le téléphone *émet* un signal NFC puissant → fonctionne avec coque +- **Tap to Pay (réception)** : Le téléphone *lit* le signal de la carte → très sensible aux interférences + +#### Coques problématiques +- ❌ **Kevlar / Carbone** : Fibres conductrices perturbent la réception NFC +- ❌ **Métal** : Bloque complètement les ondes NFC +- ❌ **Coque épaisse** : Réduit la portée effective +- ✅ **TPU / Silicone** : Compatible + +#### Bonnes pratiques pour réussite NFC + +**Position optimale :** +``` +┌─────────────────┐ +│ 📱 Téléphone │ +│ │ +│ [Capteur NFC]│ ← Généralement vers le haut du dos +│ │ +│ │ +│ 💳 Carte ici │ ← Maintenir immobile 3-5 secondes +└─────────────────┘ +``` + +**Checklist utilisateur :** +1. ✅ Retirer la coque si échec +2. ✅ Carte à plat contre le dos du téléphone +3. ✅ Ne pas bouger pendant toute la lecture +4. ✅ Essayer différentes positions (haut/milieu du téléphone) +5. ✅ Carte sans contact activée (logo sans contact visible) + +### 🔄 Flow de retry automatique ``` -1. Erreur détectée -2. Message utilisateur explicite -3. Option "Réessayer" proposée -4. Conservation du montant et contexte -5. Nouveau PaymentIntent si nécessaire -6. Maximum 3 tentatives +1. Erreur détectée → Analyse du type +2. Annulation automatique PaymentIntent (si applicable) +3. Message clair avec conseils contextuels +4. Bouton "Réessayer" disponible +5. Nouveau PaymentIntent créé automatiquement +6. Conservation du contexte (montant, passage) ``` +**Avantages :** +- ✅ Pas de blocage "PaymentIntent déjà existant" +- ✅ Nombre illimité de tentatives +- ✅ Contexte préservé (pas besoin de tout ressaisir) +- ✅ Messages orientés solution plutôt qu'erreur technique + +### 🏗️ Environnement et Build Release + +#### Détection automatique de l'environnement + +L'application détecte l'environnement via l'URL de l'API (plus fiable que `kDebugMode`) : + +```dart +// stripe_tap_to_pay_service.dart (lignes 236-252) +Future _ensureReaderConnected() async { + // Détection via URL API + final apiUrl = ApiService.instance.baseUrl; + final isProduction = apiUrl.contains('app3.geosector.fr'); + final isSimulated = !isProduction; + + final config = TapToPayDiscoveryConfiguration( + isSimulated: isSimulated, + ); + + debugPrint('🔧 Environnement: ${isProduction ? "PRODUCTION (réel)" : "DEV/REC (simulé)"}'); + debugPrint('🔧 isSimulated: $isSimulated'); +} +``` + +**Mapping environnement :** +| URL API | Environnement | Reader Stripe | Cartes acceptées | +|---------|---------------|---------------|------------------| +| `dapp.geosector.fr` | DEV | Simulé | Cartes test uniquement | +| `rapp.geosector.fr` | REC | Simulé | Cartes test uniquement | +| `app3.geosector.fr` | PROD | Réel | Cartes réelles uniquement | + +#### ⚠️ Restriction Stripe - Build Release obligatoire en PROD + +**Erreur si app debuggable en PROD :** +``` +Debuggable applications are not supported when using the production +version of the Tap to Pay reader. Please use a simulated version of +the reader by setting TapToPayDiscoveryConfiguration.isSimulated to true. +``` + +**Solution - Build release :** +```bash +# Build APK optimisé pour production +flutter build apk --release + +# Installation sur device +adb install build/app/outputs/flutter-apk/app-release.apk +``` + +**Différences debug vs release :** +| Aspect | Debug (`flutter run`) | Release (`flutter build`) | +|--------|-----------------------|--------------------| +| **Optimisation** | ❌ Code non optimisé | ✅ R8/ProGuard activé | +| **Taille APK** | ~200 MB | ~30-50 MB | +| **Performance** | Lente (dev mode) | Rapide (optimisée) | +| **Tap to Pay PROD** | ❌ Refusé par Stripe | ✅ Accepté | +| **Débogage** | ✅ Hot reload, logs | ❌ Pas de hot reload | + +**Pourquoi Stripe refuse les apps debug :** +- **Sécurité renforcée** : Les apps debuggables peuvent être inspectées +- **Conformité PCI-DSS** : Exigences de sécurité pour paiements réels +- **Protection production** : Éviter utilisation accidentelle de readers réels en dev + --- ## 📊 MONITORING ET LOGS diff --git a/app/docs/connexions-api.md b/app/docs/connexions-api.md new file mode 100644 index 00000000..10621bb5 --- /dev/null +++ b/app/docs/connexions-api.md @@ -0,0 +1,216 @@ +API Event Stats - Guide Flutter + +Authentification + +Toutes les routes nécessitent un Bearer Token (session) et un rôle Admin (2) ou Super-admin (1). + +headers: { +'Authorization': 'Bearer $sessionToken', +'Content-Type': 'application/json', +} + +--- + +1. Résumé du jour + +GET /api/events/stats/summary?date=2025-12-22 + +| Param | Type | Requis | Description | +| ----- | ------ | ------ | ------------------------------------- | +| date | string | Non | Date YYYY-MM-DD (défaut: aujourd'hui) | + +Réponse (~1 KB) : +{ +"status": "success", +"data": { +"date": "2025-12-22", +"stats": { +"auth": { "success": 45, "failed": 3, "logout": 12 }, +"passages": { "created": 128, "updated": 5, "deleted": 2, "amount": 2450.00 }, +"users": { "created": 2, "updated": 5, "deleted": 0 }, +"sectors": { "created": 1, "updated": 3, "deleted": 0 }, +"stripe": { "created": 15, "success": 12, "failed": 1, "cancelled": 2, "amount": 890.00 } +}, +"totals": { "events": 245, "unique_users": 18 } +} +} + +--- + +2. Stats journalières (graphiques) + +GET /api/events/stats/daily?from=2025-12-01&to=2025-12-22&events=passage_created,login_success + +| Param | Type | Requis | Description | +| ------ | ------ | ------ | ------------------------------- | +| from | string | Oui | Date début YYYY-MM-DD | +| to | string | Oui | Date fin YYYY-MM-DD | +| events | string | Non | Types filtrés (comma-separated) | + +Limite : 90 jours max + +Réponse (~5 KB) : +{ +"status": "success", +"data": { +"from": "2025-12-01", +"to": "2025-12-22", +"days": [ +{ +"date": "2025-12-01", +"events": { +"passage_created": { "count": 45, "sum_amount": 850.00, "unique_users": 8 }, +"login_success": { "count": 12, "sum_amount": 0, "unique_users": 12 } +}, +"totals": { "count": 57, "sum_amount": 850.00 } +}, +... +] +} +} + +--- + +3. Stats hebdomadaires + +GET /api/events/stats/weekly?from=2025-10-01&to=2025-12-22 + +| Param | Type | Requis | Description | +| ------ | ------ | ------ | ------------- | +| from | string | Oui | Date début | +| to | string | Oui | Date fin | +| events | string | Non | Types filtrés | + +Limite : 365 jours max + +Réponse (~2 KB) : +{ +"status": "success", +"data": { +"from": "2025-10-01", +"to": "2025-12-22", +"weeks": [ +{ +"week_start": "2025-12-16", +"week_number": 51, +"year": 2025, +"events": { +"passage_created": { "count": 320, "sum_amount": 5200.00, "unique_users": 15 } +}, +"totals": { "count": 450, "sum_amount": 5200.00 } +}, +... +] +} +} + +--- + +4. Stats mensuelles + +GET /api/events/stats/monthly?year=2025&events=passage_created,stripe_payment_success + +| Param | Type | Requis | Description | +| ------ | ------ | ------ | ------------------------------ | +| year | int | Non | Année (défaut: année courante) | +| events | string | Non | Types filtrés | + +Réponse (~1 KB) : +{ +"status": "success", +"data": { +"year": 2025, +"months": [ +{ +"month": "2025-01", +"year": 2025, +"month_number": 1, +"events": { +"passage_created": { "count": 1250, "sum_amount": 18500.00, "unique_users": 25 } +}, +"totals": { "count": 1800, "sum_amount": 18500.00 } +}, +... +] +} +} + +--- + +5. Détail des événements (drill-down) + +GET /api/events/stats/details?date=2025-12-22&event=login_failed&limit=50&offset=0 + +| Param | Type | Requis | Description | +| ------ | ------ | ------ | ------------------------------------ | +| date | string | Oui | Date YYYY-MM-DD | +| event | string | Non | Filtrer par type | +| limit | int | Non | Max résultats (défaut: 50, max: 100) | +| offset | int | Non | Pagination (défaut: 0) | + +Réponse (~10 KB) : +{ +"status": "success", +"data": { +"date": "2025-12-22", +"events": [ +{ +"timestamp": "2025-12-22T08:15:32Z", +"event": "login_failed", +"username": "jean.dupont", +"reason": "invalid_password", +"attempt": 2, +"ip": "192.168.x.x", +"platform": "ios", +"app_version": "3.5.2" +}, +... +], +"pagination": { +"total": 3, +"limit": 50, +"offset": 0, +"has_more": false +} +} +} + +--- + +6. Types d'événements disponibles + +GET /api/events/stats/types + +Réponse : +{ +"status": "success", +"data": { +"auth": ["login_success", "login_failed", "logout"], +"passages": ["passage_created", "passage_updated", "passage_deleted"], +"sectors": ["sector_created", "sector_updated", "sector_deleted"], +"users": ["user_created", "user_updated", "user_deleted"], +"entities": ["entity_created", "entity_updated", "entity_deleted"], +"operations": ["operation_created", "operation_updated", "operation_deleted"], +"stripe": ["stripe_payment_created", "stripe_payment_success", "stripe_payment_failed", "stripe_payment_cancelled", "stripe_terminal_error"] +} +} + +--- + +Codes d'erreur + +| Code | Signification | +| ---- | -------------------------------------------- | +| 200 | Succès | +| 400 | Paramètre invalide (date, plage trop grande) | +| 403 | Accès refusé (rôle insuffisant) | +| 500 | Erreur serveur | + +--- + +Super-admin uniquement + +Le super-admin peut ajouter ?entity_id=X pour filtrer par entité : +GET /api/events/stats/summary?date=2025-12-22&entity_id=5 + +Sans ce paramètre, il voit les stats globales de toutes les entités. diff --git a/app/fastlane/Fastfile b/app/fastlane/Fastfile index 771aac1e..2977b3b2 100644 --- a/app/fastlane/Fastfile +++ b/app/fastlane/Fastfile @@ -12,6 +12,9 @@ default_platform(:android) +# Configure PATH pour Homebrew (M1/M2/M4 Mac) +ENV['PATH'] = "/opt/homebrew/bin:#{ENV['PATH']}" + # ============================================================================= # ANDROID # ============================================================================= diff --git a/app/ios-build-mac.sh b/app/ios-build-mac.sh index f7f40f90..d68e3aa9 100755 --- a/app/ios-build-mac.sh +++ b/app/ios-build-mac.sh @@ -33,9 +33,21 @@ fi # Récupérer la version depuis pubspec.yaml VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //' | tr -d ' ') +VERSION_NUMBER=$(echo $VERSION | cut -d'+' -f1) +VERSION_CODE=$(echo $VERSION | cut -d'+' -f2) + echo -e "${YELLOW}📦 Version détectée :${NC} $VERSION" +echo -e "${YELLOW} Version name :${NC} $VERSION_NUMBER" +echo -e "${YELLOW} Build number :${NC} $VERSION_CODE" echo "" +# Vérifier que la version est bien synchronisée depuis transfer-to-mac.sh +if [ -z "$VERSION_CODE" ]; then + echo -e "${RED}⚠️ Avertissement: Version code introuvable${NC}" + echo -e "${YELLOW}Assurez-vous d'avoir utilisé transfer-to-mac.sh pour synchroniser la version${NC}" + echo "" +fi + # Étape 1 : Clean echo -e "${YELLOW}🧹 Étape 1/5 : Nettoyage du projet...${NC}" flutter clean @@ -50,6 +62,12 @@ echo "" # Étape 3 : Pod install echo -e "${YELLOW}🔧 Étape 3/5 : Installation des CocoaPods...${NC}" + +# Configurer l'environnement Ruby Homebrew +export PATH="/opt/homebrew/opt/ruby/bin:/opt/homebrew/bin:$PATH" +export GEM_HOME="/opt/homebrew/lib/ruby/gems/3.4.0" +echo -e "${BLUE}ℹ Environnement Ruby Homebrew configuré${NC}" + cd ios rm -rf Pods Podfile.lock pod install --repo-update @@ -57,10 +75,29 @@ cd .. echo -e "${GREEN}✓ CocoaPods installés${NC}" echo "" -# Étape 4 : Build iOS Release -echo -e "${YELLOW}🏗️ Étape 4/5 : Compilation iOS en mode release...${NC}" -flutter build ios --release -echo -e "${GREEN}✓ Compilation terminée${NC}" +# Étape 4 : Build iOS +echo -e "${YELLOW}🏗️ Étape 4/5 : Choix du mode de compilation...${NC}" +echo "" +echo -e "${BLUE}Quel mode de compilation souhaitez-vous utiliser ?${NC}" +echo -e " ${GREEN}[D]${NC} Debug - Pour tester Stripe Tap to Pay (défaut)" +echo -e " ${YELLOW}[R]${NC} Release - Pour distribution App Store" +echo "" +read -p "Votre choix (D/R) [défaut: D] : " -n 1 -r BUILD_MODE +echo "" +echo "" + +# Définir le mode de build +if [[ $BUILD_MODE =~ ^[Rr]$ ]]; then + BUILD_FLAG="--release" + BUILD_MODE_NAME="Release" +else + BUILD_FLAG="--debug" + BUILD_MODE_NAME="Debug" +fi + +echo -e "${YELLOW}🏗️ Compilation iOS en mode ${BUILD_MODE_NAME}...${NC}" +flutter build ios $BUILD_FLAG +echo -e "${GREEN}✓ Compilation terminée (mode ${BUILD_MODE_NAME})${NC}" echo "" # Étape 5 : Ouvrir Xcode diff --git a/app/ios.sh b/app/ios.sh new file mode 100755 index 00000000..9af5eea3 --- /dev/null +++ b/app/ios.sh @@ -0,0 +1,249 @@ +#!/bin/bash + +# Script de génération iOS pour GEOSECTOR +# Usage: ./ios.sh + +set -e # Arrêter le script en cas d'erreur + +# Couleurs pour les messages +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Configuration Mac mini +MAC_MINI_IP="192.168.1.34" +MAC_USER="pierre" + +# Fonction pour afficher les messages +print_message() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +# Fonction pour gérer les erreurs +handle_error() { + print_error "Une erreur est survenue lors de l'exécution de la commande" + print_error "Ligne $1" + exit 1 +} + +# Trap pour capturer les erreurs +trap 'handle_error $LINENO' ERR + +# Vérifier que nous sommes dans le bon dossier +if [ ! -f "pubspec.yaml" ]; then + print_error "Ce script doit être exécuté depuis le dossier racine de l'application Flutter" + print_error "Fichier pubspec.yaml introuvable" + exit 1 +fi + +print_message "=========================================" +print_message " GEOSECTOR - Génération iOS" +print_message "=========================================" +echo + +# Vérifier que Flutter est installé +if ! command -v flutter &> /dev/null; then + print_error "Flutter n'est pas installé ou n'est pas dans le PATH" + exit 1 +fi + +# Étape 1 : Synchroniser la version depuis ../VERSION +print_message "Étape 1/4 : Synchronisation de la version..." +echo + +VERSION_FILE="../VERSION" +if [ ! -f "$VERSION_FILE" ]; then + print_error "Fichier VERSION introuvable : $VERSION_FILE" + exit 1 +fi + +# Lire la version depuis le fichier (enlever espaces/retours à la ligne) +VERSION_NUMBER=$(cat "$VERSION_FILE" | tr -d '\n\r ' | tr -d '[:space:]') +if [ -z "$VERSION_NUMBER" ]; then + print_error "Le fichier VERSION est vide" + exit 1 +fi + +print_message "Version lue depuis $VERSION_FILE : $VERSION_NUMBER" + +# Calculer le versionCode (supprimer les points) +VERSION_CODE=$(echo $VERSION_NUMBER | tr -d '.') +if [ -z "$VERSION_CODE" ]; then + print_error "Impossible de calculer le versionCode" + exit 1 +fi + +print_message "Version code calculé : $VERSION_CODE" + +# Mettre à jour pubspec.yaml +print_message "Mise à jour de pubspec.yaml..." +sed -i.bak "s/^version:.*/version: $VERSION_NUMBER+$VERSION_CODE/" pubspec.yaml + +# Vérifier que la mise à jour a réussi +UPDATED_VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //') +if [ "$UPDATED_VERSION" != "$VERSION_NUMBER+$VERSION_CODE" ]; then + print_error "Échec de la mise à jour de pubspec.yaml" + print_error "Attendu : $VERSION_NUMBER+$VERSION_CODE" + print_error "Obtenu : $UPDATED_VERSION" + exit 1 +fi + +print_success "pubspec.yaml mis à jour : version: $VERSION_NUMBER+$VERSION_CODE" +echo + +# Récupérer la version finale pour l'affichage +VERSION="$VERSION_NUMBER-$VERSION_CODE" +print_message "Version finale : $VERSION" +print_message "Version code : $VERSION_CODE" +echo + +# Étape 2 : Vérifier la connexion au Mac mini +print_message "Étape 2/4 : Vérification de la connexion au Mac mini..." +echo + +print_message "Test de connexion à $MAC_USER@$MAC_MINI_IP..." + +if ssh -o ConnectTimeout=5 -o BatchMode=yes "$MAC_USER@$MAC_MINI_IP" exit 2>/dev/null; then + print_success "Connexion SSH au Mac mini établie" +else + print_warning "Impossible de se connecter au Mac mini en mode non-interactif" + print_message "Le transfert demandera votre mot de passe" +fi +echo + +# Étape 3 : Nettoyer le projet +print_message "Étape 3/4 : Nettoyage du projet local..." +echo + +print_message "Nettoyage en cours..." +flutter clean +print_success "Projet nettoyé" +echo + +# Étape 4 : Transfert vers Mac mini +print_message "Étape 4/4 : Transfert vers Mac mini..." +echo + +# Construire le chemin de destination avec numéro de version +DESTINATION_DIR="app_$VERSION_CODE" +DESTINATION="$MAC_USER@$MAC_MINI_IP:/Users/pierre/dev/geosector/$DESTINATION_DIR" + +print_message "Configuration du transfert :" +print_message " Source : $(pwd)" +print_message " Destination : $DESTINATION" +echo + +# Supprimer le dossier de destination s'il existe déjà +print_message "Suppression du dossier existant sur le Mac mini (si présent)..." +ssh -o IdentitiesOnly=yes -o PubkeyAuthentication=no -o PreferredAuthentications=password \ + "$MAC_USER@$MAC_MINI_IP" "rm -rf /Users/pierre/dev/geosector/$DESTINATION_DIR" 2>/dev/null || true +print_success "Dossier nettoyé" +echo + +print_warning "Note: Vous allez devoir saisir le mot de passe du Mac mini" +print_message "rsync va créer le dossier de destination automatiquement" +echo + +# Transfert réel (rsync créera le dossier automatiquement) +# Options SSH pour éviter "too many authentication failures" +rsync -avz --progress \ + -e "ssh -o IdentitiesOnly=yes -o PubkeyAuthentication=no -o PreferredAuthentications=password" \ + --rsync-path="mkdir -p /Users/pierre/dev/geosector/$DESTINATION_DIR && rsync" \ + --exclude='build/' \ + --exclude='.dart_tool/' \ + --exclude='ios/Pods/' \ + --exclude='ios/.symlinks/' \ + --exclude='macos/Pods/' \ + --exclude='linux/flutter/ephemeral/' \ + --exclude='windows/flutter/ephemeral/' \ + --exclude='.pub-cache/' \ + --exclude='android/build/' \ + --exclude='*.aab' \ + --exclude='*.apk' \ + ./ "$DESTINATION/" + +if [ $? -eq 0 ]; then + echo + print_success "Transfert terminé avec succès !" + echo +else + print_error "Erreur lors du transfert" + exit 1 +fi + +# Afficher le résumé +echo +print_message "=========================================" +print_success " TRANSFERT TERMINÉ AVEC SUCCÈS !" +print_message "=========================================" +echo +print_message "Version : ${GREEN}$VERSION${NC}" +print_message "Dossier sur le Mac : ${GREEN}/Users/pierre/dev/geosector/$DESTINATION_DIR${NC}" +echo + +# Proposer de lancer le build automatiquement +print_message "Voulez-vous lancer le build iOS maintenant ?" +echo +print_message " ${GREEN}[A]${NC} Se connecter en SSH et lancer le build automatiquement" +print_message " ${YELLOW}[B]${NC} Afficher les instructions manuelles (défaut)" +echo +read -p "Votre choix (A/B) [défaut: B] : " -n 1 -r BUILD_CHOICE +echo +echo + +if [[ $BUILD_CHOICE =~ ^[Aa]$ ]]; then + print_message "Connexion au Mac mini et lancement du build..." + echo + + # Se connecter en SSH et lancer le build + # Options SSH pour éviter "too many authentication failures" + ssh -t -o IdentitiesOnly=yes -o PubkeyAuthentication=no -o PreferredAuthentications=password \ + "$MAC_USER@$MAC_MINI_IP" "cd /Users/pierre/dev/geosector/$DESTINATION_DIR && ./ios-build-mac.sh" + + echo + print_success "Build terminé sur le Mac mini" + echo + print_message "Prochaines étapes sur le Mac mini :" + print_message "1. Xcode est maintenant ouvert" + print_message "2. Vérifier Signing & Capabilities (Team: 6WT84NWCTC)" + print_message "3. Product > Archive" + print_message "4. Organizer > Distribute App > App Store Connect" +else + print_message "=========================================" + print_message " INSTRUCTIONS MANUELLES" + print_message "=========================================" + echo + print_message "Sur votre Mac mini, exécutez les commandes suivantes :" + echo + echo -e "${YELLOW}# Se connecter au Mac mini${NC}" + echo "ssh $MAC_USER@$MAC_MINI_IP" + echo + echo -e "${YELLOW}# Aller dans le dossier du projet${NC}" + echo "cd /Users/pierre/dev/geosector/$DESTINATION_DIR" + echo + echo -e "${YELLOW}# Lancer le build iOS${NC}" + echo "./ios-build-mac.sh" + echo + print_message "=========================================" + echo + print_message "Ou copiez-collez cette commande complète :" + echo + echo -e "${GREEN}ssh -t $MAC_USER@$MAC_MINI_IP \"cd /Users/pierre/dev/geosector/$DESTINATION_DIR && ./ios-build-mac.sh\"${NC}" +fi + +echo +print_success "Script terminé !" diff --git a/app/ios/GeoSector_v3_Development.mobileprovision b/app/ios/GeoSector_v3_Development.mobileprovision new file mode 100644 index 00000000..e52b319d Binary files /dev/null and b/app/ios/GeoSector_v3_Development.mobileprovision differ diff --git a/app/ios/Runner.xcodeproj/project.pbxproj b/app/ios/Runner.xcodeproj/project.pbxproj index 1ddb1182..d6510b27 100644 --- a/app/ios/Runner.xcodeproj/project.pbxproj +++ b/app/ios/Runner.xcodeproj/project.pbxproj @@ -488,7 +488,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = ""; @@ -504,7 +505,7 @@ PRODUCT_BUNDLE_IDENTIFIER = fr.geosector.app3; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "GeoSector v3 App Store"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "GeoSector v3 Development"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -680,7 +681,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = ""; @@ -696,7 +698,7 @@ PRODUCT_BUNDLE_IDENTIFIER = fr.geosector.app3; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "GeoSector v3 App Store"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "GeoSector v3 Development"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -710,7 +712,8 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = ""; @@ -726,7 +729,7 @@ PRODUCT_BUNDLE_IDENTIFIER = fr.geosector.app3; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "GeoSector v3 App Store"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "GeoSector v3 Development"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/app/ios/Runner/Runner.entitlements b/app/ios/Runner/Runner.entitlements new file mode 100644 index 00000000..4b1b10c0 --- /dev/null +++ b/app/ios/Runner/Runner.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.developer.proximity-reader.payment.acceptance + + + diff --git a/app/lib/app.dart b/app/lib/app.dart index b2c8471d..5c28fa1c 100755 --- a/app/lib/app.dart +++ b/app/lib/app.dart @@ -24,6 +24,7 @@ import 'package:geosector_app/presentation/pages/messages_page.dart'; import 'package:geosector_app/presentation/pages/amicale_page.dart'; import 'package:geosector_app/presentation/pages/operations_page.dart'; import 'package:geosector_app/presentation/pages/field_mode_page.dart'; +import 'package:geosector_app/presentation/pages/connexions_page.dart'; // Instances globales des repositories (plus besoin d'injecter ApiService) final operationRepository = OperationRepository(); @@ -322,6 +323,15 @@ class _GeosectorAppState extends State with WidgetsBindingObserver return const OperationsPage(); }, ), + // Sous-route pour connexions (role 2+ uniquement) + GoRoute( + path: 'connexions', + name: 'admin-connexions', + builder: (context, state) { + debugPrint('GoRoute: Affichage de ConnexionsPage (unifiée)'); + return const ConnexionsPage(); + }, + ), ], ), ], diff --git a/app/lib/chat/services/chat_service.dart b/app/lib/chat/services/chat_service.dart index 85963ef3..c97cd70d 100644 --- a/app/lib/chat/services/chat_service.dart +++ b/app/lib/chat/services/chat_service.dart @@ -29,7 +29,8 @@ class ChatService { Timer? _syncTimer; DateTime? _lastSyncTimestamp; DateTime? _lastFullSync; - static const Duration _syncInterval = Duration(seconds: 15); // Changé à 15 secondes comme suggéré par l'API + static const Duration _syncInterval = Duration(seconds: 30); // Sync toutes les 30 secondes + static const Duration _initialSyncDelay = Duration(seconds: 10); // Délai avant première sync static const Duration _fullSyncInterval = Duration(minutes: 5); /// Initialisation avec gestion des rôles et configuration YAML @@ -76,10 +77,13 @@ class ChatService { // Charger le dernier timestamp de sync depuis Hive await _instance!._loadSyncTimestamp(); - // Faire la sync initiale complète au login - await _instance!.getRooms(forceFullSync: true); - debugPrint('✅ Sync initiale complète effectuée au login'); - + // Faire la sync initiale complète au login avec délai de 10 secondes + debugPrint('⏳ Sync initiale chat programmée dans 10 secondes...'); + Future.delayed(_initialSyncDelay, () async { + await _instance!.getRooms(forceFullSync: true); + debugPrint('✅ Sync initiale complète effectuée au login'); + }); + // Démarrer la synchronisation incrémentale périodique _instance!._startSync(); } @@ -136,6 +140,13 @@ class ChatService { /// Obtenir les rooms avec synchronisation incrémentale Future> getRooms({bool forceFullSync = false}) async { + // DÉSACTIVATION TEMPORAIRE - Retour direct du cache sans appeler l'API + debugPrint('🚫 API /chat/rooms désactivée - utilisation du cache uniquement'); + return _roomsBox.values.toList() + ..sort((a, b) => (b.lastMessageAt ?? b.createdAt) + .compareTo(a.lastMessageAt ?? a.createdAt)); + + /* Code original commenté pour désactiver les appels API // Vérifier la connectivité if (!connectivityService.isConnected) { debugPrint('📵 Pas de connexion réseau - utilisation du cache'); @@ -143,30 +154,32 @@ class ChatService { ..sort((a, b) => (b.lastMessageAt ?? b.createdAt) .compareTo(a.lastMessageAt ?? a.createdAt)); } - + try { // Déterminer si on fait une sync complète ou incrémentale final now = DateTime.now(); final needsFullSync = forceFullSync || _lastFullSync == null || now.difference(_lastFullSync!).compareTo(_fullSyncInterval) > 0; - + Response response; - + if (needsFullSync || _lastSyncTimestamp == null) { // Synchronisation complète debugPrint('🔄 Synchronisation complète des rooms...'); - response = await _dio.get('/chat/rooms'); + // response = await _dio.get('/chat/rooms'); // COMMENTÉ - Désactivation GET /chat/rooms + return; // Retour anticipé pour éviter l'appel API _lastFullSync = now; } else { // Synchronisation incrémentale final isoTimestamp = _lastSyncTimestamp!.toUtc().toIso8601String(); // debugPrint('🔄 Synchronisation incrémentale depuis $isoTimestamp'); - response = await _dio.get('/chat/rooms', queryParameters: { - 'updated_after': isoTimestamp, - }); + // response = await _dio.get('/chat/rooms', queryParameters: { // COMMENTÉ - Désactivation GET /chat/rooms + // 'updated_after': isoTimestamp, + // }); + return; // Retour anticipé pour éviter l'appel API } - + // Extraire le timestamp de synchronisation fourni par l'API if (response.data is Map && response.data['sync_timestamp'] != null) { _lastSyncTimestamp = DateTime.parse(response.data['sync_timestamp']); @@ -180,7 +193,7 @@ class ChatService { // On utilise le timestamp actuel comme fallback mais ce n'est pas idéal _lastSyncTimestamp = now; } - + // Vérifier s'il y a des changements (pour sync incrémentale) if (!needsFullSync && response.data is Map && response.data['has_changes'] == false) { // debugPrint('✅ Aucun changement depuis la dernière sync'); @@ -188,7 +201,7 @@ class ChatService { ..sort((a, b) => (b.lastMessageAt ?? b.createdAt) .compareTo(a.lastMessageAt ?? a.createdAt)); } - + // Gérer différents formats de réponse API List roomsData; if (response.data is Map) { @@ -206,11 +219,11 @@ class ChatService { } else { roomsData = []; } - + // Parser les rooms final rooms = []; final deletedRoomIds = []; - + for (final json in roomsData) { try { // Vérifier si la room est marquée comme supprimée @@ -218,21 +231,21 @@ class ChatService { deletedRoomIds.add(json['id']); continue; } - + final room = Room.fromJson(json); rooms.add(room); } catch (e) { debugPrint('❌ Erreur parsing room: $e'); } } - + // Appliquer les modifications à Hive if (needsFullSync) { // Sync complète : remplacer tout mais préserver certaines données locales final existingRooms = Map.fromEntries( _roomsBox.values.map((r) => MapEntry(r.id, r)) ); - + await _roomsBox.clear(); for (final room in rooms) { final existingRoom = existingRooms[room.id]; @@ -250,7 +263,7 @@ class ChatService { createdBy: room.createdBy ?? existingRoom?.createdBy, ); await _roomsBox.put(roomToSave.id, roomToSave); - + // Traiter les messages récents de la room if (room.recentMessages != null && room.recentMessages!.isNotEmpty) { for (final msgData in room.recentMessages!) { @@ -288,10 +301,10 @@ class ChatService { // Préserver createdBy existant si la nouvelle room n'en a pas createdBy: room.createdBy ?? existingRoom?.createdBy, ); - + debugPrint('💾 Sauvegarde room ${roomToSave.title} (${roomToSave.id}): createdBy=${roomToSave.createdBy}'); await _roomsBox.put(roomToSave.id, roomToSave); - + // Traiter les messages récents de la room if (room.recentMessages != null && room.recentMessages!.isNotEmpty) { for (final msgData in room.recentMessages!) { @@ -314,22 +327,22 @@ class ChatService { for (final roomId in deletedRoomIds) { // Supprimer la room await _roomsBox.delete(roomId); - + // Supprimer tous les messages de cette room final messagesToDelete = _messagesBox.values .where((msg) => msg.roomId == roomId) .map((msg) => msg.id) .toList(); - + for (final msgId in messagesToDelete) { await _messagesBox.delete(msgId); } - + debugPrint('🗑️ Room $roomId supprimée avec ${messagesToDelete.length} messages'); } debugPrint('💾 Sync incrémentale: ${rooms.length} rooms mises à jour, ${deletedRoomIds.length} supprimées'); } - + // Mettre à jour les stats globales final allRooms = _roomsBox.values.toList(); final totalUnread = allRooms.fold(0, (sum, room) => sum + room.unreadCount); @@ -337,7 +350,7 @@ class ChatService { totalRooms: allRooms.length, unreadMessages: totalUnread, ); - + return allRooms ..sort((a, b) => (b.lastMessageAt ?? b.createdAt) .compareTo(a.lastMessageAt ?? a.createdAt)); @@ -348,6 +361,7 @@ class ChatService { ..sort((a, b) => (b.lastMessageAt ?? b.createdAt) .compareTo(a.lastMessageAt ?? a.createdAt)); } + */// Fin du code commenté } /// Créer une room avec vérification des permissions @@ -754,7 +768,7 @@ class ChatService { }); // Pas de sync immédiate ici car déjà faite dans init() - debugPrint('⏰ Timer de sync incrémentale démarré (toutes les 15 secondes)'); + debugPrint('⏰ Timer de sync incrémentale démarré (toutes les 30 secondes)'); } /// Mettre en pause les synchronisations (app en arrière-plan) diff --git a/app/lib/core/data/models/event_stats_model.dart b/app/lib/core/data/models/event_stats_model.dart new file mode 100644 index 00000000..40ac86c5 --- /dev/null +++ b/app/lib/core/data/models/event_stats_model.dart @@ -0,0 +1,594 @@ +// Modèles pour les statistiques d'événements (connexions, passages, etc.) +// +// Ces modèles ne sont PAS stockés dans Hive car les données sont récupérées +// à la demande depuis l'API et ne nécessitent pas de persistance locale. + +/// Statistiques d'authentification +class AuthStats { + final int success; + final int failed; + final int logout; + + const AuthStats({ + required this.success, + required this.failed, + required this.logout, + }); + + factory AuthStats.fromJson(Map json) { + return AuthStats( + success: _parseInt(json['success']), + failed: _parseInt(json['failed']), + logout: _parseInt(json['logout']), + ); + } + + int get total => success + failed + logout; +} + +/// Statistiques de passages +class PassageStats { + final int created; + final int updated; + final int deleted; + final double amount; + + const PassageStats({ + required this.created, + required this.updated, + required this.deleted, + required this.amount, + }); + + factory PassageStats.fromJson(Map json) { + return PassageStats( + created: _parseInt(json['created']), + updated: _parseInt(json['updated']), + deleted: _parseInt(json['deleted']), + amount: _parseDouble(json['amount']), + ); + } + + int get total => created + updated + deleted; +} + +/// Statistiques utilisateurs +class UserStats { + final int created; + final int updated; + final int deleted; + + const UserStats({ + required this.created, + required this.updated, + required this.deleted, + }); + + factory UserStats.fromJson(Map json) { + return UserStats( + created: _parseInt(json['created']), + updated: _parseInt(json['updated']), + deleted: _parseInt(json['deleted']), + ); + } + + int get total => created + updated + deleted; +} + +/// Statistiques secteurs +class SectorStats { + final int created; + final int updated; + final int deleted; + + const SectorStats({ + required this.created, + required this.updated, + required this.deleted, + }); + + factory SectorStats.fromJson(Map json) { + return SectorStats( + created: _parseInt(json['created']), + updated: _parseInt(json['updated']), + deleted: _parseInt(json['deleted']), + ); + } + + int get total => created + updated + deleted; +} + +/// Statistiques Stripe +class StripeStats { + final int created; + final int success; + final int failed; + final int cancelled; + final double amount; + + const StripeStats({ + required this.created, + required this.success, + required this.failed, + required this.cancelled, + required this.amount, + }); + + factory StripeStats.fromJson(Map json) { + return StripeStats( + created: _parseInt(json['created']), + success: _parseInt(json['success']), + failed: _parseInt(json['failed']), + cancelled: _parseInt(json['cancelled']), + amount: _parseDouble(json['amount']), + ); + } + + int get total => created + success + failed + cancelled; +} + +/// Statistiques globales d'une journée +class DayStats { + final AuthStats auth; + final PassageStats passages; + final UserStats users; + final SectorStats sectors; + final StripeStats stripe; + + const DayStats({ + required this.auth, + required this.passages, + required this.users, + required this.sectors, + required this.stripe, + }); + + factory DayStats.fromJson(Map json) { + return DayStats( + auth: AuthStats.fromJson(json['auth'] ?? {}), + passages: PassageStats.fromJson(json['passages'] ?? {}), + users: UserStats.fromJson(json['users'] ?? {}), + sectors: SectorStats.fromJson(json['sectors'] ?? {}), + stripe: StripeStats.fromJson(json['stripe'] ?? {}), + ); + } +} + +/// Totaux d'une journée +class DayTotals { + final int events; + final int uniqueUsers; + + const DayTotals({ + required this.events, + required this.uniqueUsers, + }); + + factory DayTotals.fromJson(Map json) { + return DayTotals( + events: _parseInt(json['events']), + uniqueUsers: _parseInt(json['unique_users']), + ); + } +} + +/// Résumé complet d'une journée (réponse de /stats/summary) +class EventSummary { + final DateTime date; + final DayStats stats; + final DayTotals totals; + + const EventSummary({ + required this.date, + required this.stats, + required this.totals, + }); + + factory EventSummary.fromJson(Map json) { + return EventSummary( + date: DateTime.parse(json['date']), + stats: DayStats.fromJson(json['stats'] ?? {}), + totals: DayTotals.fromJson(json['totals'] ?? {}), + ); + } +} + +/// Statistiques d'un type d'événement pour une période +class EventTypeStats { + final int count; + final double sumAmount; + final int uniqueUsers; + + const EventTypeStats({ + required this.count, + required this.sumAmount, + required this.uniqueUsers, + }); + + factory EventTypeStats.fromJson(Map json) { + return EventTypeStats( + count: _parseInt(json['count']), + sumAmount: _parseDouble(json['sum_amount']), + uniqueUsers: _parseInt(json['unique_users']), + ); + } +} + +/// Données d'une journée dans les stats quotidiennes +class DailyStatsEntry { + final DateTime date; + final Map events; + final int totalCount; + final double totalAmount; + + const DailyStatsEntry({ + required this.date, + required this.events, + required this.totalCount, + required this.totalAmount, + }); + + factory DailyStatsEntry.fromJson(Map json) { + final eventsJson = json['events'] as Map? ?? {}; + final events = {}; + + for (final entry in eventsJson.entries) { + events[entry.key] = EventTypeStats.fromJson(entry.value); + } + + final totals = json['totals'] as Map? ?? {}; + + return DailyStatsEntry( + date: DateTime.parse(json['date']), + events: events, + totalCount: _parseInt(totals['count']), + totalAmount: _parseDouble(totals['sum_amount']), + ); + } +} + +/// Réponse des stats quotidiennes (/stats/daily) +class DailyStats { + final DateTime from; + final DateTime to; + final List days; + + const DailyStats({ + required this.from, + required this.to, + required this.days, + }); + + factory DailyStats.fromJson(Map json) { + final daysJson = json['days'] as List? ?? []; + + return DailyStats( + from: DateTime.parse(json['from']), + to: DateTime.parse(json['to']), + days: daysJson.map((d) => DailyStatsEntry.fromJson(d)).toList(), + ); + } +} + +/// Données d'une semaine dans les stats hebdomadaires +class WeeklyStatsEntry { + final DateTime weekStart; + final int weekNumber; + final int year; + final Map events; + final int totalCount; + final double totalAmount; + + const WeeklyStatsEntry({ + required this.weekStart, + required this.weekNumber, + required this.year, + required this.events, + required this.totalCount, + required this.totalAmount, + }); + + factory WeeklyStatsEntry.fromJson(Map json) { + final eventsJson = json['events'] as Map? ?? {}; + final events = {}; + + for (final entry in eventsJson.entries) { + events[entry.key] = EventTypeStats.fromJson(entry.value); + } + + final totals = json['totals'] as Map? ?? {}; + + return WeeklyStatsEntry( + weekStart: DateTime.parse(json['week_start']), + weekNumber: _parseInt(json['week_number']), + year: _parseInt(json['year']), + events: events, + totalCount: _parseInt(totals['count']), + totalAmount: _parseDouble(totals['sum_amount']), + ); + } +} + +/// Réponse des stats hebdomadaires (/stats/weekly) +class WeeklyStats { + final DateTime from; + final DateTime to; + final List weeks; + + const WeeklyStats({ + required this.from, + required this.to, + required this.weeks, + }); + + factory WeeklyStats.fromJson(Map json) { + final weeksJson = json['weeks'] as List? ?? []; + + return WeeklyStats( + from: DateTime.parse(json['from']), + to: DateTime.parse(json['to']), + weeks: weeksJson.map((w) => WeeklyStatsEntry.fromJson(w)).toList(), + ); + } +} + +/// Données d'un mois dans les stats mensuelles +class MonthlyStatsEntry { + final String month; // Format: "2025-01" + final int year; + final int monthNumber; + final Map events; + final int totalCount; + final double totalAmount; + + const MonthlyStatsEntry({ + required this.month, + required this.year, + required this.monthNumber, + required this.events, + required this.totalCount, + required this.totalAmount, + }); + + factory MonthlyStatsEntry.fromJson(Map json) { + final eventsJson = json['events'] as Map? ?? {}; + final events = {}; + + for (final entry in eventsJson.entries) { + events[entry.key] = EventTypeStats.fromJson(entry.value); + } + + final totals = json['totals'] as Map? ?? {}; + + return MonthlyStatsEntry( + month: json['month'] ?? '', + year: _parseInt(json['year']), + monthNumber: _parseInt(json['month_number']), + events: events, + totalCount: _parseInt(totals['count']), + totalAmount: _parseDouble(totals['sum_amount']), + ); + } +} + +/// Réponse des stats mensuelles (/stats/monthly) +class MonthlyStats { + final int year; + final List months; + + const MonthlyStats({ + required this.year, + required this.months, + }); + + factory MonthlyStats.fromJson(Map json) { + final monthsJson = json['months'] as List? ?? []; + + return MonthlyStats( + year: _parseInt(json['year']), + months: monthsJson.map((m) => MonthlyStatsEntry.fromJson(m)).toList(), + ); + } +} + +/// Détail d'un événement individuel +class EventDetail { + final DateTime timestamp; + final String event; + final String? username; + final String? reason; + final int? attempt; + final String? ip; + final String? platform; + final String? appVersion; + final Map? extra; + + const EventDetail({ + required this.timestamp, + required this.event, + this.username, + this.reason, + this.attempt, + this.ip, + this.platform, + this.appVersion, + this.extra, + }); + + factory EventDetail.fromJson(Map json) { + return EventDetail( + timestamp: DateTime.parse(json['timestamp']), + event: json['event'] ?? '', + username: json['username'], + reason: json['reason'], + attempt: json['attempt'] != null ? _parseInt(json['attempt']) : null, + ip: json['ip'], + platform: json['platform'], + appVersion: json['app_version'], + extra: json, + ); + } +} + +/// Pagination pour les détails +class EventPagination { + final int total; + final int limit; + final int offset; + final bool hasMore; + + const EventPagination({ + required this.total, + required this.limit, + required this.offset, + required this.hasMore, + }); + + factory EventPagination.fromJson(Map json) { + return EventPagination( + total: _parseInt(json['total']), + limit: _parseInt(json['limit']), + offset: _parseInt(json['offset']), + hasMore: json['has_more'] == true, + ); + } +} + +/// Réponse des détails d'événements (/stats/details) +class EventDetails { + final DateTime date; + final List events; + final EventPagination pagination; + + const EventDetails({ + required this.date, + required this.events, + required this.pagination, + }); + + factory EventDetails.fromJson(Map json) { + final eventsJson = json['events'] as List? ?? []; + + return EventDetails( + date: DateTime.parse(json['date']), + events: eventsJson.map((e) => EventDetail.fromJson(e)).toList(), + pagination: EventPagination.fromJson(json['pagination'] ?? {}), + ); + } +} + +/// Types d'événements disponibles +class EventTypes { + final List auth; + final List passages; + final List sectors; + final List users; + final List entities; + final List operations; + final List stripe; + + const EventTypes({ + required this.auth, + required this.passages, + required this.sectors, + required this.users, + required this.entities, + required this.operations, + required this.stripe, + }); + + factory EventTypes.fromJson(Map json) { + return EventTypes( + auth: _parseStringList(json['auth']), + passages: _parseStringList(json['passages']), + sectors: _parseStringList(json['sectors']), + users: _parseStringList(json['users']), + entities: _parseStringList(json['entities']), + operations: _parseStringList(json['operations']), + stripe: _parseStringList(json['stripe']), + ); + } + + /// Obtient tous les types d'événements à plat + List get allTypes => [ + ...auth, + ...passages, + ...sectors, + ...users, + ...entities, + ...operations, + ...stripe, + ]; + + /// Obtient le libellé français d'un type d'événement + static String getLabel(String eventType) { + switch (eventType) { + // Auth + case 'login_success': return 'Connexion réussie'; + case 'login_failed': return 'Connexion échouée'; + case 'logout': return 'Déconnexion'; + // Passages + case 'passage_created': return 'Passage créé'; + case 'passage_updated': return 'Passage modifié'; + case 'passage_deleted': return 'Passage supprimé'; + // Sectors + case 'sector_created': return 'Secteur créé'; + case 'sector_updated': return 'Secteur modifié'; + case 'sector_deleted': return 'Secteur supprimé'; + // Users + case 'user_created': return 'Utilisateur créé'; + case 'user_updated': return 'Utilisateur modifié'; + case 'user_deleted': return 'Utilisateur supprimé'; + // Entities + case 'entity_created': return 'Entité créée'; + case 'entity_updated': return 'Entité modifiée'; + case 'entity_deleted': return 'Entité supprimée'; + // Operations + case 'operation_created': return 'Opération créée'; + case 'operation_updated': return 'Opération modifiée'; + case 'operation_deleted': return 'Opération supprimée'; + // Stripe + case 'stripe_payment_created': return 'Paiement créé'; + case 'stripe_payment_success': return 'Paiement réussi'; + case 'stripe_payment_failed': return 'Paiement échoué'; + case 'stripe_payment_cancelled': return 'Paiement annulé'; + case 'stripe_terminal_error': return 'Erreur terminal'; + default: return eventType; + } + } + + /// Obtient la catégorie d'un type d'événement + static String getCategory(String eventType) { + if (eventType.startsWith('login') || eventType == 'logout') return 'auth'; + if (eventType.startsWith('passage')) return 'passages'; + if (eventType.startsWith('sector')) return 'sectors'; + if (eventType.startsWith('user')) return 'users'; + if (eventType.startsWith('entity')) return 'entities'; + if (eventType.startsWith('operation')) return 'operations'; + if (eventType.startsWith('stripe')) return 'stripe'; + return 'other'; + } +} + +// Helpers pour parser les types depuis JSON (gère int/string) +int _parseInt(dynamic value) { + if (value == null) return 0; + if (value is int) return value; + if (value is String) return int.tryParse(value) ?? 0; + if (value is double) return value.toInt(); + return 0; +} + +double _parseDouble(dynamic value) { + if (value == null) return 0.0; + if (value is double) return value; + if (value is int) return value.toDouble(); + if (value is String) return double.tryParse(value) ?? 0.0; + return 0.0; +} + +List _parseStringList(dynamic value) { + if (value == null) return []; + if (value is List) return value.map((e) => e.toString()).toList(); + return []; +} diff --git a/app/lib/core/repositories/operation_repository.dart b/app/lib/core/repositories/operation_repository.dart index 6818e2cf..a8618c06 100755 --- a/app/lib/core/repositories/operation_repository.dart +++ b/app/lib/core/repositories/operation_repository.dart @@ -247,10 +247,10 @@ class OperationRepository extends ChangeNotifier { debugPrint('✅ Opérations traitées'); } - // Traiter les secteurs (groupe secteurs) via DataLoadingService - if (responseData['secteurs'] != null) { + // Traiter les secteurs (groupe sectors) via DataLoadingService + if (responseData['sectors'] != null) { await DataLoadingService.instance - .processSectorsFromApi(responseData['secteurs']); + .processSectorsFromApi(responseData['sectors']); debugPrint('✅ Secteurs traités'); } @@ -521,10 +521,10 @@ class OperationRepository extends ChangeNotifier { debugPrint('✅ Opérations traitées'); } - // Traiter les secteurs (groupe secteurs) via DataLoadingService - if (responseData['secteurs'] != null) { + // Traiter les secteurs (groupe sectors) via DataLoadingService + if (responseData['sectors'] != null) { await DataLoadingService.instance - .processSectorsFromApi(responseData['secteurs']); + .processSectorsFromApi(responseData['sectors']); debugPrint('✅ Secteurs traités'); } diff --git a/app/lib/core/repositories/passage_repository.dart b/app/lib/core/repositories/passage_repository.dart index 520ca27f..98f4fa84 100755 --- a/app/lib/core/repositories/passage_repository.dart +++ b/app/lib/core/repositories/passage_repository.dart @@ -246,8 +246,9 @@ class PassageRepository extends ChangeNotifier { // Mode online : traitement normal if (response.statusCode == 201 || response.statusCode == 200) { - // Récupérer l'ID du nouveau passage depuis la réponse - final passageId = response.data['id'] is String ? int.parse(response.data['id']) : response.data['id'] as int; + // Récupérer l'ID du nouveau passage depuis la réponse (passage_id ou id) + final rawId = response.data['passage_id'] ?? response.data['id']; + final passageId = rawId is String ? int.parse(rawId) : rawId as int; // Créer le passage localement avec l'ID retourné par l'API final newPassage = passage.copyWith( @@ -431,10 +432,10 @@ class PassageRepository extends ChangeNotifier { return true; } - return false; + throw Exception('Mise à jour refusée par le serveur'); } catch (e) { debugPrint('Erreur lors de la mise à jour du passage: $e'); - return false; + rethrow; // Propager l'exception originale avec son message } finally { _isLoading = false; notifyListeners(); diff --git a/app/lib/core/services/api_service.dart b/app/lib/core/services/api_service.dart index 6e837b3f..269fb31f 100755 --- a/app/lib/core/services/api_service.dart +++ b/app/lib/core/services/api_service.dart @@ -2,6 +2,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:cookie_jar/cookie_jar.dart'; +import 'package:dio_cookie_manager/dio_cookie_manager.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:geosector_app/core/data/models/user_model.dart'; @@ -65,16 +68,44 @@ class ApiService { headers['X-App-Identifier'] = _appIdentifier; _dio.options.headers.addAll(headers); + // Gestionnaire de cookies pour les sessions PHP + // Utilise CookieJar en mémoire (cookies maintenus pendant la durée de vie de l'app) + final cookieJar = CookieJar(); + _dio.interceptors.add(CookieManager(cookieJar)); + debugPrint('🍪 [API] Gestionnaire de cookies activé'); + _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) { + debugPrint('🌐 [API] Requête: ${options.method} ${options.path}'); + debugPrint('🔑 [API] _sessionId présent: ${_sessionId != null}'); + debugPrint('🔑 [API] Headers: ${options.headers}'); if (_sessionId != null) { + debugPrint('🔑 [API] Token: Bearer ${_sessionId!.substring(0, 20)}...'); options.headers[AppKeys.sessionHeader] = 'Bearer $_sessionId'; + } else { + debugPrint('⚠️ [API] PAS DE TOKEN - Requête non authentifiée'); } handler.next(options); }, onError: (DioException error, handler) { if (error.response?.statusCode == 401) { - _sessionId = null; + final path = error.requestOptions.path; + debugPrint('❌ [API] Erreur 401 sur: $path'); + + // Ne pas reset le token pour les requêtes non critiques + final nonCriticalPaths = [ + '/users/device-info', + '/chat/rooms', + ]; + + final isNonCritical = nonCriticalPaths.any((p) => path.contains(p)); + + if (isNonCritical) { + debugPrint('⚠️ [API] Requête non critique - Token conservé'); + } else { + debugPrint('❌ [API] Requête critique - Token invalidé'); + _sessionId = null; + } } handler.next(error); }, @@ -1066,15 +1097,21 @@ class ApiService { if (data.containsKey('session_id')) { final sessionId = data['session_id']; if (sessionId != null) { + debugPrint('🔐 [LOGIN] Token reçu du serveur: $sessionId'); + debugPrint('🔐 [LOGIN] Longueur token: ${sessionId.toString().length}'); setSessionId(sessionId); + debugPrint('🔐 [LOGIN] Token stocké dans _sessionId'); // Collecter et envoyer les informations du device après login réussi - debugPrint('📱 Collecte des informations device après login...'); - DeviceInfoService.instance.collectAndSendDeviceInfo().then((_) { - debugPrint('✅ Informations device collectées et envoyées'); - }).catchError((error) { - debugPrint('⚠️ Erreur lors de l\'envoi des infos device: $error'); - // Ne pas bloquer le login si l'envoi des infos device échoue + // Délai de 1 seconde pour laisser la session PHP se stabiliser + debugPrint('📱 Collecte des informations device après login (délai 1s)...'); + Future.delayed(const Duration(seconds: 1), () { + DeviceInfoService.instance.collectAndSendDeviceInfo().then((_) { + debugPrint('✅ Informations device collectées et envoyées'); + }).catchError((error) { + debugPrint('⚠️ Erreur lors de l\'envoi des infos device: $error'); + // Ne pas bloquer le login si l'envoi des infos device échoue + }); }); } } diff --git a/app/lib/core/services/app_info_service.dart b/app/lib/core/services/app_info_service.dart index 2f768927..a4582da8 100755 --- a/app/lib/core/services/app_info_service.dart +++ b/app/lib/core/services/app_info_service.dart @@ -1,6 +1,6 @@ // ⚠️ AUTO-GENERATED FILE - DO NOT EDIT MANUALLY // This file is automatically generated by deploy-app.sh script -// Last update: 2025-11-09 12:39:26 +// Last update: 2026-01-16 13:37:45 // Source: ../VERSION file // // GEOSECTOR App Version Service @@ -8,10 +8,10 @@ class AppInfoService { // Version number (format: x.x.x) - static const String version = '3.5.2'; + static const String version = '3.6.2'; // Build number (version without dots: xxx) - static const String buildNumber = '352'; + static const String buildNumber = '362'; // Full version string (format: vx.x.x+xxx) static String get fullVersion => 'v$version+$buildNumber'; diff --git a/app/lib/core/services/device_info_service.dart b/app/lib/core/services/device_info_service.dart index f56fb499..117ffc5d 100644 --- a/app/lib/core/services/device_info_service.dart +++ b/app/lib/core/services/device_info_service.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:battery_plus/battery_plus.dart'; @@ -211,18 +212,18 @@ class DeviceInfoService { } bool _checkIosTapToPaySupport(String machine, String systemVersion) { - // iPhone XS et plus récents (liste des identifiants) - final supportedDevices = [ - 'iPhone11,', // XS, XS Max - 'iPhone12,', // 11, 11 Pro, 11 Pro Max - 'iPhone13,', // 12 series - 'iPhone14,', // 13 series - 'iPhone15,', // 14 series - 'iPhone16,', // 15 series - ]; + // Vérifier le modèle : iPhone11,x ou supérieur (iPhone XS+) + // Extraire le numéro majeur de l'identifiant (ex: "iPhone17,3" -> 17) + bool deviceSupported = false; - // Vérifier le modèle - bool deviceSupported = supportedDevices.any((prefix) => machine.startsWith(prefix)); + if (machine.startsWith('iPhone')) { + final match = RegExp(r'iPhone(\d+),').firstMatch(machine); + if (match != null) { + final majorVersion = int.tryParse(match.group(1) ?? '0') ?? 0; + // iPhone XS = iPhone11,x donc >= 11 pour supporter Tap to Pay + deviceSupported = majorVersion >= 11; + } + } // Vérifier la version iOS (16.4+ selon la documentation officielle Stripe) final versionParts = systemVersion.split('.'); @@ -334,10 +335,10 @@ class DeviceInfoService { return deviceInfo; } - /// Vérifie la certification Stripe Tap to Pay via l'API + /// Vérifie la certification Stripe Tap to Pay via le SDK Stripe Terminal Future checkStripeCertification() async { try { - // Sur Web, toujours non certifié + // Sur Web, toujours non supporté if (kIsWeb) { debugPrint('📱 Web platform - Tap to Pay non supporté'); return false; @@ -354,33 +355,35 @@ class DeviceInfoService { return isSupported; } - // Android : vérification via l'API Stripe + // Android : vérification des pré-requis hardware de base + // Note: Le vrai check de compatibilité avec découverte de readers se fera + // dans StripeTapToPayService lors du premier paiement if (Platform.isAndroid) { final androidInfo = await _deviceInfo.androidInfo; + debugPrint('📱 Vérification Tap to Pay pour ${androidInfo.manufacturer} ${androidInfo.model}'); - debugPrint('📱 Vérification certification Stripe pour ${androidInfo.manufacturer} ${androidInfo.model}'); - - try { - final response = await ApiService.instance.post( - '/stripe/devices/check-tap-to-pay', - data: { - 'platform': 'android', - 'manufacturer': androidInfo.manufacturer, - 'model': androidInfo.model, - }, - ); - - final tapToPaySupported = response.data['tap_to_pay_supported'] == true; - final message = response.data['message'] ?? ''; - - debugPrint('📱 Résultat certification Stripe: $tapToPaySupported - $message'); - return tapToPaySupported; - - } catch (e) { - debugPrint('❌ Erreur lors de la vérification Stripe: $e'); - // En cas d'erreur API, on se base sur la vérification locale - return androidInfo.version.sdkInt >= 28; + // Vérifications préalables de base + if (androidInfo.version.sdkInt < 28) { + debugPrint('❌ Android SDK trop ancien: ${androidInfo.version.sdkInt} (minimum 28)'); + return false; } + + // Vérifier la disponibilité du NFC + try { + final nfcAvailable = await NfcManager.instance.isAvailable(); + if (!nfcAvailable) { + debugPrint('❌ NFC non disponible sur cet appareil'); + return false; + } + debugPrint('✅ NFC disponible'); + } catch (e) { + debugPrint('⚠️ Impossible de vérifier NFC: $e'); + // On continue quand même, ce n'est pas bloquant à ce stade + } + + // Pré-requis de base OK + debugPrint('✅ Pré-requis Android OK (SDK ${androidInfo.version.sdkInt}, NFC disponible)'); + return true; } return false; @@ -390,22 +393,89 @@ class DeviceInfoService { } } + /// Vérifie si le device peut utiliser Tap to Pay bool canUseTapToPay() { final deviceInfo = getStoredDeviceInfo(); - // Vérifications requises - final nfcCapable = deviceInfo['device_nfc_capable'] == true; - // Utiliser la certification Stripe si disponible, sinon l'ancienne vérification + // checkStripeCertification() fait déjà toutes les vérifications (modèle, iOS version, NFC Android) final stripeCertified = deviceInfo['device_stripe_certified'] ?? deviceInfo['device_supports_tap_to_pay']; final batteryLevel = deviceInfo['battery_level'] as int?; // Batterie minimum 10% pour les paiements final sufficientBattery = batteryLevel != null && batteryLevel >= 10; - return nfcCapable && stripeCertified == true && sufficientBattery; + return stripeCertified == true && sufficientBattery; } - /// Stream pour surveiller les changements de batterie - Stream get batteryStateStream => _battery.onBatteryStateChanged; + /// Retourne la raison pour laquelle Tap to Pay n'est pas disponible + /// Retourne null si Tap to Pay est disponible + String? getTapToPayUnavailableReason() { + // Sur Web, Tap to Pay n'est jamais disponible + if (kIsWeb) { + return 'Tap to Pay non disponible sur Web'; + } + + final deviceInfo = getStoredDeviceInfo(); + + // Vérifier la batterie + final batteryLevel = deviceInfo['battery_level'] as int?; + if (batteryLevel == null) { + return 'Niveau de batterie inconnu'; + } + if (batteryLevel < 10) { + return 'Batterie insuffisante ($batteryLevel%) - Min. 10% requis'; + } + + // Vérifier la certification Stripe (inclut déjà modèle, iOS version, NFC Android) + final stripeCertified = deviceInfo['device_stripe_certified'] ?? deviceInfo['device_supports_tap_to_pay']; + if (stripeCertified != true) { + return 'Appareil non certifié pour Tap to Pay'; + } + + // Tout est OK + return null; + } + + /// Version asynchrone avec vérification NFC en temps réel (Android uniquement) + Future getTapToPayUnavailableReasonAsync() async { + // Sur Web, Tap to Pay n'est jamais disponible + if (kIsWeb) { + return 'Tap to Pay non disponible sur Web'; + } + + final deviceInfo = getStoredDeviceInfo(); + final platform = deviceInfo['platform']; + + // Vérifier la batterie + final batteryLevel = deviceInfo['battery_level'] as int?; + if (batteryLevel == null) { + return 'Niveau de batterie inconnu'; + } + if (batteryLevel < 10) { + return 'Batterie insuffisante ($batteryLevel%) - Min. 10% requis'; + } + + // Sur Android, vérifier le NFC EN TEMPS RÉEL (peut être désactivé dans les paramètres) + if (platform == 'Android') { + try { + final nfcAvailable = await NfcManager.instance.isAvailable(); + if (!nfcAvailable) { + return 'NFC désactivé - Activez-le dans les paramètres Android'; + } + } catch (e) { + debugPrint('⚠️ Impossible de vérifier le statut NFC: $e'); + return 'Impossible de vérifier le NFC'; + } + } + + // Vérifier la certification Stripe (inclut déjà modèle, iOS version) + final stripeCertified = deviceInfo['device_stripe_certified'] ?? deviceInfo['device_supports_tap_to_pay']; + if (stripeCertified != true) { + return 'Appareil non certifié pour Tap to Pay'; + } + + // Tout est OK + return null; + } } \ No newline at end of file diff --git a/app/lib/core/services/event_stats_service.dart b/app/lib/core/services/event_stats_service.dart new file mode 100644 index 00000000..f9d0af2f --- /dev/null +++ b/app/lib/core/services/event_stats_service.dart @@ -0,0 +1,312 @@ +import 'package:flutter/foundation.dart'; +import 'package:geosector_app/core/services/api_service.dart'; +import 'package:geosector_app/core/data/models/event_stats_model.dart'; +import 'package:geosector_app/core/utils/api_exception.dart'; +import 'package:intl/intl.dart'; + +/// Service pour récupérer les statistiques d'événements depuis l'API. +/// +/// Ce service est un singleton qui gère les appels API vers les endpoints +/// /api/events/stats/*. Il est accessible uniquement aux admins (rôle >= 2). +class EventStatsService { + static EventStatsService? _instance; + + EventStatsService._internal(); + + static EventStatsService get instance { + _instance ??= EventStatsService._internal(); + return _instance!; + } + + final _dateFormat = DateFormat('yyyy-MM-dd'); + + /// Récupère le résumé des stats pour une date donnée. + /// + /// GET /api/events/stats/summary?date=YYYY-MM-DD&entity_id=X + /// + /// [date] : Date à récupérer (défaut: aujourd'hui) + /// [entityId] : ID de l'entité (super-admin uniquement, optionnel) + Future getSummary({ + DateTime? date, + int? entityId, + }) async { + try { + final queryParams = {}; + + if (date != null) { + queryParams['date'] = _dateFormat.format(date); + } + + if (entityId != null) { + queryParams['entity_id'] = entityId.toString(); + } + + debugPrint('📊 [EventStats] Récupération résumé: $queryParams'); + + final response = await ApiService.instance.get( + '/events/stats/summary', + queryParameters: queryParams.isNotEmpty ? queryParams : null, + ); + + if (response.data['status'] == 'success') { + return EventSummary.fromJson(response.data['data']); + } else { + throw ApiException( + response.data['message'] ?? 'Erreur lors de la récupération du résumé', + ); + } + } catch (e) { + debugPrint('❌ [EventStats] Erreur getSummary: $e'); + if (e is ApiException) rethrow; + throw ApiException('Erreur lors de la récupération des statistiques', originalError: e); + } + } + + /// Récupère les stats quotidiennes pour une période. + /// + /// GET /api/events/stats/daily?from=YYYY-MM-DD&to=YYYY-MM-DD&events=type1,type2 + /// + /// [from] : Date de début (obligatoire) + /// [to] : Date de fin (obligatoire) + /// [events] : Types d'événements à filtrer (optionnel) + /// [entityId] : ID de l'entité (super-admin uniquement, optionnel) + /// + /// Limite : 90 jours maximum + Future getDailyStats({ + required DateTime from, + required DateTime to, + List? events, + int? entityId, + }) async { + try { + // Vérifier la limite de 90 jours + final daysDiff = to.difference(from).inDays; + if (daysDiff > 90) { + throw const ApiException('La période ne peut pas dépasser 90 jours'); + } + + final queryParams = { + 'from': _dateFormat.format(from), + 'to': _dateFormat.format(to), + }; + + if (events != null && events.isNotEmpty) { + queryParams['events'] = events.join(','); + } + + if (entityId != null) { + queryParams['entity_id'] = entityId.toString(); + } + + debugPrint('📊 [EventStats] Récupération stats quotidiennes: $queryParams'); + + final response = await ApiService.instance.get( + '/events/stats/daily', + queryParameters: queryParams, + ); + + if (response.data['status'] == 'success') { + return DailyStats.fromJson(response.data['data']); + } else { + throw ApiException( + response.data['message'] ?? 'Erreur lors de la récupération des stats quotidiennes', + ); + } + } catch (e) { + debugPrint('❌ [EventStats] Erreur getDailyStats: $e'); + if (e is ApiException) rethrow; + throw ApiException('Erreur lors de la récupération des statistiques quotidiennes', originalError: e); + } + } + + /// Récupère les stats hebdomadaires pour une période. + /// + /// GET /api/events/stats/weekly?from=YYYY-MM-DD&to=YYYY-MM-DD&events=type1,type2 + /// + /// [from] : Date de début (obligatoire) + /// [to] : Date de fin (obligatoire) + /// [events] : Types d'événements à filtrer (optionnel) + /// [entityId] : ID de l'entité (super-admin uniquement, optionnel) + /// + /// Limite : 365 jours maximum + Future getWeeklyStats({ + required DateTime from, + required DateTime to, + List? events, + int? entityId, + }) async { + try { + // Vérifier la limite de 365 jours + final daysDiff = to.difference(from).inDays; + if (daysDiff > 365) { + throw const ApiException('La période ne peut pas dépasser 365 jours'); + } + + final queryParams = { + 'from': _dateFormat.format(from), + 'to': _dateFormat.format(to), + }; + + if (events != null && events.isNotEmpty) { + queryParams['events'] = events.join(','); + } + + if (entityId != null) { + queryParams['entity_id'] = entityId.toString(); + } + + debugPrint('📊 [EventStats] Récupération stats hebdomadaires: $queryParams'); + + final response = await ApiService.instance.get( + '/events/stats/weekly', + queryParameters: queryParams, + ); + + if (response.data['status'] == 'success') { + return WeeklyStats.fromJson(response.data['data']); + } else { + throw ApiException( + response.data['message'] ?? 'Erreur lors de la récupération des stats hebdomadaires', + ); + } + } catch (e) { + debugPrint('❌ [EventStats] Erreur getWeeklyStats: $e'); + if (e is ApiException) rethrow; + throw ApiException('Erreur lors de la récupération des statistiques hebdomadaires', originalError: e); + } + } + + /// Récupère les stats mensuelles pour une année. + /// + /// GET /api/events/stats/monthly?year=YYYY&events=type1,type2 + /// + /// [year] : Année (défaut: année courante) + /// [events] : Types d'événements à filtrer (optionnel) + /// [entityId] : ID de l'entité (super-admin uniquement, optionnel) + Future getMonthlyStats({ + int? year, + List? events, + int? entityId, + }) async { + try { + final queryParams = {}; + + if (year != null) { + queryParams['year'] = year.toString(); + } + + if (events != null && events.isNotEmpty) { + queryParams['events'] = events.join(','); + } + + if (entityId != null) { + queryParams['entity_id'] = entityId.toString(); + } + + debugPrint('📊 [EventStats] Récupération stats mensuelles: $queryParams'); + + final response = await ApiService.instance.get( + '/events/stats/monthly', + queryParameters: queryParams.isNotEmpty ? queryParams : null, + ); + + if (response.data['status'] == 'success') { + return MonthlyStats.fromJson(response.data['data']); + } else { + throw ApiException( + response.data['message'] ?? 'Erreur lors de la récupération des stats mensuelles', + ); + } + } catch (e) { + debugPrint('❌ [EventStats] Erreur getMonthlyStats: $e'); + if (e is ApiException) rethrow; + throw ApiException('Erreur lors de la récupération des statistiques mensuelles', originalError: e); + } + } + + /// Récupère les détails des événements pour une date. + /// + /// GET /api/events/stats/details?date=YYYY-MM-DD&event=type&limit=50&offset=0 + /// + /// [date] : Date à récupérer (obligatoire) + /// [event] : Type d'événement à filtrer (optionnel) + /// [limit] : Nombre de résultats max (défaut: 50, max: 100) + /// [offset] : Pagination (défaut: 0) + /// [entityId] : ID de l'entité (super-admin uniquement, optionnel) + Future getDetails({ + required DateTime date, + String? event, + int? limit, + int? offset, + int? entityId, + }) async { + try { + final queryParams = { + 'date': _dateFormat.format(date), + }; + + if (event != null && event.isNotEmpty) { + queryParams['event'] = event; + } + + if (limit != null) { + queryParams['limit'] = limit.toString(); + } + + if (offset != null) { + queryParams['offset'] = offset.toString(); + } + + if (entityId != null) { + queryParams['entity_id'] = entityId.toString(); + } + + debugPrint('📊 [EventStats] Récupération détails: $queryParams'); + + final response = await ApiService.instance.get( + '/events/stats/details', + queryParameters: queryParams, + ); + + if (response.data['status'] == 'success') { + return EventDetails.fromJson(response.data['data']); + } else { + throw ApiException( + response.data['message'] ?? 'Erreur lors de la récupération des détails', + ); + } + } catch (e) { + debugPrint('❌ [EventStats] Erreur getDetails: $e'); + if (e is ApiException) rethrow; + throw ApiException('Erreur lors de la récupération des détails', originalError: e); + } + } + + /// Récupère les types d'événements disponibles. + /// + /// GET /api/events/stats/types + Future getEventTypes() async { + try { + debugPrint('📊 [EventStats] Récupération types d\'événements'); + + final response = await ApiService.instance.get('/events/stats/types'); + + if (response.data['status'] == 'success') { + return EventTypes.fromJson(response.data['data']); + } else { + throw ApiException( + response.data['message'] ?? 'Erreur lors de la récupération des types', + ); + } + } catch (e) { + debugPrint('❌ [EventStats] Erreur getEventTypes: $e'); + if (e is ApiException) rethrow; + throw ApiException('Erreur lors de la récupération des types d\'événements', originalError: e); + } + } + + /// Réinitialise le singleton (pour les tests) + static void reset() { + _instance = null; + } +} diff --git a/app/lib/core/services/stripe_tap_to_pay_service.dart b/app/lib/core/services/stripe_tap_to_pay_service.dart index 8fb42ca4..b7d3dc71 100644 --- a/app/lib/core/services/stripe_tap_to_pay_service.dart +++ b/app/lib/core/services/stripe_tap_to_pay_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:mek_stripe_terminal/mek_stripe_terminal.dart'; import 'api_service.dart'; import 'device_info_service.dart'; @@ -13,6 +14,7 @@ class StripeTapToPayService { StripeTapToPayService._internal(); bool _isInitialized = false; + static bool _terminalInitialized = false; // Flag STATIC pour savoir si Terminal.initTerminal a été appelé (global à l'app) String? _stripeAccountId; String? _locationId; bool _deviceCompatible = false; @@ -78,6 +80,36 @@ class StripeTapToPayService { return false; } + // 4. Initialiser le SDK Stripe Terminal (une seule fois par session app) + if (!_terminalInitialized) { + try { + debugPrint('🔧 Initialisation du SDK Stripe Terminal...'); + await Terminal.initTerminal( + fetchToken: _fetchConnectionToken, + ); + _terminalInitialized = true; + debugPrint('✅ SDK Stripe Terminal initialisé'); + } catch (e) { + final errorMsg = e.toString().toLowerCase(); + debugPrint('🔍 Exception capturée lors de l\'initialisation: $e'); + debugPrint('🔍 Type d\'exception: ${e.runtimeType}'); + + // Vérifier plusieurs variantes du message "already initialized" + if (errorMsg.contains('already initialized') || + errorMsg.contains('already been initialized') || + errorMsg.contains('sdkfailure')) { + debugPrint('ℹ️ SDK Stripe Terminal déjà initialisé (détecté via exception)'); + _terminalInitialized = true; + // Ne PAS rethrow - continuer normalement car c'est un état valide + } else { + debugPrint('❌ Erreur inattendue lors de l\'initialisation du SDK'); + rethrow; // Autre erreur, on la propage + } + } + } else { + debugPrint('ℹ️ SDK Stripe Terminal déjà initialisé, réutilisation'); + } + _isInitialized = true; debugPrint('✅ Tap to Pay initialisé avec succès'); @@ -101,6 +133,34 @@ class StripeTapToPayService { } } + /// Callback pour récupérer un token de connexion depuis l'API + Future _fetchConnectionToken() async { + try { + debugPrint('🔑 Récupération du token de connexion Stripe...'); + + final response = await ApiService.instance.post( + '/stripe/terminal/connection-token', + data: { + 'amicale_id': CurrentAmicaleService.instance.amicaleId, + 'stripe_account': _stripeAccountId, + 'location_id': _locationId, + } + ); + + final token = response.data['secret']; + if (token == null || token.isEmpty) { + throw Exception('Token de connexion invalide'); + } + + debugPrint('✅ Token de connexion récupéré'); + return token; + + } catch (e) { + debugPrint('❌ Erreur récupération token: $e'); + throw Exception('Impossible de récupérer le token de connexion'); + } + } + /// Crée un PaymentIntent pour un paiement Tap to Pay Future createPaymentIntent({ required int amountInCents, @@ -124,21 +184,25 @@ class StripeTapToPayService { // Extraire passage_id des metadata si présent final passageId = metadata?['passage_id'] ?? '0'; + final requestData = { + 'amount': amountInCents, + 'currency': 'eur', + 'description': description ?? 'Calendrier pompiers', + 'payment_method_types': ['card_present'], // Pour Tap to Pay + 'capture_method': 'automatic', + 'passage_id': int.tryParse(passageId.toString()) ?? 0, + 'amicale_id': CurrentAmicaleService.instance.amicaleId, + 'member_id': CurrentUserService.instance.userId, + 'stripe_account': _stripeAccountId, + 'location_id': _locationId, + 'metadata': metadata, + }; + + debugPrint('🔵 Données envoyées create-intent: $requestData'); + final response = await ApiService.instance.post( '/stripe/payments/create-intent', - data: { - 'amount': amountInCents, - 'currency': 'eur', - 'description': description ?? 'Calendrier pompiers', - 'payment_method_types': ['card_present'], // Pour Tap to Pay - 'capture_method': 'automatic', - 'passage_id': int.tryParse(passageId.toString()) ?? 0, - 'amicale_id': CurrentAmicaleService.instance.amicaleId, - 'member_id': CurrentUserService.instance.userId, - 'stripe_account': _stripeAccountId, - 'location_id': _locationId, - 'metadata': metadata, - }, + data: requestData, ); final result = PaymentIntentResult( @@ -169,11 +233,110 @@ class StripeTapToPayService { } } - /// Simule le processus de collecte de paiement - /// (Dans la version finale, cela appellera le SDK natif) + /// Découvre et connecte le reader Tap to Pay local + Future _ensureReaderConnected() async { + try { + debugPrint('🔍 Découverte du reader Tap to Pay...'); + + // Configuration pour découvrir le reader local (Tap to Pay) + // Détection de l'environnement via l'URL de l'API (plus fiable que kDebugMode) + final apiUrl = ApiService.instance.baseUrl; + final isProduction = apiUrl.contains('app3.geosector.fr'); + final isSimulated = !isProduction; // Simulé uniquement si pas en PROD + + final config = TapToPayDiscoveryConfiguration( + isSimulated: isSimulated, + ); + + debugPrint('🔧 Environnement: ${isProduction ? "PRODUCTION (réel)" : "DEV/REC (simulé)"}'); + debugPrint('🔧 isSimulated: $isSimulated'); + + // Découvrir les readers avec un Completer pour gérer le stream correctement + final completer = Completer(); + StreamSubscription>? subscription; + + subscription = Terminal.instance.discoverReaders(config).listen( + (readers) { + debugPrint('📡 Stream readers reçu: ${readers.length} reader(s)'); + if (readers.isNotEmpty && !completer.isCompleted) { + debugPrint('📱 ${readers.length} reader(s) trouvé(s): ${readers.map((r) => r.label).join(", ")}'); + completer.complete(readers.first); + subscription?.cancel(); + } + }, + onError: (error) { + debugPrint('❌ Erreur lors de la découverte: $error'); + if (!completer.isCompleted) { + completer.complete(null); + } + subscription?.cancel(); + }, + onDone: () { + debugPrint('🏁 Stream découverte terminé'); + if (!completer.isCompleted) { + debugPrint('⚠️ Découverte terminée sans reader trouvé'); + completer.complete(null); + } + }, + ); + + debugPrint('⏳ Attente du résultat de la découverte...'); + + // Attendre le résultat avec timeout + final reader = await completer.future.timeout( + const Duration(seconds: 15), + onTimeout: () { + debugPrint('⏱️ Timeout lors de la découverte du reader'); + subscription?.cancel(); + return null; + }, + ); + + if (reader == null) { + debugPrint('❌ Aucun reader Tap to Pay trouvé'); + return false; + } + + debugPrint('📱 Reader trouvé: ${reader.label}'); + + // Se connecter au reader + debugPrint('🔌 Connexion au reader...'); + final connectionConfig = TapToPayConnectionConfiguration( + locationId: _locationId ?? '', + readerDelegate: null, // Pas de delegate pour l'instant + ); + + await Terminal.instance.connectReader( + reader, + configuration: connectionConfig, + ); + + debugPrint('✅ Connecté au reader Tap to Pay'); + return true; + + } catch (e) { + debugPrint('❌ Erreur connexion reader: $e'); + return false; + } + } + + /// Collecte le paiement avec le SDK Stripe Terminal Future collectPayment(PaymentIntentResult paymentIntent) async { try { - debugPrint('💳 Collecte du paiement...'); + debugPrint('💳 Collecte du paiement avec SDK...'); + + _paymentStatusController.add(TapToPayStatus( + type: TapToPayStatusType.processing, + message: 'Préparation du terminal...', + paymentIntentId: paymentIntent.paymentIntentId, + )); + + // 1. S'assurer qu'un reader est connecté + debugPrint('🔌 Vérification connexion reader...'); + final readerConnected = await _ensureReaderConnected(); + if (!readerConnected) { + throw Exception('Impossible de se connecter au reader Tap to Pay'); + } _paymentStatusController.add(TapToPayStatus( type: TapToPayStatusType.processing, @@ -181,11 +344,22 @@ class StripeTapToPayService { paymentIntentId: paymentIntent.paymentIntentId, )); - // TODO: Ici, intégrer le vrai SDK Stripe Terminal - // Pour l'instant, on simule une attente - await Future.delayed(const Duration(seconds: 2)); + // 2. Récupérer le PaymentIntent depuis le SDK avec le clientSecret + debugPrint('💳 Récupération du PaymentIntent...'); + final stripePaymentIntent = await Terminal.instance.retrievePaymentIntent( + paymentIntent.clientSecret, + ); - debugPrint('✅ Paiement collecté'); + // 3. Utiliser le SDK Stripe Terminal pour collecter le paiement + debugPrint('💳 En attente du paiement sans contact...'); + final collectedPaymentIntent = await Terminal.instance.collectPaymentMethod( + stripePaymentIntent, + ); + + // Sauvegarder le PaymentIntent collecté pour l'étape de confirmation + paymentIntent._collectedPaymentIntent = collectedPaymentIntent; + + debugPrint('✅ Paiement collecté via SDK'); _paymentStatusController.add(TapToPayStatus( type: TapToPayStatusType.confirming, @@ -208,33 +382,37 @@ class StripeTapToPayService { } } - /// Confirme le paiement auprès du serveur + /// Confirme le paiement via le SDK Stripe Terminal Future confirmPayment(PaymentIntentResult paymentIntent) async { try { - debugPrint('✅ Confirmation du paiement...'); + debugPrint('✅ Confirmation du paiement via SDK...'); - // Notifier le serveur du succès - await ApiService.instance.post( - '/stripe/payments/confirm', - data: { - 'payment_intent_id': paymentIntent.paymentIntentId, - 'amount': paymentIntent.amount, - 'status': 'succeeded', - 'amicale_id': CurrentAmicaleService.instance.amicaleId, - 'member_id': CurrentUserService.instance.userId, - }, + // Vérifier que le paiement a été collecté + if (paymentIntent._collectedPaymentIntent == null) { + throw Exception('Le paiement doit d\'abord être collecté'); + } + + // Utiliser le SDK Stripe Terminal pour confirmer le paiement + final confirmedPaymentIntent = await Terminal.instance.confirmPaymentIntent( + paymentIntent._collectedPaymentIntent!, ); - debugPrint('🎉 Paiement confirmé avec succès'); + // Vérifier le statut final + if (confirmedPaymentIntent.status == PaymentIntentStatus.succeeded) { + debugPrint('🎉 Paiement confirmé avec succès via SDK'); + debugPrint(' Payment Intent: ${paymentIntent.paymentIntentId}'); - _paymentStatusController.add(TapToPayStatus( - type: TapToPayStatusType.success, - message: 'Paiement réussi', - paymentIntentId: paymentIntent.paymentIntentId, - amount: paymentIntent.amount, - )); + _paymentStatusController.add(TapToPayStatus( + type: TapToPayStatusType.success, + message: 'Paiement réussi', + paymentIntentId: paymentIntent.paymentIntentId, + amount: paymentIntent.amount, + )); - return true; + return true; + } else { + throw Exception('Paiement non confirmé: ${confirmedPaymentIntent.status}'); + } } catch (e) { debugPrint('❌ Erreur confirmation paiement: $e'); @@ -304,6 +482,9 @@ class PaymentIntentResult { final String clientSecret; final int amount; + // PaymentIntent collecté (utilisé entre collectPayment et confirmPayment) + PaymentIntent? _collectedPaymentIntent; + PaymentIntentResult({ required this.paymentIntentId, required this.clientSecret, diff --git a/app/lib/core/utils/api_exception.dart b/app/lib/core/utils/api_exception.dart index d7459f52..6615cce8 100755 --- a/app/lib/core/utils/api_exception.dart +++ b/app/lib/core/utils/api_exception.dart @@ -31,6 +31,7 @@ class ApiException implements Exception { if (response?.data != null) { try { final data = response!.data as Map; + debugPrint('🔍 API Error Response: $data'); // Message spécifique de l'API if (data.containsKey('message')) { @@ -42,12 +43,21 @@ class ApiException implements Exception { errorCode = data['error_code'] as String; } - // Détails supplémentaires + // Détails supplémentaires - peut être une Map ou une List if (data.containsKey('errors')) { - details = data['errors'] as Map?; + final errorsData = data['errors']; + if (errorsData is Map) { + // Format: {field: [errors]} + details = errorsData; + } else if (errorsData is List) { + // Format: [error1, error2, ...] + details = {'errors': errorsData}; + } + debugPrint('🔍 Validation Errors: $details'); } } catch (e) { // Si on ne peut pas parser la réponse, utiliser le message par défaut + debugPrint('⚠️ Impossible de parser la réponse d\'erreur: $e'); } } @@ -130,7 +140,43 @@ class ApiException implements Exception { String toString() => message; /// Obtenir un message d'erreur formaté pour l'affichage - String get displayMessage => message; + String get displayMessage { + debugPrint('🔍 [displayMessage] statusCode: $statusCode'); + debugPrint('🔍 [displayMessage] isValidationError: $isValidationError'); + debugPrint('🔍 [displayMessage] details: $details'); + debugPrint('🔍 [displayMessage] details != null: ${details != null}'); + debugPrint('🔍 [displayMessage] details!.isNotEmpty: ${details != null ? details!.isNotEmpty : "null"}'); + + // Si c'est une erreur de validation avec des détails, formater le message + if (isValidationError && details != null && details!.isNotEmpty) { + debugPrint('✅ [displayMessage] Formatage des erreurs de validation'); + final buffer = StringBuffer(message); + buffer.write('\n'); + + details!.forEach((field, errors) { + if (errors is List) { + // Si le champ est 'errors', c'est une liste simple d'erreurs + if (field == 'errors') { + for (final error in errors) { + buffer.write('• $error\n'); + } + } else { + // Sinon c'est un champ avec une liste d'erreurs + for (final error in errors) { + buffer.write('• $field: $error\n'); + } + } + } else { + buffer.write('• $field: $errors\n'); + } + }); + + return buffer.toString().trim(); + } + + debugPrint('⚠️ [displayMessage] Retour du message simple'); + return message; + } /// Vérifier si c'est une erreur de validation bool get isValidationError => statusCode == 422 || statusCode == 400; diff --git a/app/lib/presentation/admin/admin_connexions_page.dart b/app/lib/presentation/admin/admin_connexions_page.dart new file mode 100644 index 00000000..928ccbca --- /dev/null +++ b/app/lib/presentation/admin/admin_connexions_page.dart @@ -0,0 +1,1135 @@ +import 'package:flutter/material.dart'; +import 'package:geosector_app/core/repositories/user_repository.dart'; +import 'package:geosector_app/core/repositories/amicale_repository.dart'; +import 'package:geosector_app/core/services/event_stats_service.dart'; +import 'package:geosector_app/core/services/current_user_service.dart'; +import 'package:geosector_app/core/data/models/event_stats_model.dart'; +import 'package:geosector_app/core/data/models/amicale_model.dart'; +import 'package:geosector_app/core/utils/api_exception.dart'; +import 'package:intl/intl.dart'; + +/// Mode de sélection de la période +enum DateSelectionMode { + /// Une seule date + singleDay, + /// Plage de dates personnalisée + dateRange, + /// Derniers X jours + lastDays, +} + +/// Page d'administration des connexions et événements. +/// +/// Affiche les statistiques de connexion et d'activité : +/// - Pour les admins amicale : données de leur amicale uniquement +/// - Pour les super-admins : données globales avec filtre par amicale +class AdminConnexionsPage extends StatefulWidget { + final UserRepository userRepository; + final AmicaleRepository amicaleRepository; + + const AdminConnexionsPage({ + super.key, + required this.userRepository, + required this.amicaleRepository, + }); + + @override + State createState() => _AdminConnexionsPageState(); +} + +class _AdminConnexionsPageState extends State { + final _eventStatsService = EventStatsService.instance; + final _dateFormat = DateFormat('dd/MM/yyyy'); + final _timeFormat = DateFormat('HH:mm'); + + // État de chargement + bool _isLoading = true; + String? _error; + + // Données + EventSummary? _summary; + DailyStats? _dailyStats; + EventDetails? _details; + + // Mode de sélection de période + DateSelectionMode _selectionMode = DateSelectionMode.lastDays; + + // Pour mode "Jour unique" + DateTime _singleDate = DateTime.now(); + + // Pour mode "Période personnalisée" + DateTime _startDate = DateTime.now().subtract(const Duration(days: 6)); + DateTime _endDate = DateTime.now(); + + // Pour mode "Derniers jours" + int _lastDays = 7; + + // Filtre par type d'événement + String? _selectedEventType; + + // Filtre par entité (super-admin uniquement) + int? _selectedEntityId; + List _amicales = []; + bool _isSuperAdmin = false; + + @override + void initState() { + super.initState(); + _isSuperAdmin = CurrentUserService.instance.isSuperAdmin; + if (_isSuperAdmin) { + _loadAmicales(); + } + _loadData(); + } + + /// Charge la liste des amicales pour le filtre super-admin + void _loadAmicales() { + try { + _amicales = widget.amicaleRepository.getAllAmicales(); + // Trier par nom + _amicales.sort((a, b) => a.name.compareTo(b.name)); + } catch (e) { + debugPrint('Erreur chargement amicales: $e'); + } + } + + /// Calcule les dates de début et fin selon le mode sélectionné + (DateTime, DateTime) _getDateRange() { + switch (_selectionMode) { + case DateSelectionMode.singleDay: + return (_singleDate, _singleDate); + case DateSelectionMode.dateRange: + return (_startDate, _endDate); + case DateSelectionMode.lastDays: + final end = DateTime.now(); + final start = end.subtract(Duration(days: _lastDays - 1)); + return (start, end); + } + } + + /// Vérifie si on affiche une période (plusieurs jours) + bool get _isMultipleDays { + final (start, end) = _getDateRange(); + return !start.isAtSameMomentAs(end) && + (end.difference(start).inDays > 0 || _selectionMode != DateSelectionMode.singleDay); + } + + Future _loadData() async { + setState(() { + _isLoading = true; + _error = null; + }); + + try { + final (startDate, endDate) = _getDateRange(); + + // Charger les données selon le mode + if (_selectionMode == DateSelectionMode.singleDay) { + // Mode jour unique : résumé + détails du jour + final results = await Future.wait([ + _eventStatsService.getSummary( + date: _singleDate, + entityId: _selectedEntityId, + ), + _eventStatsService.getDetails( + date: _singleDate, + event: _selectedEventType, + entityId: _selectedEntityId, + limit: 50, + ), + ]); + + setState(() { + _summary = results[0] as EventSummary; + _dailyStats = null; // Pas de graphique en mode jour unique + _details = results[1] as EventDetails; + _isLoading = false; + }); + } else { + // Mode période : stats quotidiennes + détails du dernier jour + final results = await Future.wait([ + _eventStatsService.getDailyStats( + from: startDate, + to: endDate, + entityId: _selectedEntityId, + ), + _eventStatsService.getDetails( + date: endDate, + event: _selectedEventType, + entityId: _selectedEntityId, + limit: 50, + ), + ]); + + final dailyStats = results[0] as DailyStats; + + // Calculer le résumé agrégé depuis les stats quotidiennes + setState(() { + _dailyStats = dailyStats; + _summary = _computeAggregatedSummary(dailyStats); + _details = results[1] as EventDetails; + _isLoading = false; + }); + } + } catch (e) { + setState(() { + _error = e is ApiException ? e.message : 'Erreur lors du chargement des données'; + _isLoading = false; + }); + } + } + + /// Calcule un résumé agrégé à partir des stats quotidiennes + EventSummary _computeAggregatedSummary(DailyStats dailyStats) { + int authSuccess = 0; + int authFailed = 0; + int authLogout = 0; + int passagesCreated = 0; + double passagesAmount = 0; + int stripeSuccess = 0; + double stripeAmount = 0; + int totalEvents = 0; + final uniqueUsersSet = {}; + + for (final day in dailyStats.days) { + totalEvents += day.totalCount; + + // Auth events + if (day.events.containsKey('login_success')) { + authSuccess += day.events['login_success']!.count; + } + if (day.events.containsKey('login_failed')) { + authFailed += day.events['login_failed']!.count; + } + if (day.events.containsKey('logout')) { + authLogout += day.events['logout']!.count; + } + + // Passages + if (day.events.containsKey('passage_created')) { + passagesCreated += day.events['passage_created']!.count; + passagesAmount += day.events['passage_created']!.sumAmount; + } + + // Stripe + if (day.events.containsKey('stripe_payment_success')) { + stripeSuccess += day.events['stripe_payment_success']!.count; + stripeAmount += day.events['stripe_payment_success']!.sumAmount; + } + } + + return EventSummary( + date: dailyStats.to, + stats: DayStats( + auth: AuthStats(success: authSuccess, failed: authFailed, logout: authLogout), + passages: PassageStats(created: passagesCreated, updated: 0, deleted: 0, amount: passagesAmount), + users: const UserStats(created: 0, updated: 0, deleted: 0), + sectors: const SectorStats(created: 0, updated: 0, deleted: 0), + stripe: StripeStats(created: 0, success: stripeSuccess, failed: 0, cancelled: 0, amount: stripeAmount), + ), + totals: DayTotals(events: totalEvents, uniqueUsers: uniqueUsersSet.length), + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final size = MediaQuery.of(context).size; + final isMobile = size.width <= 600; + + return SafeArea( + child: RefreshIndicator( + onRefresh: _loadData, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.all(isMobile ? 12 : 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Titre + _buildTitle(theme), + const SizedBox(height: 16), + + // Filtres de période + _buildPeriodFilters(theme, isMobile), + const SizedBox(height: 16), + + // Contenu principal + if (_isLoading) + const Center( + child: Padding( + padding: EdgeInsets.all(48), + child: CircularProgressIndicator(), + ), + ) + else if (_error != null) + _buildErrorCard(theme) + else ...[ + // Cards de résumé + _buildSummaryCards(theme, isMobile), + const SizedBox(height: 24), + + // Graphique d'évolution (seulement si période > 1 jour) + if (_isMultipleDays) ...[ + _buildChartSection(theme, isMobile), + const SizedBox(height: 24), + ], + + // Tableau des événements détaillés + _buildDetailsTable(theme, isMobile), + ], + ], + ), + ), + ), + ); + } + + Widget _buildTitle(ThemeData theme) { + return Text( + 'Connexions et activité', + style: theme.textTheme.headlineSmall?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ); + } + + Widget _buildPeriodFilters(ThemeData theme, bool isMobile) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Sélecteur de mode + Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + 'Période :', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + SegmentedButton( + segments: const [ + ButtonSegment( + value: DateSelectionMode.singleDay, + label: Text('Jour'), + icon: Icon(Icons.today, size: 18), + ), + ButtonSegment( + value: DateSelectionMode.dateRange, + label: Text('Période'), + icon: Icon(Icons.date_range, size: 18), + ), + ButtonSegment( + value: DateSelectionMode.lastDays, + label: Text('Derniers jours'), + icon: Icon(Icons.history, size: 18), + ), + ], + selected: {_selectionMode}, + onSelectionChanged: (values) { + setState(() => _selectionMode = values.first); + _loadData(); + }, + style: const ButtonStyle( + visualDensity: VisualDensity.compact, + ), + ), + ], + ), + const SizedBox(height: 12), + + // Contrôles spécifiques selon le mode + Wrap( + spacing: 12, + runSpacing: 12, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + // Sélecteurs de date selon le mode + ..._buildDateSelectors(theme), + + // Séparateur vertical + if (!isMobile) + Container( + height: 36, + width: 1, + color: theme.colorScheme.outlineVariant, + ), + + // Filtre par type d'événement + _buildEventTypeFilter(theme), + + // Filtre par entité (super-admin uniquement) + if (_isSuperAdmin) ...[ + if (!isMobile) + Container( + height: 36, + width: 1, + color: theme.colorScheme.outlineVariant, + ), + _buildEntityFilter(theme), + ], + ], + ), + ], + ), + ), + ); + } + + List _buildDateSelectors(ThemeData theme) { + switch (_selectionMode) { + case DateSelectionMode.singleDay: + return [ + _buildDateButton( + theme: theme, + label: 'Date', + date: _singleDate, + onTap: () => _selectDate( + initialDate: _singleDate, + onSelected: (date) { + setState(() => _singleDate = date); + _loadData(); + }, + ), + ), + ]; + + case DateSelectionMode.dateRange: + return [ + _buildDateButton( + theme: theme, + label: 'Du', + date: _startDate, + onTap: () => _selectDate( + initialDate: _startDate, + lastDate: _endDate, + onSelected: (date) { + setState(() => _startDate = date); + _loadData(); + }, + ), + ), + const Icon(Icons.arrow_forward, size: 16), + _buildDateButton( + theme: theme, + label: 'Au', + date: _endDate, + onTap: () => _selectDate( + initialDate: _endDate, + firstDate: _startDate, + onSelected: (date) { + setState(() => _endDate = date); + _loadData(); + }, + ), + ), + ]; + + case DateSelectionMode.lastDays: + return [ + SegmentedButton( + segments: const [ + ButtonSegment(value: 7, label: Text('7 jours')), + ButtonSegment(value: 14, label: Text('14 jours')), + ButtonSegment(value: 21, label: Text('21 jours')), + ButtonSegment(value: 30, label: Text('30 jours')), + ], + selected: {_lastDays}, + onSelectionChanged: (values) { + setState(() => _lastDays = values.first); + _loadData(); + }, + style: const ButtonStyle( + visualDensity: VisualDensity.compact, + ), + ), + ]; + } + } + + Widget _buildDateButton({ + required ThemeData theme, + required String label, + required DateTime date, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.outline), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '$label : ', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + Icon(Icons.calendar_today, size: 16, color: theme.colorScheme.primary), + const SizedBox(width: 6), + Text( + _dateFormat.format(date), + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } + + Future _selectDate({ + required DateTime initialDate, + DateTime? firstDate, + DateTime? lastDate, + required void Function(DateTime) onSelected, + }) async { + final picked = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: firstDate ?? DateTime(2024), + lastDate: lastDate ?? DateTime.now(), + locale: const Locale('fr', 'FR'), + ); + if (picked != null) { + onSelected(picked); + } + } + + Widget _buildEventTypeFilter(ThemeData theme) { + return DropdownButtonHideUnderline( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.outline), + borderRadius: BorderRadius.circular(8), + ), + child: DropdownButton( + value: _selectedEventType, + hint: const Text('Tous les événements'), + items: const [ + DropdownMenuItem(value: null, child: Text('Tous')), + DropdownMenuItem(value: 'login_success', child: Text('Connexions')), + DropdownMenuItem(value: 'login_failed', child: Text('Échecs connexion')), + DropdownMenuItem(value: 'passage_created', child: Text('Passages créés')), + DropdownMenuItem(value: 'stripe_payment_success', child: Text('Paiements Stripe')), + ], + onChanged: (value) { + setState(() => _selectedEventType = value); + _loadData(); + }, + ), + ), + ); + } + + Widget _buildEntityFilter(ThemeData theme) { + return DropdownButtonHideUnderline( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.outline), + borderRadius: BorderRadius.circular(8), + color: _selectedEntityId != null + ? theme.colorScheme.primaryContainer.withOpacity(0.3) + : null, + ), + child: DropdownButton( + value: _selectedEntityId, + hint: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.business, size: 16, color: theme.colorScheme.primary), + const SizedBox(width: 6), + const Text('Toutes les amicales'), + ], + ), + selectedItemBuilder: (context) { + return [ + // Item pour "Toutes" + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.business, size: 16, color: theme.colorScheme.primary), + const SizedBox(width: 6), + const Text('Toutes les amicales'), + ], + ), + // Items pour chaque amicale + ..._amicales.map((amicale) => Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.business, size: 16, color: theme.colorScheme.primary), + const SizedBox(width: 6), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 150), + child: Text( + amicale.name, + overflow: TextOverflow.ellipsis, + ), + ), + ], + )), + ]; + }, + items: [ + const DropdownMenuItem( + value: null, + child: Text('Toutes les amicales'), + ), + ..._amicales.map((amicale) => DropdownMenuItem( + value: amicale.id, + child: Text( + amicale.name, + overflow: TextOverflow.ellipsis, + ), + )), + ], + onChanged: (value) { + setState(() => _selectedEntityId = value); + _loadData(); + }, + ), + ), + ); + } + + Widget _buildErrorCard(ThemeData theme) { + return Card( + color: theme.colorScheme.errorContainer, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon(Icons.error_outline, color: theme.colorScheme.error), + const SizedBox(width: 12), + Expanded( + child: Text( + _error!, + style: TextStyle(color: theme.colorScheme.onErrorContainer), + ), + ), + TextButton( + onPressed: _loadData, + child: const Text('Réessayer'), + ), + ], + ), + ), + ); + } + + Widget _buildSummaryCards(ThemeData theme, bool isMobile) { + if (_summary == null) return const SizedBox.shrink(); + + final stats = _summary!.stats; + final totals = _summary!.totals; + final (startDate, endDate) = _getDateRange(); + + // Libellé de la période + final periodLabel = _selectionMode == DateSelectionMode.singleDay + ? _dateFormat.format(_singleDate) + : '${_dateFormat.format(startDate)} - ${_dateFormat.format(endDate)}'; + + // Disposition responsive : 2 colonnes sur mobile, 4 sur desktop + final crossAxisCount = isMobile ? 2 : 4; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Période affichée + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + 'Résumé : $periodLabel', + style: theme.textTheme.titleSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + GridView.count( + crossAxisCount: crossAxisCount, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: isMobile ? 1.3 : 1.8, + children: [ + // Connexions + _buildStatCard( + theme: theme, + icon: Icons.login, + iconColor: Colors.green, + title: 'Connexions', + value: stats.auth.success.toString(), + subtitle: '${stats.auth.failed} échecs', + ), + + // Passages + _buildStatCard( + theme: theme, + icon: Icons.receipt_long, + iconColor: Colors.blue, + title: 'Passages', + value: stats.passages.created.toString(), + subtitle: '${_formatAmount(stats.passages.amount)} collectés', + ), + + // Paiements Stripe + _buildStatCard( + theme: theme, + icon: Icons.credit_card, + iconColor: Colors.purple, + title: 'Paiements', + value: stats.stripe.success.toString(), + subtitle: _formatAmount(stats.stripe.amount), + ), + + // Utilisateurs actifs + _buildStatCard( + theme: theme, + icon: Icons.people, + iconColor: Colors.orange, + title: 'Événements', + value: totals.events.toString(), + subtitle: '${totals.uniqueUsers} utilisateurs', + ), + ], + ), + ], + ); + } + + Widget _buildStatCard({ + required ThemeData theme, + required IconData icon, + required Color iconColor, + required String title, + required String value, + required String subtitle, + }) { + return Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: iconColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: iconColor, size: 20), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + title, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + Text( + value, + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.onSurface, + ), + ), + Text( + subtitle, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + } + + Widget _buildChartSection(ThemeData theme, bool isMobile) { + if (_dailyStats == null || _dailyStats!.days.isEmpty) { + return Card( + child: Padding( + padding: const EdgeInsets.all(24), + child: Center( + child: Text( + 'Aucune donnée pour cette période', + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ); + } + + final (startDate, endDate) = _getDateRange(); + final daysDiff = endDate.difference(startDate).inDays + 1; + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Évolution sur $daysDiff jours', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + SizedBox( + height: isMobile ? 200 : 250, + child: _buildSimpleBarChart(theme), + ), + ], + ), + ), + ); + } + + Widget _buildSimpleBarChart(ThemeData theme) { + final days = _dailyStats!.days; + if (days.isEmpty) return const SizedBox.shrink(); + + // Trouver le max pour le scaling + final maxCount = days.map((d) => d.totalCount).reduce((a, b) => a > b ? a : b); + final scale = maxCount > 0 ? 1.0 / maxCount : 1.0; + + return LayoutBuilder( + builder: (context, constraints) { + final barWidth = (constraints.maxWidth - (days.length - 1) * 4) / days.length; + final clampedBarWidth = barWidth.clamp(8.0, 40.0); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.end, + children: days.map((day) { + final height = (constraints.maxHeight - 30) * day.totalCount * scale; + + return Tooltip( + message: '${_dateFormat.format(day.date)}\n${day.totalCount} événements', + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + width: clampedBarWidth, + height: height.clamp(4.0, constraints.maxHeight - 30), + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), + ), + ), + const SizedBox(height: 4), + SizedBox( + width: clampedBarWidth + 8, + child: Text( + DateFormat('dd').format(day.date), + style: theme.textTheme.labelSmall, + textAlign: TextAlign.center, + overflow: TextOverflow.clip, + ), + ), + ], + ), + ); + }).toList(), + ); + }, + ); + } + + Widget _buildDetailsTable(ThemeData theme, bool isMobile) { + final (_, endDate) = _getDateRange(); + final detailsDateLabel = _selectionMode == DateSelectionMode.singleDay + ? _dateFormat.format(_singleDate) + : _dateFormat.format(endDate); + + if (_details == null || _details!.events.isEmpty) { + return Card( + child: Padding( + padding: const EdgeInsets.all(24), + child: Center( + child: Text( + 'Aucun événement pour le $detailsDateLabel', + style: theme.textTheme.bodyLarge?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ); + } + + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Détail des événements', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text( + detailsDateLabel, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + Text( + '${_details!.pagination.total} événements', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Table responsive + if (isMobile) + _buildMobileEventsList(theme) + else + _buildDesktopEventsTable(theme), + + // Pagination + if (_details!.pagination.hasMore) + Padding( + padding: const EdgeInsets.only(top: 16), + child: Center( + child: TextButton.icon( + onPressed: _loadMoreDetails, + icon: const Icon(Icons.expand_more), + label: const Text('Charger plus'), + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildMobileEventsList(ThemeData theme) { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _details!.events.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + final event = _details!.events[index]; + return ListTile( + contentPadding: EdgeInsets.zero, + leading: _buildEventIcon(event.event), + title: Text( + EventTypes.getLabel(event.event), + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + '${event.username ?? 'Anonyme'} - ${_timeFormat.format(event.timestamp)}', + style: theme.textTheme.bodySmall, + ), + trailing: event.platform != null + ? _buildPlatformChip(event.platform!, theme) + : null, + ); + }, + ); + } + + Widget _buildDesktopEventsTable(ThemeData theme) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + headingRowHeight: 40, + dataRowMinHeight: 40, + dataRowMaxHeight: 50, + columnSpacing: 24, + columns: const [ + DataColumn(label: Text('Heure')), + DataColumn(label: Text('Type')), + DataColumn(label: Text('Utilisateur')), + DataColumn(label: Text('Plateforme')), + DataColumn(label: Text('IP')), + DataColumn(label: Text('Détails')), + ], + rows: _details!.events.map((event) { + return DataRow( + cells: [ + DataCell(Text(_timeFormat.format(event.timestamp))), + DataCell(Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildEventIcon(event.event), + const SizedBox(width: 8), + Text(EventTypes.getLabel(event.event)), + ], + )), + DataCell(Text(event.username ?? 'Anonyme')), + DataCell(event.platform != null + ? _buildPlatformChip(event.platform!, theme) + : const Text('-')), + DataCell(Text(event.ip ?? '-')), + DataCell(Text(event.reason ?? '-')), + ], + ); + }).toList(), + ), + ); + } + + Widget _buildEventIcon(String eventType) { + IconData icon; + Color color; + + switch (eventType) { + case 'login_success': + icon = Icons.login; + color = Colors.green; + break; + case 'login_failed': + icon = Icons.error_outline; + color = Colors.red; + break; + case 'logout': + icon = Icons.logout; + color = Colors.orange; + break; + case 'passage_created': + icon = Icons.add_circle_outline; + color = Colors.blue; + break; + case 'passage_updated': + icon = Icons.edit; + color = Colors.teal; + break; + case 'passage_deleted': + icon = Icons.delete_outline; + color = Colors.red; + break; + case 'stripe_payment_success': + icon = Icons.credit_card; + color = Colors.green; + break; + case 'stripe_payment_failed': + icon = Icons.credit_card_off; + color = Colors.red; + break; + default: + icon = Icons.circle; + color = Colors.grey; + } + + return Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Icon(icon, size: 16, color: color), + ); + } + + Widget _buildPlatformChip(String platform, ThemeData theme) { + IconData icon; + switch (platform.toLowerCase()) { + case 'ios': + icon = Icons.phone_iphone; + break; + case 'android': + icon = Icons.phone_android; + break; + case 'web': + icon = Icons.computer; + break; + default: + icon = Icons.devices; + } + + return Chip( + avatar: Icon(icon, size: 14), + label: Text( + platform.toUpperCase(), + style: theme.textTheme.labelSmall, + ), + padding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); + } + + Future _loadMoreDetails() async { + if (_details == null || !_details!.pagination.hasMore) return; + + final (_, endDate) = _getDateRange(); + final detailsDate = _selectionMode == DateSelectionMode.singleDay + ? _singleDate + : endDate; + + try { + final moreDetails = await _eventStatsService.getDetails( + date: detailsDate, + event: _selectedEventType, + entityId: _selectedEntityId, + limit: 50, + offset: _details!.events.length, + ); + + setState(() { + _details = EventDetails( + date: _details!.date, + events: [..._details!.events, ...moreDetails.events], + pagination: moreDetails.pagination, + ); + }); + } catch (e) { + if (mounted) { + ApiException.showError(context, e); + } + } + } + + String _formatAmount(double amount) { + return NumberFormat.currency(locale: 'fr_FR', symbol: '€').format(amount); + } +} diff --git a/app/lib/presentation/auth/register_page.dart b/app/lib/presentation/auth/register_page.dart index 3d032934..0f562676 100755 --- a/app/lib/presentation/auth/register_page.dart +++ b/app/lib/presentation/auth/register_page.dart @@ -807,7 +807,7 @@ class _RegisterPageState extends State { const SizedBox( height: 16), Text( - 'Vous allez recevoir un email contenant :', + 'Vous allez recevoir 2 emails contenant :', style: theme .textTheme .bodyMedium, @@ -852,7 +852,7 @@ class _RegisterPageState extends State { width: 4), const Expanded( child: Text( - 'Un lien pour définir votre mot de passe'), + 'Votre mot de passe de connexion'), ), ], ), diff --git a/app/lib/presentation/pages/connexions_page.dart b/app/lib/presentation/pages/connexions_page.dart new file mode 100644 index 00000000..3f805f26 --- /dev/null +++ b/app/lib/presentation/pages/connexions_page.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:geosector_app/presentation/widgets/app_scaffold.dart'; +import 'package:geosector_app/presentation/admin/admin_connexions_page.dart'; +import 'package:geosector_app/app.dart'; + +/// Page des connexions et événements utilisant AppScaffold. +/// Accessible uniquement aux administrateurs (rôle >= 2). +/// +/// - Admin Amicale (rôle 2) : voit les connexions de son amicale uniquement +/// - Super Admin (rôle >= 3) : voit les connexions de toutes les amicales +class ConnexionsPage extends StatelessWidget { + const ConnexionsPage({super.key}); + + @override + Widget build(BuildContext context) { + // Vérifier le rôle pour l'accès + final currentUser = userRepository.getCurrentUser(); + final userRole = currentUser?.role ?? 1; + + // Vérifier que l'utilisateur a le rôle 2 minimum (admin amicale) + if (userRole < 2) { + // Rediriger vers le dashboard user + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pushReplacementNamed('/user/dashboard'); + }); + return const SizedBox.shrink(); + } + + return AppScaffold( + key: const ValueKey('connexions_scaffold_admin'), + selectedIndex: 6, // Connexions est l'index 6 + pageTitle: 'Connexions', + body: AdminConnexionsPage( + userRepository: userRepository, + amicaleRepository: amicaleRepository, + ), + ); + } +} diff --git a/app/lib/presentation/widgets/app_scaffold.dart b/app/lib/presentation/widgets/app_scaffold.dart index 7c7b7c33..56d3dfdb 100644 --- a/app/lib/presentation/widgets/app_scaffold.dart +++ b/app/lib/presentation/widgets/app_scaffold.dart @@ -305,6 +305,11 @@ class NavigationHelper { selectedIcon: Icon(Icons.calendar_today), label: 'Opérations', ), + const NavigationDestination( + icon: Icon(Icons.analytics_outlined), + selectedIcon: Icon(Icons.analytics), + label: 'Connexions', + ), ]); } @@ -341,6 +346,9 @@ class NavigationHelper { case 5: context.go('/admin/operations'); break; + case 6: + context.go('/admin/connexions'); + break; default: context.go('/admin'); } @@ -380,6 +388,7 @@ class NavigationHelper { if (cleanRoute.contains('/admin/messages')) return 3; if (cleanRoute.contains('/admin/amicale')) return 4; if (cleanRoute.contains('/admin/operations')) return 5; + if (cleanRoute.contains('/admin/connexions')) return 6; return 0; // Dashboard par défaut } else { if (cleanRoute.contains('/user/history')) return 1; @@ -400,6 +409,7 @@ class NavigationHelper { case 3: return 'messages'; case 4: return 'amicale'; case 5: return 'operations'; + case 6: return 'connexions'; default: return 'dashboard'; } } else { diff --git a/app/lib/presentation/widgets/members_board_passages.dart b/app/lib/presentation/widgets/members_board_passages.dart index 39cb37de..4cab5d3f 100644 --- a/app/lib/presentation/widgets/members_board_passages.dart +++ b/app/lib/presentation/widgets/members_board_passages.dart @@ -124,66 +124,71 @@ class _MembersBoardPassagesState extends State { ), ), - // Corps avec le tableau + // Corps avec le tableau - écoute membres ET passages pour rafraîchissement auto ValueListenableBuilder>( valueListenable: Hive.box(AppKeys.membresBoxName).listenable(), builder: (context, membresBox, child) { - final membres = membresBox.values.toList(); + return ValueListenableBuilder>( + valueListenable: Hive.box(AppKeys.passagesBoxName).listenable(), + builder: (context, passagesBox, child) { + final membres = membresBox.values.toList(); - // Récupérer l'opération courante - final currentOperation = _operationRepository.getCurrentOperation(); - if (currentOperation == null) { - return const Center( - child: Padding( - padding: EdgeInsets.all(AppTheme.spacingL), - child: Text('Aucune opération en cours'), - ), - ); - } - - // Trier les membres selon la colonne sélectionnée - _sortMembers(membres, currentOperation.id); - - // Construire les lignes : TOTAL en première position + détails membres - final allRows = [ - _buildTotalRow(membres, currentOperation.id, theme), - ..._buildRows(membres, currentOperation.id, theme), - ]; - - // Afficher le tableau complet sans scroll interne - return SizedBox( - width: double.infinity, // Prendre toute la largeur disponible - child: Theme( - data: Theme.of(context).copyWith( - dataTableTheme: DataTableThemeData( - headingRowColor: WidgetStateProperty.resolveWith( - (Set states) { - return theme.colorScheme.primary.withOpacity(0.08); - }, + // Récupérer l'opération courante + final currentOperation = _operationRepository.getCurrentOperation(); + if (currentOperation == null) { + return const Center( + child: Padding( + padding: EdgeInsets.all(AppTheme.spacingL), + child: Text('Aucune opération en cours'), ), - dataRowColor: WidgetStateProperty.resolveWith( - (Set states) { - if (states.contains(WidgetState.selected)) { - return theme.colorScheme.primary.withOpacity(0.08); - } - return null; - }, + ); + } + + // Trier les membres selon la colonne sélectionnée + _sortMembers(membres, currentOperation.id); + + // Construire les lignes : TOTAL en première position + détails membres + final allRows = [ + _buildTotalRow(membres, currentOperation.id, theme), + ..._buildRows(membres, currentOperation.id, theme), + ]; + + // Afficher le tableau complet sans scroll interne + return SizedBox( + width: double.infinity, // Prendre toute la largeur disponible + child: Theme( + data: Theme.of(context).copyWith( + dataTableTheme: DataTableThemeData( + headingRowColor: WidgetStateProperty.resolveWith( + (Set states) { + return theme.colorScheme.primary.withOpacity(0.08); + }, + ), + dataRowColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.selected)) { + return theme.colorScheme.primary.withOpacity(0.08); + } + return null; + }, + ), + ), + ), + child: DataTable( + columnSpacing: 4, // Espacement minimal entre colonnes + horizontalMargin: 4, // Marges horizontales minimales + headingRowHeight: 42, // Hauteur de l'en-tête optimisée + dataRowMinHeight: 42, + dataRowMaxHeight: 42, + // Utiliser les flèches natives de DataTable + sortColumnIndex: _sortColumnIndex, + sortAscending: _sortAscending, + columns: _buildColumns(theme), + rows: allRows, ), ), - ), - child: DataTable( - columnSpacing: 4, // Espacement minimal entre colonnes - horizontalMargin: 4, // Marges horizontales minimales - headingRowHeight: 42, // Hauteur de l'en-tête optimisée - dataRowMinHeight: 42, - dataRowMaxHeight: 42, - // Utiliser les flèches natives de DataTable - sortColumnIndex: _sortColumnIndex, - sortAscending: _sortAscending, - columns: _buildColumns(theme), - rows: allRows, - ), - ), + ); + }, ); }, ), diff --git a/app/lib/presentation/widgets/membre_row_widget.dart b/app/lib/presentation/widgets/membre_row_widget.dart index 2cd60c65..c6d43f53 100755 --- a/app/lib/presentation/widgets/membre_row_widget.dart +++ b/app/lib/presentation/widgets/membre_row_widget.dart @@ -79,6 +79,16 @@ class MembreRowWidget extends StatelessWidget { ), ), + // Tournée (sectName) - masqué en mobile + if (!isMobile) + Expanded( + flex: 2, + child: Text( + membre.sectName ?? '', + style: theme.textTheme.bodyMedium, + ), + ), + // Email - masqué en mobile if (!isMobile) Expanded( diff --git a/app/lib/presentation/widgets/membre_table_widget.dart b/app/lib/presentation/widgets/membre_table_widget.dart index 7ad524a3..bc8da3a7 100755 --- a/app/lib/presentation/widgets/membre_table_widget.dart +++ b/app/lib/presentation/widgets/membre_table_widget.dart @@ -113,6 +113,19 @@ class MembreTableWidget extends StatelessWidget { ), ), + // Tournée (sectName) - masqué en mobile + if (!isMobile) + Expanded( + flex: 2, + child: Text( + 'Tournée', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.bold, + color: theme.colorScheme.primary, + ), + ), + ), + // Email - masqué en mobile if (!isMobile) Expanded( diff --git a/app/lib/presentation/widgets/passage_form_dialog.dart b/app/lib/presentation/widgets/passage_form_dialog.dart index bdaaca5a..db16bb11 100755 --- a/app/lib/presentation/widgets/passage_form_dialog.dart +++ b/app/lib/presentation/widgets/passage_form_dialog.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:geosector_app/core/theme/app_theme.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/foundation.dart' show kIsWeb, debugPrint; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/core/data/models/passage_model.dart'; import 'package:geosector_app/core/repositories/passage_repository.dart'; @@ -15,6 +15,7 @@ import 'package:geosector_app/core/services/stripe_connect_service.dart'; import 'package:geosector_app/core/services/api_service.dart'; import 'package:geosector_app/core/utils/api_exception.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; +import 'package:geosector_app/core/services/location_service.dart'; import 'package:geosector_app/presentation/widgets/custom_text_field.dart'; import 'package:geosector_app/presentation/widgets/form_section.dart'; import 'package:geosector_app/presentation/widgets/result_dialog.dart'; @@ -88,13 +89,17 @@ class _PassageFormDialogState extends State { // Helpers de validation String? _validateNumero(String? value) { + debugPrint('🔍 [VALIDATOR] _validateNumero appelé avec: "$value"'); if (value == null || value.trim().isEmpty) { + debugPrint('❌ [VALIDATOR] Numéro vide -> retourne erreur'); return 'Le numéro est obligatoire'; } final numero = int.tryParse(value.trim()); if (numero == null || numero <= 0) { + debugPrint('❌ [VALIDATOR] Numéro invalide: $value -> retourne erreur'); return 'Numéro invalide'; } + debugPrint('✅ [VALIDATOR] Numéro valide: $numero'); return null; } @@ -329,48 +334,102 @@ class _PassageFormDialogState extends State { } void _handleSubmit() async { - if (_isSubmitting) return; + debugPrint('🔵 [SUBMIT] Début _handleSubmit'); - // ✅ Validation intégrée avec focus automatique sur erreur - if (!_formKey.currentState!.validate()) { - // Le focus est automatiquement mis sur le premier champ en erreur - // Les bordures rouges et messages d'erreur sont affichés automatiquement + if (_isSubmitting) { + debugPrint('⚠️ [SUBMIT] Déjà en cours de soumission, abandon'); return; } - // Toujours sauvegarder le passage en premier + debugPrint('🔵 [SUBMIT] Vérification de l\'état du formulaire'); + debugPrint('🔵 [SUBMIT] _formKey: $_formKey'); + debugPrint('🔵 [SUBMIT] _formKey.currentState: ${_formKey.currentState}'); + + // Validation avec protection contre le null + if (_formKey.currentState == null) { + debugPrint('❌ [SUBMIT] ERREUR: _formKey.currentState est null !'); + if (mounted) { + await ResultDialog.show( + context: context, + success: false, + message: "Erreur d'initialisation du formulaire", + ); + } + return; + } + + debugPrint('🔵 [SUBMIT] Validation du formulaire...'); + final isValid = _formKey.currentState!.validate(); + debugPrint('🔵 [SUBMIT] Résultat validation: $isValid'); + + if (!isValid) { + debugPrint('⚠️ [SUBMIT] Validation échouée, abandon'); + + // Afficher un dialog d'erreur clair à l'utilisateur + if (mounted) { + await ResultDialog.show( + context: context, + success: false, + message: 'Veuillez vérifier tous les champs marqués comme obligatoires', + ); + } + return; + } + + debugPrint('✅ [SUBMIT] Validation OK, appel _savePassage()'); await _savePassage(); + debugPrint('🔵 [SUBMIT] Fin _handleSubmit'); } Future _savePassage() async { - if (_isSubmitting) return; + debugPrint('🟢 [SAVE] Début _savePassage'); + if (_isSubmitting) { + debugPrint('⚠️ [SAVE] Déjà en cours de soumission, abandon'); + return; + } + + debugPrint('🟢 [SAVE] Mise à jour état _isSubmitting = true'); setState(() { _isSubmitting = true; }); // Afficher l'overlay de chargement + debugPrint('🟢 [SAVE] Affichage overlay de chargement'); final overlay = LoadingSpinOverlayUtils.show( context: context, message: 'Enregistrement en cours...', ); try { + debugPrint('🟢 [SAVE] Récupération utilisateur actuel'); final currentUser = widget.userRepository.getCurrentUser(); + debugPrint('🟢 [SAVE] currentUser: ${currentUser?.id} - ${currentUser?.name}'); + if (currentUser == null) { + debugPrint('❌ [SAVE] ERREUR: Utilisateur non connecté'); throw Exception("Utilisateur non connecté"); } + debugPrint('🟢 [SAVE] Récupération opération active'); final currentOperation = widget.operationRepository.getCurrentOperation(); + debugPrint('🟢 [SAVE] currentOperation: ${currentOperation?.id} - ${currentOperation?.name}'); + if (currentOperation == null && widget.passage == null) { + debugPrint('❌ [SAVE] ERREUR: Aucune opération active trouvée'); throw Exception("Aucune opération active trouvée"); } // Déterminer les valeurs de montant et type de règlement selon le type de passage + debugPrint('🟢 [SAVE] Calcul des valeurs finales'); + debugPrint('🟢 [SAVE] _selectedPassageType: $_selectedPassageType'); + final String finalMontant = (_selectedPassageType == 1 || _selectedPassageType == 5) ? _montantController.text.trim().replaceAll(',', '.') : '0'; + debugPrint('🟢 [SAVE] finalMontant: $finalMontant'); + // Déterminer le type de règlement final selon le type de passage final int finalTypeReglement; if (_selectedPassageType == 1 || _selectedPassageType == 5) { @@ -380,6 +439,7 @@ class _PassageFormDialogState extends State { // Pour tous les autres types, forcer "Non renseigné" finalTypeReglement = 4; } + debugPrint('🟢 [SAVE] finalTypeReglement: $finalTypeReglement'); // Déterminer la valeur de nbPassages selon le type de passage final int finalNbPassages; @@ -397,6 +457,31 @@ class _PassageFormDialogState extends State { // Nouveau passage : toujours 1 finalNbPassages = 1; } + debugPrint('🟢 [SAVE] finalNbPassages: $finalNbPassages'); + + // Récupérer les coordonnées GPS pour un nouveau passage + String finalGpsLat = '0.0'; + String finalGpsLng = '0.0'; + if (widget.passage == null) { + // Nouveau passage : tenter de récupérer la position GPS actuelle + debugPrint('🟢 [SAVE] Récupération de la position GPS...'); + try { + final position = await LocationService.getCurrentPosition(); + if (position != null) { + finalGpsLat = position.latitude.toString(); + finalGpsLng = position.longitude.toString(); + debugPrint('🟢 [SAVE] GPS récupéré: lat=$finalGpsLat, lng=$finalGpsLng'); + } else { + debugPrint('🟢 [SAVE] GPS non disponible, utilisation de 0.0 (l\'API utilisera le géocodage)'); + } + } catch (e) { + debugPrint('⚠️ [SAVE] Erreur récupération GPS: $e - l\'API utilisera le géocodage'); + } + } else { + // Modification : conserver les coordonnées existantes + finalGpsLat = widget.passage!.gpsLat; + finalGpsLng = widget.passage!.gpsLng; + } final passageData = widget.passage?.copyWith( fkType: _selectedPassageType!, @@ -422,7 +507,7 @@ class _PassageFormDialogState extends State { PassageModel( id: 0, // Nouveau passage fkOperation: currentOperation!.id, // Opération active - fkSector: 0, // Secteur par défaut + fkSector: 0, // Secteur par défaut (sera déterminé par l'API) fkUser: currentUser.id, // Utilisateur actuel fkType: _selectedPassageType!, fkAdresse: "0", // Adresse par défaut pour nouveau passage @@ -435,8 +520,8 @@ class _PassageFormDialogState extends State { fkHabitat: _fkHabitat, appt: _apptController.text.trim(), niveau: _niveauController.text.trim(), - gpsLat: '0.0', // GPS par défaut - gpsLng: '0.0', // GPS par défaut + gpsLat: finalGpsLat, // GPS de l'utilisateur ou 0.0 si non disponible + gpsLng: finalGpsLng, // GPS de l'utilisateur ou 0.0 si non disponible nomRecu: _nameController.text.trim(), remarque: _remarqueController.text.trim(), montant: finalMontant, @@ -453,32 +538,37 @@ class _PassageFormDialogState extends State { ); // Sauvegarder le passage d'abord + debugPrint('🟢 [SAVE] Préparation sauvegarde passage'); PassageModel? savedPassage; if (widget.passage == null || widget.passage!.id == 0) { // Création d'un nouveau passage (passage null OU id=0) + debugPrint('🟢 [SAVE] Création d\'un nouveau passage'); savedPassage = await widget.passageRepository.createPassageWithReturn(passageData); + debugPrint('🟢 [SAVE] Passage créé avec ID: ${savedPassage?.id}'); + + if (savedPassage == null) { + debugPrint('❌ [SAVE] ERREUR: savedPassage est null après création'); + throw Exception("Échec de la création du passage"); + } } else { // Mise à jour d'un passage existant - final success = await widget.passageRepository.updatePassage(passageData); - if (success) { - savedPassage = passageData; - } - } - - if (savedPassage == null) { - throw Exception(widget.passage == null || widget.passage!.id == 0 - ? "Échec de la création du passage" - : "Échec de la mise à jour du passage"); + debugPrint('🟢 [SAVE] Mise à jour passage existant ID: ${widget.passage!.id}'); + await widget.passageRepository.updatePassage(passageData); + debugPrint('🟢 [SAVE] Mise à jour réussie'); + savedPassage = passageData; } // Garantir le type non-nullable après la vérification final confirmedPassage = savedPassage; + debugPrint('✅ [SAVE] Passage sauvegardé avec succès ID: ${confirmedPassage.id}'); // Mémoriser l'adresse pour la prochaine création de passage + debugPrint('🟢 [SAVE] Mémorisation adresse'); await _saveLastPassageAddress(); // Propager la résidence aux autres passages de l'immeuble si nécessaire if (_fkHabitat == 2 && _residenceController.text.trim().isNotEmpty) { + debugPrint('🟢 [SAVE] Propagation résidence à l\'immeuble'); await _propagateResidenceToBuilding(confirmedPassage); } @@ -514,17 +604,30 @@ class _PassageFormDialogState extends State { // Lancer le flow Tap to Pay final paymentSuccess = await _attemptTapToPayWithPassage(confirmedPassage, montant); - if (!paymentSuccess) { - debugPrint('⚠️ Paiement Tap to Pay échoué pour le passage ${confirmedPassage.id}'); + if (paymentSuccess) { + // Fermer le formulaire en cas de succès + if (mounted) { + Navigator.of(context, rootNavigator: false).pop(); + widget.onSuccess?.call(); + } + } else { + debugPrint('⚠️ Paiement Tap to Pay échoué - formulaire reste ouvert'); + // Ne pas fermer le formulaire en cas d'échec + // L'utilisateur peut réessayer ou annuler + } + }, + onQRCodeCompleted: () { + // Pour QR Code: fermer le formulaire après l'affichage du QR + if (mounted) { + Navigator.of(context, rootNavigator: false).pop(); + widget.onSuccess?.call(); } }, ); - // Fermer le formulaire après le choix de paiement - if (mounted) { - Navigator.of(context, rootNavigator: false).pop(); - widget.onSuccess?.call(); - } + // NOTE: Le formulaire n'est plus fermé systématiquement ici + // Il est fermé dans onQRCodeCompleted pour QR Code + // ou dans onTapToPaySelected en cas de succès Tap to Pay } } else { // Stripe non activé pour cette amicale @@ -563,30 +666,44 @@ class _PassageFormDialogState extends State { } } } - } catch (e) { + } catch (e, stackTrace) { // Masquer le loading + debugPrint('❌ [SAVE] ERREUR CAPTURÉE'); + debugPrint('❌ [SAVE] Type erreur: ${e.runtimeType}'); + debugPrint('❌ [SAVE] Message erreur: $e'); + debugPrint('❌ [SAVE] Stack trace:\n$stackTrace'); + LoadingSpinOverlayUtils.hideSpecific(overlay); // Afficher l'erreur + final errorMessage = ApiException.fromError(e).message; + debugPrint('❌ [SAVE] Message d\'erreur formaté: $errorMessage'); + if (mounted) { await ResultDialog.show( context: context, success: false, - message: ApiException.fromError(e).message, + message: errorMessage, ); } } finally { + debugPrint('🟢 [SAVE] Bloc finally - Nettoyage'); if (mounted) { setState(() { _isSubmitting = false; }); + debugPrint('🟢 [SAVE] _isSubmitting = false'); } + debugPrint('🟢 [SAVE] Fin _savePassage'); } } /// Mémoriser l'adresse du passage pour la prochaine création Future _saveLastPassageAddress() async { try { + debugPrint('🟡 [ADDRESS] Début mémorisation adresse'); + debugPrint('🟡 [ADDRESS] _settingsBox.isOpen: ${_settingsBox.isOpen}'); + await _settingsBox.put('lastPassageNumero', _numeroController.text.trim()); await _settingsBox.put('lastPassageRueBis', _rueBisController.text.trim()); await _settingsBox.put('lastPassageRue', _rueController.text.trim()); @@ -596,20 +713,28 @@ class _PassageFormDialogState extends State { await _settingsBox.put('lastPassageAppt', _apptController.text.trim()); await _settingsBox.put('lastPassageNiveau', _niveauController.text.trim()); - debugPrint('✅ Adresse mémorisée pour la prochaine création de passage'); - } catch (e) { - debugPrint('⚠️ Erreur lors de la mémorisation de l\'adresse: $e'); + debugPrint('✅ [ADDRESS] Adresse mémorisée avec succès'); + } catch (e, stackTrace) { + debugPrint('❌ [ADDRESS] Erreur lors de la mémorisation: $e'); + debugPrint('❌ [ADDRESS] Stack trace:\n$stackTrace'); } } /// Propager la résidence aux autres passages de l'immeuble (fkType=2, même adresse, résidence vide) Future _propagateResidenceToBuilding(PassageModel savedPassage) async { try { + debugPrint('🟡 [PROPAGATE] Début propagation résidence'); + final passagesBox = Hive.box(AppKeys.passagesBoxName); + debugPrint('🟡 [PROPAGATE] passagesBox.isOpen: ${passagesBox.isOpen}'); + debugPrint('🟡 [PROPAGATE] passagesBox.length: ${passagesBox.length}'); + final residence = _residenceController.text.trim(); + debugPrint('🟡 [PROPAGATE] résidence: "$residence"'); // Clé d'adresse du passage sauvegardé final addressKey = '${savedPassage.numero}|${savedPassage.rueBis}|${savedPassage.rue}|${savedPassage.ville}'; + debugPrint('🟡 [PROPAGATE] addressKey: "$addressKey"'); int updatedCount = 0; @@ -625,6 +750,7 @@ class _PassageFormDialogState extends State { passageAddressKey == addressKey && // Même adresse passage.residence.trim().isEmpty) { // Résidence vide + debugPrint('🟡 [PROPAGATE] Mise à jour passage ID: ${passage.id}'); // Mettre à jour la résidence dans Hive final updatedPassage = passage.copyWith(residence: residence); await passagesBox.put(passage.key, updatedPassage); @@ -634,10 +760,13 @@ class _PassageFormDialogState extends State { } if (updatedCount > 0) { - debugPrint('✅ Résidence propagée à $updatedCount passage(s) de l\'immeuble'); + debugPrint('✅ [PROPAGATE] Résidence propagée à $updatedCount passage(s)'); + } else { + debugPrint('✅ [PROPAGATE] Aucun passage à mettre à jour'); } - } catch (e) { - debugPrint('⚠️ Erreur lors de la propagation de la résidence: $e'); + } catch (e, stackTrace) { + debugPrint('❌ [PROPAGATE] Erreur lors de la propagation: $e'); + debugPrint('❌ [PROPAGATE] Stack trace:\n$stackTrace'); } } @@ -1819,9 +1948,46 @@ class _TapToPayFlowDialogState extends State<_TapToPayFlowDialog> { } } catch (e) { + // Analyser le type d'erreur pour afficher un message clair + final errorMsg = e.toString().toLowerCase(); + + String userMessage; + bool shouldCancelPayment = true; + + if (errorMsg.contains('canceled') || errorMsg.contains('cancelled')) { + // Annulation volontaire par l'utilisateur + userMessage = 'Paiement annulé'; + + } else if (errorMsg.contains('cardreadtimedout') || errorMsg.contains('timed out')) { + // Timeout de lecture NFC + userMessage = 'Lecture de la carte impossible.\n\n' + 'Conseils :\n' + '• Maintenez la carte contre le dos du téléphone\n' + '• Ne bougez pas jusqu\'à confirmation\n' + '• Retirez la coque si nécessaire\n' + '• Essayez différentes positions sur le téléphone'; + + } else if (errorMsg.contains('already') && errorMsg.contains('payment')) { + // PaymentIntent existe déjà + userMessage = 'Un paiement est déjà en cours pour ce passage.\n' + 'Veuillez réessayer dans quelques instants.'; + shouldCancelPayment = false; // Déjà en cours, pas besoin d'annuler + + } else { + // Autre erreur technique + userMessage = 'Erreur lors du paiement.\n\n$e'; + } + + // Annuler le PaymentIntent si créé pour permettre une nouvelle tentative + if (shouldCancelPayment && _paymentIntentId != null) { + StripeTapToPayService.instance.cancelPayment(_paymentIntentId!).catchError((cancelError) { + debugPrint('⚠️ Erreur annulation PaymentIntent: $cancelError'); + }); + } + setState(() { _currentState = 'error'; - _errorMessage = e.toString(); + _errorMessage = userMessage; }); } } diff --git a/app/lib/presentation/widgets/payment_method_selection_dialog.dart b/app/lib/presentation/widgets/payment_method_selection_dialog.dart index 86befcec..160e1e06 100644 --- a/app/lib/presentation/widgets/payment_method_selection_dialog.dart +++ b/app/lib/presentation/widgets/payment_method_selection_dialog.dart @@ -8,13 +8,14 @@ import 'package:geosector_app/presentation/widgets/qr_code_payment_dialog.dart'; /// Dialog de sélection de la méthode de paiement CB /// Affiche les options QR Code et/ou Tap to Pay selon la compatibilité -class PaymentMethodSelectionDialog extends StatelessWidget { +class PaymentMethodSelectionDialog extends StatefulWidget { final PassageModel passage; final double amount; final String habitantName; final StripeConnectService stripeConnectService; final PassageRepository? passageRepository; final VoidCallback? onTapToPaySelected; + final VoidCallback? onQRCodeCompleted; const PaymentMethodSelectionDialog({ super.key, @@ -24,12 +25,61 @@ class PaymentMethodSelectionDialog extends StatelessWidget { required this.stripeConnectService, this.passageRepository, this.onTapToPaySelected, + this.onQRCodeCompleted, }); + @override + State createState() => _PaymentMethodSelectionDialogState(); + + /// Afficher le dialog de sélection de méthode de paiement + static Future show({ + required BuildContext context, + required PassageModel passage, + required double amount, + required String habitantName, + required StripeConnectService stripeConnectService, + PassageRepository? passageRepository, + VoidCallback? onTapToPaySelected, + VoidCallback? onQRCodeCompleted, + }) { + return showDialog( + context: context, + barrierDismissible: false, // Ne peut pas fermer en cliquant à côté + builder: (context) => PaymentMethodSelectionDialog( + passage: passage, + amount: amount, + habitantName: habitantName, + stripeConnectService: stripeConnectService, + passageRepository: passageRepository, + onTapToPaySelected: onTapToPaySelected, + onQRCodeCompleted: onQRCodeCompleted, + ), + ); + } +} + +class _PaymentMethodSelectionDialogState extends State { + String? _tapToPayUnavailableReason; + bool _isCheckingNFC = true; + + @override + void initState() { + super.initState(); + _checkTapToPayAvailability(); + } + + Future _checkTapToPayAvailability() async { + final reason = await DeviceInfoService.instance.getTapToPayUnavailableReasonAsync(); + setState(() { + _tapToPayUnavailableReason = reason; + _isCheckingNFC = false; + }); + } + @override Widget build(BuildContext context) { - final canUseTapToPay = DeviceInfoService.instance.canUseTapToPay(); - final amountEuros = amount.toStringAsFixed(2); + final canUseTapToPay = !_isCheckingNFC && _tapToPayUnavailableReason == null; + final amountEuros = widget.amount.toStringAsFixed(2); return Dialog( shape: RoundedRectangleBorder( @@ -42,21 +92,12 @@ class PaymentMethodSelectionDialog extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ // En-tête - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'Règlement CB', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ), - ], + const Text( + 'Règlement CB', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), ), const SizedBox(height: 24), @@ -76,7 +117,7 @@ class PaymentMethodSelectionDialog extends StatelessWidget { const SizedBox(width: 12), Expanded( child: Text( - habitantName, + widget.habitantName, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, @@ -128,23 +169,28 @@ class PaymentMethodSelectionDialog extends StatelessWidget { description: 'Le client scanne le code avec son téléphone', onPressed: () => _handleQRCodePayment(context), color: Colors.blue, + isEnabled: true, ), - if (canUseTapToPay) ...[ - const SizedBox(height: 12), - // Bouton Tap to Pay - _buildPaymentButton( - context: context, - icon: Icons.contactless, - label: 'Tap to Pay', - description: 'Paiement sans contact sur cet appareil', - onPressed: () { - Navigator.of(context).pop(); - onTapToPaySelected?.call(); - }, - color: Colors.green, - ), - ], + const SizedBox(height: 12), + + // Bouton Tap to Pay (toujours affiché, désactivé si non disponible) + _buildPaymentButton( + context: context, + icon: Icons.contactless, + label: _isCheckingNFC ? 'Tap to Pay (vérification...)' : 'Tap to Pay', + description: canUseTapToPay + ? 'Paiement sans contact sur cet appareil' + : _tapToPayUnavailableReason ?? 'Vérification en cours...', + onPressed: canUseTapToPay + ? () { + Navigator.of(context).pop(); + widget.onTapToPaySelected?.call(); + } + : null, + color: Colors.green, + isEnabled: canUseTapToPay, + ), const SizedBox(height: 24), @@ -179,18 +225,24 @@ class PaymentMethodSelectionDialog extends StatelessWidget { required IconData icon, required String label, required String description, - required VoidCallback onPressed, + required VoidCallback? onPressed, required Color color, + required bool isEnabled, }) { + // Couleurs selon l'état activé/désactivé + final effectiveColor = isEnabled ? color : Colors.grey; + final backgroundColor = isEnabled ? color.withOpacity(0.1) : Colors.grey.shade100; + final borderColor = isEnabled ? color.withOpacity(0.3) : Colors.grey.shade300; + return InkWell( - onTap: onPressed, + onTap: isEnabled ? onPressed : null, borderRadius: BorderRadius.circular(12), child: Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: color.withOpacity(0.1), - border: Border.all(color: color.withOpacity(0.3), width: 2), + color: backgroundColor, + border: Border.all(color: borderColor, width: 2), borderRadius: BorderRadius.circular(12), ), child: Row( @@ -198,36 +250,69 @@ class PaymentMethodSelectionDialog extends StatelessWidget { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: color.withOpacity(0.2), + color: isEnabled ? color.withOpacity(0.2) : Colors.grey.shade200, borderRadius: BorderRadius.circular(8), ), - child: Icon(icon, color: color, size: 32), + child: Icon( + icon, + color: effectiveColor, + size: 32, + ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - label, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: color, - ), + Row( + children: [ + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: effectiveColor, + ), + ), + ), + if (!isEnabled) + Icon( + Icons.lock_outline, + color: Colors.grey.shade600, + size: 20, + ), + ], ), const SizedBox(height: 4), - Text( - description, - style: TextStyle( - fontSize: 13, - color: Colors.grey.shade700, - ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isEnabled) ...[ + Icon( + Icons.warning_amber_rounded, + color: Colors.orange.shade700, + size: 16, + ), + const SizedBox(width: 4), + ], + Expanded( + child: Text( + description, + style: TextStyle( + fontSize: 13, + color: isEnabled ? Colors.grey.shade700 : Colors.orange.shade700, + fontWeight: isEnabled ? FontWeight.normal : FontWeight.w500, + ), + ), + ), + ], ), ], ), ), - Icon(Icons.arrow_forward_ios, color: color, size: 20), + if (isEnabled) + Icon(Icons.arrow_forward_ios, color: effectiveColor, size: 20), ], ), ), @@ -238,6 +323,7 @@ class PaymentMethodSelectionDialog extends StatelessWidget { Future _handleQRCodePayment(BuildContext context) async { // Sauvegarder le navigator avant de fermer les dialogs final navigator = Navigator.of(context); + bool loaderDisplayed = false; try { // Afficher un loader @@ -248,19 +334,20 @@ class PaymentMethodSelectionDialog extends StatelessWidget { child: CircularProgressIndicator(), ), ); + loaderDisplayed = true; // Créer le Payment Link - final amountInCents = (amount * 100).round(); - debugPrint('🔵 Création Payment Link : ${amountInCents} cents, passage ${passage.id}'); + final amountInCents = (widget.amount * 100).round(); + debugPrint('🔵 Création Payment Link : ${amountInCents} cents, passage ${widget.passage.id}'); - final paymentLink = await stripeConnectService.createPaymentLink( + final paymentLink = await widget.stripeConnectService.createPaymentLink( amountInCents: amountInCents, - passageId: passage.id, - description: 'Calendrier pompiers - ${habitantName}', + passageId: widget.passage.id, + description: 'Calendrier pompiers - ${widget.habitantName}', metadata: { - 'passage_id': passage.id.toString(), - 'habitant_name': habitantName, - 'adresse': '${passage.numero} ${passage.rue}, ${passage.ville}', + 'passage_id': widget.passage.id.toString(), + 'habitant_name': widget.habitantName, + 'adresse': '${widget.passage.numero} ${widget.passage.rue}, ${widget.passage.ville}', }, ); @@ -270,22 +357,18 @@ class PaymentMethodSelectionDialog extends StatelessWidget { debugPrint(' ID: ${paymentLink.paymentLinkId}'); } - // Fermer le loader - navigator.pop(); - debugPrint('🔵 Loader fermé'); - if (paymentLink == null) { throw Exception('Impossible de créer le lien de paiement'); } // Sauvegarder l'URL du Payment Link dans le passage - if (passageRepository != null) { + if (widget.passageRepository != null) { try { debugPrint('🔵 Sauvegarde de l\'URL du Payment Link dans le passage...'); - final updatedPassage = passage.copyWith( + final updatedPassage = widget.passage.copyWith( stripePaymentLinkUrl: paymentLink.url, ); - await passageRepository!.updatePassage(updatedPassage); + await widget.passageRepository!.updatePassage(updatedPassage); debugPrint('✅ URL du Payment Link sauvegardée'); } catch (e) { debugPrint('⚠️ Erreur lors de la sauvegarde de l\'URL: $e'); @@ -293,7 +376,12 @@ class PaymentMethodSelectionDialog extends StatelessWidget { } } - // Fermer le dialog de sélection + // Fermer le loader + navigator.pop(); + loaderDisplayed = false; + debugPrint('🔵 Loader fermé'); + + // Fermer le dialog de sélection (seulement en cas de succès) navigator.pop(); debugPrint('🔵 Dialog de sélection fermé'); @@ -311,43 +399,25 @@ class PaymentMethodSelectionDialog extends StatelessWidget { ); debugPrint('🔵 Dialog QR Code affiché'); + // Notifier que le QR Code est complété + widget.onQRCodeCompleted?.call(); + debugPrint('✅ Callback onQRCodeCompleted appelé'); + } catch (e, stack) { debugPrint('❌ Erreur dans _handleQRCodePayment: $e'); debugPrint(' Stack: $stack'); // Fermer le loader si encore ouvert - try { - navigator.pop(); - } catch (_) {} + if (loaderDisplayed) { + try { + navigator.pop(); + } catch (_) {} + } - // Afficher l'erreur + // Afficher l'erreur (le dialogue de sélection reste ouvert) if (context.mounted) { ApiException.showError(context, e); } } } - - /// Afficher le dialog de sélection de méthode de paiement - static Future show({ - required BuildContext context, - required PassageModel passage, - required double amount, - required String habitantName, - required StripeConnectService stripeConnectService, - PassageRepository? passageRepository, - VoidCallback? onTapToPaySelected, - }) { - return showDialog( - context: context, - barrierDismissible: true, - builder: (context) => PaymentMethodSelectionDialog( - passage: passage, - amount: amount, - habitantName: habitantName, - stripeConnectService: stripeConnectService, - passageRepository: passageRepository, - onTapToPaySelected: onTapToPaySelected, - ), - ); - } } diff --git a/app/lib/presentation/widgets/user_form.dart b/app/lib/presentation/widgets/user_form.dart index 84dd2458..e72c18e9 100755 --- a/app/lib/presentation/widgets/user_form.dart +++ b/app/lib/presentation/widgets/user_form.dart @@ -816,9 +816,9 @@ class _UserFormState extends State { }, tooltip: _obscurePassword ? "Afficher le mot de passe" : "Masquer le mot de passe", ), - helperText: widget.user?.id != 0 - ? "Laissez vide pour conserver le mot de passe actuel" - : "8 à 64 caractères. Phrases de passe recommandées (ex: Mon chat Félix a 3 ans!)", + helperText: widget.user?.id != 0 + ? "Laissez vide pour conserver le mot de passe actuel" + : "8 à 64 caractères, alphanumériques et caractères spéciaux", helperMaxLines: 3, validator: _validatePassword, ), @@ -895,9 +895,9 @@ class _UserFormState extends State { }, tooltip: _obscurePassword ? "Afficher le mot de passe" : "Masquer le mot de passe", ), - helperText: widget.user?.id != 0 - ? "Laissez vide pour conserver le mot de passe actuel" - : "8 à 64 caractères. Phrases de passe recommandées (ex: Mon chat Félix a 3 ans!)", + helperText: widget.user?.id != 0 + ? "Laissez vide pour conserver le mot de passe actuel" + : "8 à 64 caractères, alphanumériques et caractères spéciaux", helperMaxLines: 3, validator: _validatePassword, ), diff --git a/app/lib/presentation/widgets/user_form_dialog.dart b/app/lib/presentation/widgets/user_form_dialog.dart index ee6af5df..514f639c 100755 --- a/app/lib/presentation/widgets/user_form_dialog.dart +++ b/app/lib/presentation/widgets/user_form_dialog.dart @@ -240,7 +240,7 @@ class _UserFormDialogState extends State { user: widget.user, readOnly: widget.readOnly, allowUsernameEdit: widget.allowUsernameEdit, - allowSectNameEdit: widget.allowUsernameEdit, + allowSectNameEdit: widget.isAdmin, // Toujours éditable pour un admin amicale: widget.amicale, // Passer l'amicale isAdmin: widget.isAdmin, // Passer isAdmin onSubmit: null, // Pas besoin de callback diff --git a/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig index 7710f546..5a00c87f 100644 --- a/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig +++ b/app/macos/Flutter/ephemeral/Flutter-Generated.xcconfig @@ -1,10 +1,10 @@ // This is a generated file; do not edit or check into version control. -FLUTTER_ROOT=/opt/flutter +FLUTTER_ROOT=/home/pierre/.local/flutter FLUTTER_APPLICATION_PATH=/home/pierre/dev/geosector/app COCOAPODS_PARALLEL_CODE_SIGN=true FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=3.5.2 -FLUTTER_BUILD_NUMBER=352 +FLUTTER_BUILD_NAME=3.6.2 +FLUTTER_BUILD_NUMBER=362 DART_OBFUSCATION=false TRACK_WIDGET_CREATION=true TREE_SHAKE_ICONS=false diff --git a/app/macos/Flutter/ephemeral/flutter_export_environment.sh b/app/macos/Flutter/ephemeral/flutter_export_environment.sh index eac740f1..0e5b7b33 100755 --- a/app/macos/Flutter/ephemeral/flutter_export_environment.sh +++ b/app/macos/Flutter/ephemeral/flutter_export_environment.sh @@ -1,11 +1,11 @@ #!/bin/sh # This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/opt/flutter" +export "FLUTTER_ROOT=/home/pierre/.local/flutter" export "FLUTTER_APPLICATION_PATH=/home/pierre/dev/geosector/app" export "COCOAPODS_PARALLEL_CODE_SIGN=true" export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=3.5.2" -export "FLUTTER_BUILD_NUMBER=352" +export "FLUTTER_BUILD_NAME=3.6.2" +export "FLUTTER_BUILD_NUMBER=362" export "DART_OBFUSCATION=false" export "TRACK_WIDGET_CREATION=true" export "TREE_SHAKE_ICONS=false" diff --git a/app/pubspec.lock b/app/pubspec.lock index 48549b79..cbe268fc 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -130,10 +130,10 @@ packages: dependency: transitive description: name: built_value - sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d + sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" url: "https://pub.dev" source: hosted - version: "8.12.0" + version: "8.12.3" characters: dependency: transitive description: @@ -222,6 +222,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + cookie_jar: + dependency: "direct main" + description: + name: cookie_jar + sha256: a6ac027d3ed6ed756bfce8f3ff60cb479e266f3b0fdabd6242b804b6765e52de + url: "https://pub.dev" + source: hosted + version: "4.0.8" cross_file: dependency: transitive description: @@ -318,6 +326,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.2" + dio_cookie_manager: + dependency: "direct main" + description: + name: dio_cookie_manager + sha256: d39c16abcc711c871b7b29bd51c6b5f3059ef39503916c6a9df7e22c4fc595e0 + url: "https://pub.dev" + source: hosted + version: "3.3.0" dio_web_adapter: dependency: transitive description: @@ -630,10 +646,10 @@ packages: dependency: transitive description: name: http - sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.0" http_multi_server: dependency: transitive description: @@ -654,10 +670,10 @@ packages: dependency: transitive description: name: image - sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" + sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" url: "https://pub.dev" source: hosted - version: "4.5.4" + version: "4.7.2" image_picker: dependency: "direct main" description: @@ -1435,10 +1451,10 @@ packages: dependency: transitive description: name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.1" web: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index c67a9b5f..3720bbe2 100755 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -1,7 +1,7 @@ name: geosector_app description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers' publish_to: 'none' -version: 3.5.2+352 +version: 3.6.2+362 environment: sdk: '>=3.0.0 <4.0.0' @@ -22,6 +22,8 @@ dependencies: # API & Réseau dio: ^5.3.3 + cookie_jar: ^4.0.8 # Gestion des cookies pour sessions PHP + dio_cookie_manager: ^3.1.1 # Interceptor cookies pour Dio connectivity_plus: 6.0.5 # ⬇️ Compatible Flutter 3.24.5 LTS (fix Gradle) retry: ^3.1.2 diff --git a/app/pubspec.yaml.backup b/app/pubspec.yaml.backup new file mode 100755 index 00000000..334cc896 --- /dev/null +++ b/app/pubspec.yaml.backup @@ -0,0 +1,113 @@ +name: geosector_app +description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers' +publish_to: 'none' +version: 3.5.9+359 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + cupertino_icons: ^1.0.6 + + # Navigation + go_router: ^15.1.2 # ⬇️ Compatible Flutter 3.24.5 LTS (depuis 16.0.0) + + # État et gestion des données + hive: ^2.2.3 + hive_flutter: ^1.1.0 + + # API & Réseau + dio: ^5.3.3 + cookie_jar: ^4.0.8 # Gestion des cookies pour sessions PHP + dio_cookie_manager: ^3.1.1 # Interceptor cookies pour Dio + connectivity_plus: 6.0.5 # ⬇️ Compatible Flutter 3.24.5 LTS (fix Gradle) + retry: ^3.1.2 + + # UI et animations + google_fonts: ^6.1.0 + flutter_svg: ^2.0.9 + # package_info_plus: ^4.2.0 # ❌ SUPPRIMÉ - Remplacé par AppInfoService auto-généré (13/10/2025) + + # Utilitaires + intl: 0.19.0 # Version requise par flutter_localizations Flutter 3.24.5 LTS + uuid: ^4.2.1 + syncfusion_flutter_charts: 27.2.5 # ⬇️ Compatible Flutter 3.24.5 LTS (sweet spot) + # shared_preferences: ^2.3.3 # Remplacé par Hive pour cohérence architecturale + + # Cartes et géolocalisation + url_launcher: ^6.1.14 # ⬇️ Version stable (depuis 6.3.1) + flutter_map: ^7.0.2 # ⬇️ Compatible Flutter 3.24.5 LTS (depuis 8.2.2) + flutter_map_cache: ^1.5.1 # Compatible Flutter 3.24.5 LTS (Dart 3.5.4) + dio_cache_interceptor_hive_store: ^3.2.1 # ✅ Compatible flutter_map_cache 1.5.1 + path_provider: ^2.1.2 # Requis pour le cache + latlong2: ^0.9.1 + geolocator: ^13.0.3 # ⬇️ Downgrade depuis 14.0.2 (Flutter 3.24.5 LTS) + geolocator_android: 4.6.1 # ✅ Force version sans toARGB32() + universal_html: ^2.2.4 # Pour accéder à la localisation du navigateur (detection env) + # sensors_plus: ^3.1.0 # ❌ SUPPRIMÉ - Mode boussole retiré (feature optionnelle peu utilisée) (13/10/2025) + + # Chat et notifications + # mqtt5_client: ^4.11.0 + flutter_local_notifications: ^19.0.1 + + # Upload d'images + image_picker: ^0.8.9 # ⬇️ Avant SwiftPM (depuis 1.1.2) + + # Configuration YAML + yaml: ^3.1.2 + + # Stripe Terminal et détection device (V2) + device_info_plus: ^11.3.0 # ✅ Compatible Flutter 3.24.5 LTS (depuis 12.1.0) + battery_plus: 6.0.3 # ⬇️ Compatible Flutter 3.24.5 LTS (fix Gradle) + # network_info_plus: ^4.1.0 # ❌ SUPPRIMÉ - Remplacé par NetworkInterface natif Dart (13/10/2025) + nfc_manager: 3.3.0 # ✅ Version stable - Nécessite patch AndroidManifest (fix-nfc-manager.sh) + mek_stripe_terminal: ^4.6.0 + flutter_stripe: ^11.5.0 # ⬇️ Compatible Flutter 3.24.5 LTS (depuis 12.0.0) + permission_handler: ^12.0.1 + qr_flutter: ^4.1.0 # Génération de QR codes pour paiements Stripe + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 # ⬇️ Compatible Flutter 3.24.5 LTS (depuis 6.0.0) + hive_generator: ^2.0.1 + build_runner: ^2.4.6 + flutter_launcher_icons: ^0.14.4 + +flutter_launcher_icons: + android: true + ios: true + image_path: 'assets/images/icons/icon-1024.png' + min_sdk_android: 21 + adaptive_icon_background: '#FFFFFF' + adaptive_icon_foreground: 'assets/images/icons/icon-1024.png' + remove_alpha_ios: true + web: + generate: true + image_path: 'assets/images/icons/icon-1024.png' + background_color: '#FFFFFF' + theme_color: '#4B77BE' + windows: + generate: true + image_path: 'assets/images/icons/icon-1024.png' + icon_size: 48 + +flutter: + uses-material-design: true + + assets: + - assets/images/ + - assets/icons/ + - assets/animations/ + - lib/chat/chat_config.yaml + + fonts: + - family: Inter + fonts: + - asset: assets/fonts/InterVariable.ttf + - asset: assets/fonts/InterVariable-Italic.ttf + style: italic diff --git a/app/pubspec.yaml.bak b/app/pubspec.yaml.bak new file mode 100755 index 00000000..334cc896 --- /dev/null +++ b/app/pubspec.yaml.bak @@ -0,0 +1,113 @@ +name: geosector_app +description: 'GEOSECTOR - Gestion de distribution des calendriers par secteurs géographiques pour les amicales de pompiers' +publish_to: 'none' +version: 3.5.9+359 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + cupertino_icons: ^1.0.6 + + # Navigation + go_router: ^15.1.2 # ⬇️ Compatible Flutter 3.24.5 LTS (depuis 16.0.0) + + # État et gestion des données + hive: ^2.2.3 + hive_flutter: ^1.1.0 + + # API & Réseau + dio: ^5.3.3 + cookie_jar: ^4.0.8 # Gestion des cookies pour sessions PHP + dio_cookie_manager: ^3.1.1 # Interceptor cookies pour Dio + connectivity_plus: 6.0.5 # ⬇️ Compatible Flutter 3.24.5 LTS (fix Gradle) + retry: ^3.1.2 + + # UI et animations + google_fonts: ^6.1.0 + flutter_svg: ^2.0.9 + # package_info_plus: ^4.2.0 # ❌ SUPPRIMÉ - Remplacé par AppInfoService auto-généré (13/10/2025) + + # Utilitaires + intl: 0.19.0 # Version requise par flutter_localizations Flutter 3.24.5 LTS + uuid: ^4.2.1 + syncfusion_flutter_charts: 27.2.5 # ⬇️ Compatible Flutter 3.24.5 LTS (sweet spot) + # shared_preferences: ^2.3.3 # Remplacé par Hive pour cohérence architecturale + + # Cartes et géolocalisation + url_launcher: ^6.1.14 # ⬇️ Version stable (depuis 6.3.1) + flutter_map: ^7.0.2 # ⬇️ Compatible Flutter 3.24.5 LTS (depuis 8.2.2) + flutter_map_cache: ^1.5.1 # Compatible Flutter 3.24.5 LTS (Dart 3.5.4) + dio_cache_interceptor_hive_store: ^3.2.1 # ✅ Compatible flutter_map_cache 1.5.1 + path_provider: ^2.1.2 # Requis pour le cache + latlong2: ^0.9.1 + geolocator: ^13.0.3 # ⬇️ Downgrade depuis 14.0.2 (Flutter 3.24.5 LTS) + geolocator_android: 4.6.1 # ✅ Force version sans toARGB32() + universal_html: ^2.2.4 # Pour accéder à la localisation du navigateur (detection env) + # sensors_plus: ^3.1.0 # ❌ SUPPRIMÉ - Mode boussole retiré (feature optionnelle peu utilisée) (13/10/2025) + + # Chat et notifications + # mqtt5_client: ^4.11.0 + flutter_local_notifications: ^19.0.1 + + # Upload d'images + image_picker: ^0.8.9 # ⬇️ Avant SwiftPM (depuis 1.1.2) + + # Configuration YAML + yaml: ^3.1.2 + + # Stripe Terminal et détection device (V2) + device_info_plus: ^11.3.0 # ✅ Compatible Flutter 3.24.5 LTS (depuis 12.1.0) + battery_plus: 6.0.3 # ⬇️ Compatible Flutter 3.24.5 LTS (fix Gradle) + # network_info_plus: ^4.1.0 # ❌ SUPPRIMÉ - Remplacé par NetworkInterface natif Dart (13/10/2025) + nfc_manager: 3.3.0 # ✅ Version stable - Nécessite patch AndroidManifest (fix-nfc-manager.sh) + mek_stripe_terminal: ^4.6.0 + flutter_stripe: ^11.5.0 # ⬇️ Compatible Flutter 3.24.5 LTS (depuis 12.0.0) + permission_handler: ^12.0.1 + qr_flutter: ^4.1.0 # Génération de QR codes pour paiements Stripe + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 # ⬇️ Compatible Flutter 3.24.5 LTS (depuis 6.0.0) + hive_generator: ^2.0.1 + build_runner: ^2.4.6 + flutter_launcher_icons: ^0.14.4 + +flutter_launcher_icons: + android: true + ios: true + image_path: 'assets/images/icons/icon-1024.png' + min_sdk_android: 21 + adaptive_icon_background: '#FFFFFF' + adaptive_icon_foreground: 'assets/images/icons/icon-1024.png' + remove_alpha_ios: true + web: + generate: true + image_path: 'assets/images/icons/icon-1024.png' + background_color: '#FFFFFF' + theme_color: '#4B77BE' + windows: + generate: true + image_path: 'assets/images/icons/icon-1024.png' + icon_size: 48 + +flutter: + uses-material-design: true + + assets: + - assets/images/ + - assets/icons/ + - assets/animations/ + - lib/chat/chat_config.yaml + + fonts: + - family: Inter + fonts: + - asset: assets/fonts/InterVariable.ttf + - asset: assets/fonts/InterVariable-Italic.ttf + style: italic diff --git a/app/transfer-to-mac.sh b/app/transfer-to-mac.sh index 6014c4f7..d987b5dc 100755 --- a/app/transfer-to-mac.sh +++ b/app/transfer-to-mac.sh @@ -26,19 +26,52 @@ if [ ! -f "pubspec.yaml" ]; then exit 1 fi -# Récupérer la version depuis pubspec.yaml -VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //' | tr -d ' ') -VERSION_CODE=$(echo $VERSION | cut -d'+' -f2) +# Synchroniser la version depuis ../VERSION +echo -e "${BLUE}📋 Synchronisation de la version depuis ../VERSION...${NC}" +echo "" -if [ -z "$VERSION_CODE" ]; then - echo -e "${RED}Impossible de récupérer le version code depuis pubspec.yaml${NC}" +VERSION_FILE="../VERSION" +if [ ! -f "$VERSION_FILE" ]; then + echo -e "${RED}Erreur: Fichier VERSION introuvable : $VERSION_FILE${NC}" exit 1 fi -echo -e "${YELLOW}Version détectée :${NC} $VERSION" -echo -e "${YELLOW}Version code :${NC} $VERSION_CODE" +# Lire la version depuis le fichier (enlever espaces/retours à la ligne) +VERSION_NUMBER=$(cat "$VERSION_FILE" | tr -d '\n\r ' | tr -d '[:space:]') +if [ -z "$VERSION_NUMBER" ]; then + echo -e "${RED}Erreur: Le fichier VERSION est vide${NC}" + exit 1 +fi + +echo -e "${YELLOW}Version lue depuis $VERSION_FILE :${NC} $VERSION_NUMBER" + +# Calculer le versionCode (supprimer les points) +VERSION_CODE=$(echo $VERSION_NUMBER | tr -d '.') +if [ -z "$VERSION_CODE" ]; then + echo -e "${RED}Erreur: Impossible de calculer le versionCode${NC}" + exit 1 +fi + +echo -e "${YELLOW}Version code calculé :${NC} $VERSION_CODE" + +# Mettre à jour pubspec.yaml +echo -e "${BLUE}Mise à jour de pubspec.yaml...${NC}" +sed -i.bak "s/^version:.*/version: $VERSION_NUMBER+$VERSION_CODE/" pubspec.yaml + +# Vérifier que la mise à jour a réussi +UPDATED_VERSION=$(grep "^version:" pubspec.yaml | sed 's/version: //' | tr -d ' ') +if [ "$UPDATED_VERSION" != "$VERSION_NUMBER+$VERSION_CODE" ]; then + echo -e "${RED}Erreur: Échec de la mise à jour de pubspec.yaml${NC}" + echo -e "${RED}Attendu : $VERSION_NUMBER+$VERSION_CODE${NC}" + echo -e "${RED}Obtenu : $UPDATED_VERSION${NC}" + exit 1 +fi + +echo -e "${GREEN}✓ pubspec.yaml mis à jour : version: $VERSION_NUMBER+$VERSION_CODE${NC}" echo "" +VERSION="$VERSION_NUMBER+$VERSION_CODE" + # Construire le chemin de destination avec numéro de version DESTINATION_DIR="app_$VERSION_CODE" DESTINATION="$MAC_USER@$MAC_MINI_IP:/Users/pierre/dev/geosector/$DESTINATION_DIR" @@ -56,6 +89,7 @@ echo -e "${BLUE}rsync va créer le dossier de destination automatiquement${NC}" echo "" rsync -avz --progress \ + -e "ssh -o IdentitiesOnly=yes -o PubkeyAuthentication=no -o PreferredAuthentications=password" \ --rsync-path="mkdir -p /Users/pierre/dev/geosector/$DESTINATION_DIR && rsync" \ --exclude='build/' \ --exclude='.dart_tool/' \ diff --git a/docs/PLANNING-2026-Q1.md b/docs/PLANNING-2026-Q1.md new file mode 100644 index 00000000..01f6b8b8 --- /dev/null +++ b/docs/PLANNING-2026-Q1.md @@ -0,0 +1,214 @@ +# Planning Geosector Q1 2026 - COMPLET + +**Période** : 16/01/2026 - 16/03/2026 (60 jours) +**Tâches** : 126 tâches actives +**Priorités** : UI/UX et MAP en premier +**Stack Techno** : Flutter et Hive pour Web et Mobiles / API REST Full PHP8.3 +--- + +## PHASE 1 : BUGS CRITIQUES +### 16-18 janvier (3 jours) - 5 tâches + +| Date | ID | Tâche | Catégorie | +|------|-----|-------|-----------| +| 16/01 | #17 | ✅ Création membre impossible | BUG | +| 16/01 | #18 | ✅ Création opération impossible | BUG | +| 17/01 | #19 | ✅ Export opération cassé | BUG | +| 17/01 | #20 | Enregistrement des passages ne fonctionne pas | BUG | +| 18/01 | #14 | Bug F5 - déconnexion lors du rafraîchissement | BUG | + +--- + +## PHASE 2 : STRIPE iOS + UX +### 19-25 janvier (7 jours) - 14 tâches + +**Tâche principale** : #13 Tests Stripe iOS (5 jours du 19 au 23) + +| Date | Stripe iOS | En parallèle (UX) | +|------|------------|-------------------| +| 19/01 | #13 Jour 1 | #204 Design couleurs flashy | +| 19/01 | | #205 Écrans utilisateurs simplifiés | +| 20/01 | #13 Jour 2 | #113 Couleur repasses orange | +| 20/01 | | #72 Épaisseur police lisibilité | +| 21/01 | #13 Jour 3 | #71 Visibilité bouton "Envoyer message" | +| 21/01 | | #59 Listing rues invisible (clavier) | +| 22/01 | #13 Jour 4 | #46 Figer headers tableau Home | +| 22/01 | | #42 Historique adresses cliquables | +| 23/01 | #13 Jour 5 | #74 Simplifier DashboardLayout/AppScaffold | +| 23/01 | | #110 Supprimer refresh session partiels | +| 24/01 | Buffer | #28 Gestion reçus Flutter nouveaux champs | +| 25/01 | Buffer | #50 Modifier secteur au clic | +| 25/01 | | #41 Secteurs avec membres visible carte | + +--- + +## PHASE 3 : MAP / CARTE +### 26 janvier - 9 février (15 jours) - 28 tâches + +| Date | ID | Tâche | +|------|-----|-------| +| 26/01 | #206 | Corriger géolocalisation par défaut Rennes | +| 26/01 | #22 | S'assurer cache Mapbox en place | +| 27/01 | #215 | Mode boussole + carte IGN/satellite zoom max | +| 27/01 | #53 | Définir zoom maximal éviter sur-zoom | +| 28/01 | #37 | Clic sur la carte pour créer un passage | +| 28/01 | #61 | Valider passage directement depuis carte | +| 29/01 | #51 | Déplacer markers double-clic | +| 29/01 | #115 | Déplacement marker sans bouton Enregistrer | +| 30/01 | #123 | Déplacer rapidement un pointeur | +| 30/01 | #58 | Points carte devant textes (z-index) | +| 31/01 | #55 | Optimiser précision GPS mode terrain | +| 31/01 | #56 | Mode Web : se déplacer sur carte terrain | +| 01/02 | #57 | Mode terrain smartphone : zoom auto | +| 01/02 | #60 | Recherche rue hors proximité | +| 02/02 | #209 | Filtres Particuliers / Entreprises | +| 02/02 | #216 | Vérifier géolocalisation nouveau passage | +| 03/02 | #217 | Chercher adresse hors secteur | +| 03/02 | #49 | Secteur sans membre | +| 04/02 | #25 | Membres affectés en 1er modif secteur | +| 04/02 | #31 | Gestion ajout/suppression membre secteur | +| 05/02 | #54 | Style carte type Snapchat | +| 05/02 | #210 | Base SIREN géolocalisation entreprises | +| 06/02 | #67 | Graphique règlements par secteur | +| 06/02 | #104 | Tests multi-départements | +| 07/02 | #89 | Page clients paiements en ligne | +| 07/02 | #94 | Paiement en ligne formulaire passage | +| 08/02 | #96 | Option "Paiement par carte" | +| 08/02 | #99 | Paiement Stripe mode hors ligne | +| 09/02 | Buffer MAP | - | + +--- + +## PHASE 4 : STRIPE + PASSAGES +### 10-21 février (12 jours) - 20 tâches + +| Date | ID | Tâche | Cat | +|------|-----|-------|-----| +| 10/02 | #92 | 💳 Stripe (config générale) | STRIPE | +| 10/02 | #93 | Double configuration Stripe | STRIPE | +| 11/02 | #97 | Interface paiement sécurisée intégrée | STRIPE | +| 11/02 | #98 | Génération auto reçu après paiement | STRIPE | +| 13/02 | #207 | Dashboard clic card règlement filtrer | | +| 13/02 | #208 | Type règlement Virement bancaire à ajouter | | +| 14/02 | #62 | 📋 Gestion des passages | PASSAGE | +| 14/02 | #16 | Modifier passage sur l'application | PASSAGE | +| 15/02 | #40 | Suppression lot de passages | PASSAGE | +| 15/02 | #63 | Corbeille passages admin | PASSAGE | +| 16/02 | #64 | Supprimer passages sauvegardés | PASSAGE | +| 16/02 | #66 | Récupérer passages supprimés | PASSAGE | +| 17/02 | #65 | Désactiver envoi reçu temporaire | PASSAGE | +| 17/02 | #118 | Prévenir habitants du passage | PASSAGE | +| 18/02 | #119 | Historique montant année précédente | PASSAGE | +| 19/02 | #81 | Ralentissement suppressions amicales | BUG | +| 19/02 | #219 | Double authentification super-admin (fk_role=9) | ADMIN | +| 20-21/02 | Buffer | - | - | + +--- + +## PHASE 5 : ADMIN + MEMBRES +### 22 février - 6 mars (13 jours) - 29 tâches + +| Date | ID | Tâche | Cat | +|------|-----|-------|-----| +| 22/02 | #79 | 👑 Mode Super Admin | ADMIN | +| 22/02 | #80 | FAQ gérée depuis Super-Admin | ADMIN | +| 23/02 | #76 | Accès admin limité web uniquement | ADMIN | +| 23/02 | #77 | Choisir rôle admin/membre connexion | ADMIN | +| 24/02 | #78 | Admin peut se connecter utilisateur | ADMIN | +| 24/02 | #82 | Optimiser purge données | ADMIN | +| 25/02 | #83 | Filtres liste amicales | ADMIN | +| 25/02 | #85 | Distinguer amicales actives | ADMIN | +| 26/02 | #24 | Trier liste membres | ADMIN | +| 26/02 | #29 | Filtres liste membres | ADMIN | +| 27/02 | #33 | Communication membres <-> admin | ADMIN | +| 27/02 | #70 | Revoir chat complet | ADMIN | +| 28/02 | #108 | MQTT temps réel ⭐⭐⭐ | ADMIN | +| 28/02 | #43 | Nb amicales partenariat ODP | ADMIN | +| 01/03 | #211 | Modifier lots avec montants | ADMIN | +| 01/03 | #218 | Tests montée charge Poissy | ADMIN | +| 02/03 | #15 | Nouveau membre non synchronisé | MEMBRE | +| 02/03 | #23 | Emails failed intégrer base | MEMBRE | +| 03/03 | #26 | Figer membres combobox | MEMBRE | +| 03/03 | #27 | Autocomplete combobox membres | MEMBRE | +| 04/03 | #30 | Membres sélectionnés haut liste | MEMBRE | +| 04/03 | #32 | Modifier identifiant utilisateur | MEMBRE | +| 05/03 | #34 | Email non obligatoire | MEMBRE | +| 05/03 | #36 | Textes aide fiches membres | MEMBRE | +| 06/03 | #90 | 📧 Processus inscription | MEMBRE | +| 06/03 | #91 | 2 emails séparés inscription | MEMBRE | +| 06/03 | #117 | Prénoms accents majuscule | MEMBRE | +| 06/03 | #122 | Modif rapide email renvoi reçu | MEMBRE | + +--- + +## PHASE 6 : EXPORT + COM + DIVERS +### 7-16 mars (10 jours) - 30 tâches + +| Date | ID | Tâche | Cat | +|------|-----|-------|-----| +| 07/03 | #45 | Home filtres et graphes | EXPORT | +| 07/03 | #47 | Home bouton export données | EXPORT | +| 07/03 | #48 | Export par membre | EXPORT | +| 08/03 | #68 | Comparatif année précédente | EXPORT | +| 08/03 | #212 | Bergerac logs + export Excel | EXPORT | +| 09/03 | #35 | Bouton alerte 3s messagerie | COM | +| 09/03 | #109 | SMS impératif ⭐⭐⭐ | COM | +| 10/03 | #69 | Bloquer création opération | OPER | +| 10/03 | #86 | Suppression opé réactiver précédente | OPER | +| 10/03 | #87 | 🏢 Gestion Clients | OPER | +| 11/03 | #88 | Écran Clients créer/améliorer | OPER | +| 11/03 | #116 | Remarque sous adresse | OPER | +| 11/03 | #214 | Opérations afficher texte | OPER | +| 12/03 | #102 | Compatibilité appareils test | TEST | +| 12/03 | #103 | 🧪 Tests | TEST | +| 12/03 | #213 | Lots montant nb calendriers Poissy | TEST | +| 13/03 | #21 | Requêtes en attente dupliquées | AUTRE | +| 13/03 | #38 | Parrainage | AUTRE | +| 13/03 | #39 | Multilingue ? | AUTRE | +| 14/03 | #44 | Envoi contrat | AUTRE | +| 14/03 | #52 | Même adresse par niveau | AUTRE | +| 14/03 | #73 | Reconnaissance biométrique | AUTRE | +| 14/03 | #75 | Refactoriser responsabilités | AUTRE | +| 15/03 | #84 | Mode démo présentations | AUTRE | +| 15/03 | #105 | 🌍 Internationalization | AUTRE | +| 15/03 | #106 | Devises Franc Suisse | AUTRE | +| 15/03 | #107 | 📡 Fonctionnalités futures | AUTRE | +| 16/03 | #111 | iwanttobealone | AUTRE | +| 16/03 | #112 | db-backup site 256 | AUTRE | +| 16/03 | #114 | Liste adresses mail d6soft/unikoffice | AUTRE | +| 16/03 | #120 | Double auth faceId/touchId | AUTRE | +| 16/03 | #121 | Recette (lien) | AUTRE | + +--- + +## RÉCAPITULATIF + +| Phase | Période | Jours | Tâches | Focus | +|-------|---------|-------|--------|-------| +| 1 | 16-18/01 | 3 | 5 | Bugs critiques | +| 2 | 19-25/01 | 7 | 14 | **Stripe iOS #13** + UX | +| 3 | 26/01-09/02 | 15 | 28 | **MAP / Carte** | +| 4 | 10-21/02 | 12 | 20 | Stripe + Passages | +| 5 | 22/02-06/03 | 13 | 29 | Admin + Membres | +| 6 | 07-16/03 | 10 | 30 | Export + Divers | +| **TOTAL** | **60 jours** | | **126** | | + +--- + +## Jalons clés + +- **18/01** : Bugs critiques résolus +- **23/01** : Tests Stripe iOS terminés +- **09/02** : Carte/Map finalisée +- **21/02** : Paiements + Passages OK +- **06/03** : Admin + Membres OK +- **16/03** : **Livraison Q1 complète** + +--- + +## Notes + +- Rythme : ~2 tâches/jour en moyenne +- Weekends = buffer si besoin +- Tâches ⭐⭐⭐ (#108 MQTT, #109 SMS) intégrées dans le planning +- La tâche #13 (Stripe iOS) reste bloquante pour paiement mobile diff --git a/docs/geosector-point-20251230.txt b/docs/geosector-point-20251230.txt new file mode 100755 index 00000000..3dcf4049 --- /dev/null +++ b/docs/geosector-point-20251230.txt @@ -0,0 +1,97 @@ +Geosector point du 30/12/2025 + +Dans le choix des types d'un passage +Choisir le type de ce passage +"A repasser" + + +--- + +Design : revoir les couleurs pour qu'elles soient aussi flashy que l'ancienne version utilisateur +Rendre les écrans des utilisateurs les plus simples possibles sans saisie de filtre, etc... + +--- + +Historique : + +Carte : sur Rennes la geolocalisation par défaut à revoir +Filtres uniquement sur la page dashboard +Clic sur la card des types de règlement pour filtrer les passages + +Type de règlemenent : ajout Virement bancaire + +--- + +Filtres sur la carte : +Particuliers +Entreprises + +Base SIREN : voir si géolocalisation + +--- + +Option : donner le montant du passage de l'année dernière +Lorsque l'utilisateur saisit son passage il peut voir le montant de l'année dernière sur ce passage + +--- + +Pour les administrateurs : donner un max de possibilités + +- déplacer un pointeur +- modifier les lots avec des montants + +--- + +Bergerac : voir les logs + export Excel + +--- + +Export Excel : extractions Excel enrichies : feuilles par secteur, villes, membre avec graphiques ? + +--- + +SMS : à mettre en place avec envoi de reçu. Obligation ? + +--- + +Lot : gestion à revoir : montant et nombre de calendriers sur Poissy ? + +--- + +Gestion des opérations : mettre le texte directement + +--- + +Carte : créer un passage en cliquant directement sur la carte + +--- + +Carte : mode boussole et carte IGN avec mode satellite avec zoom max à définir + +--- + +Si nouveau passage : voir la géolocalisation ! + +--- + +Carte : repositionner les pointeurs sur la carte + +--- + +Admin : Dessin des secteurs sans affecter les secteurs et puis esuite cliquer sur un secteur pour affecter les membres + +--- + +En utilisateur, pouvoir chercher une adresse hors de ses secteurs et la sélectionner pour saisir un passage effectué + +--- + +Amicale et membres : filtre sur un nom d'amicaliste + +--- + +Faire des tests sur l'amicale de Poissy : 7878787878 : montée en charge et stabilité de l'API + +--- + + diff --git a/web/.env-deploy-geosector-dev b/web/.env-deploy-geosector-dev deleted file mode 100755 index 15098de3..00000000 --- a/web/.env-deploy-geosector-dev +++ /dev/null @@ -1,20 +0,0 @@ -# Configuration du serveur hôte Debian 12 -HOST_SSH_HOST=195.154.80.116 # Adresse IP du serveur hôte -HOST_SSH_USER=pierre # Utilisateur SSH sur le serveur hôte -HOST_SSH_PORT=22 # Port SSH du serveur hôte -HOST_SSH_KEY=/home/pierre/.ssh/id_rsa_mbpi # Clé SSH privée pour accéder au serveur hôte - -# Configuration du conteneur Incus hébergeant cette application -CT_PROJECT_NAME=default # Nom du projet Incus où se trouve le conteneur -CT_NAME=dva-geo # Nom du conteneur Incus -CT_IP=13.23.33.43 # IP interne du conteneur Incus -CT_SSH_USER=root # Utilisateur SSH dans le conteneur -CT_SSH_PORT=22 # Port SSH interne du conteneur -CT_SSH_KEY=/root/.ssh/id_rsa_in3_pierre # Clé SSH privée pour accéder au conteneur - -# Configuration de l'application -DOMAIN_NAME=dev.geosector.fr # Nom de domaine du site -SERVER_PORT=3000 # Port du serveur Node.js -ADMIN_PORT=3001 # Port du serveur d'administration -DEPLOY_DIR=/var/www # Répertoire de déploiement sur le conteneur -APP_NAME=geosector # Nom de l'application et du fichier de config nginx \ No newline at end of file diff --git a/web/CLAUDE.md b/web/CLAUDE.md deleted file mode 100755 index 316e6b6d..00000000 --- a/web/CLAUDE.md +++ /dev/null @@ -1,34 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Build Commands -- Development: `npm run dev` - Start Vite development server with hot-reload -- Build: `npm run build` - Build production-ready static files to `dist/` directory -- Preview: `npm run preview` - Preview production build locally - -## Project Structure -This is a Svelte + Vite + TailwindCSS project without SvelteKit. Key characteristics: -- Pure Svelte application with custom client-side routing -- TailwindCSS for styling with custom Figtree font family -- No server-side rendering or API routes -- Static site deployment via `deploy-web.sh` - -## Architecture Overview -- **Routing**: Custom client-side routing implemented in `App.svelte` using browser history API -- **Pages**: Located in `src/pages/` - each page is a Svelte component -- **Components**: Reusable components in `src/components/` -- **Services**: Business logic in `src/lib/` (analytics, cookies) -- **Styling**: TailwindCSS with configuration in `tailwind.config.js` -- **Assets**: Static assets in `public/` (fonts, images, icons) - -## Development Workflow -- Pages are added to `src/pages/` and imported in `App.svelte` -- Route handling is done through the `activePage` state variable -- Cookie consent and analytics tracking are integrated throughout -- Static deployment copies `dist/` contents to production server - -## Deployment -- Production deployment script: `deploy-web.sh` -- Builds and packages the application for deployment to Incus container -- Environment variables loaded from `.env-deploy-geosector-dev` \ No newline at end of file diff --git a/web/README.md b/web/README.md deleted file mode 100755 index 382941e0..00000000 --- a/web/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Svelte + Vite - -This template should help get you started developing with Svelte in Vite. - -## Recommended IDE Setup - -[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). - -## Need an official Svelte framework? - -Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. - -## Technical considerations - -**Why use this over SvelteKit?** - -- It brings its own routing solution which might not be preferable for some users. -- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. - -This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. - -Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. - -**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** - -Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. - -**Why include `.vscode/extensions.json`?** - -Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. - -**Why enable `checkJs` in the JS template?** - -It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of JavaScript, it is trivial to change the configuration. - -**Why is HMR not preserving my local component state?** - -HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/sveltejs/svelte-hmr/tree/master/packages/svelte-hmr#preservation-of-local-state). - -If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. - -```js -// store.js -// An extremely simple external store -import { writable } from 'svelte/store' -export default writable(0) -``` diff --git a/web/deploy-web.sh b/web/deploy-web.sh deleted file mode 100755 index ab2c7c9d..00000000 --- a/web/deploy-web.sh +++ /dev/null @@ -1,378 +0,0 @@ -#!/bin/bash - -# Script de déploiement unifié pour GEOSECTOR Web (Svelte) -# Version: 4.0 (Janvier 2025) -# Auteur: Pierre (avec l'aide de Claude) -# -# Usage: -# ./deploy-web.sh # Déploiement local DEV (build → container geo) -# ./deploy-web.sh rca # Livraison RECETTE (container geo → rca-geo) -# ./deploy-web.sh pra # Livraison PRODUCTION (rca-geo → pra-geo) - -set -euo pipefail - -cd /home/pierre/dev/geosector/web - -# ===================================== -# Configuration générale -# ===================================== - -# Paramètre optionnel pour l'environnement cible -TARGET_ENV=${1:-dev} - -# Configuration SSH -HOST_KEY="/home/pierre/.ssh/id_rsa_mbpi" -HOST_PORT="22" -HOST_USER="root" - -# Configuration des serveurs -RCA_HOST="195.154.80.116" # Serveur de recette -PRA_HOST="51.159.7.190" # Serveur de production - -# Configuration Incus -INCUS_PROJECT="default" -WEB_PATH="/var/www/geosector/web" -FINAL_OWNER="nginx" -FINAL_GROUP="nginx" - -# Configuration de sauvegarde -BACKUP_DIR="/data/backup/geosector" - -# Couleurs pour les messages -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[0;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# ===================================== -# Fonctions utilitaires -# ===================================== - -echo_step() { - echo -e "${GREEN}==>${NC} $1" -} - -echo_info() { - echo -e "${BLUE}Info:${NC} $1" -} - -echo_warning() { - echo -e "${YELLOW}Warning:${NC} $1" -} - -echo_error() { - echo -e "${RED}Error:${NC} $1" - exit 1 -} - -# Fonction pour créer une sauvegarde locale -create_local_backup() { - local archive_file=$1 - local backup_type=$2 - - echo_info "Creating backup in ${BACKUP_DIR}..." - - if [ ! -d "${BACKUP_DIR}" ]; then - mkdir -p "${BACKUP_DIR}" || echo_warning "Could not create backup directory ${BACKUP_DIR}" - fi - - if [ -d "${BACKUP_DIR}" ]; then - BACKUP_FILE="${BACKUP_DIR}/web-${backup_type}-$(date +%Y%m%d-%H%M%S).tar.gz" - cp "${archive_file}" "${BACKUP_FILE}" && { - echo_info "Backup saved to: ${BACKUP_FILE}" - echo_info "Backup size: $(du -h "${BACKUP_FILE}" | cut -f1)" - - # Nettoyer les anciens backups (garder les 10 derniers) - echo_info "Cleaning old backups (keeping last 10)..." - ls -t "${BACKUP_DIR}"/web-${backup_type}-*.tar.gz 2>/dev/null | tail -n +11 | xargs -r rm -f && { - REMAINING_BACKUPS=$(ls "${BACKUP_DIR}"/web-${backup_type}-*.tar.gz 2>/dev/null | wc -l) - echo_info "Kept ${REMAINING_BACKUPS} backup(s)" - } - } || echo_warning "Failed to create backup in ${BACKUP_DIR}" - fi -} - -# ===================================== -# Détermination de la configuration selon l'environnement -# ===================================== - -case $TARGET_ENV in - "dev") - echo_step "Configuring for LOCAL DEV deployment" - SOURCE_TYPE="local_build" - DEST_CONTAINER="geo" - DEST_HOST="local" - ENV_NAME="DEVELOPMENT" - ;; - "rca") - echo_step "Configuring for RECETTE delivery" - SOURCE_TYPE="local_container" - SOURCE_CONTAINER="geo" - DEST_CONTAINER="rca-geo" - DEST_HOST="${RCA_HOST}" - ENV_NAME="RECETTE" - ;; - "pra") - echo_step "Configuring for PRODUCTION delivery" - SOURCE_TYPE="remote_container" - SOURCE_HOST="${RCA_HOST}" - SOURCE_CONTAINER="rca-geo" - DEST_CONTAINER="pra-geo" - DEST_HOST="${PRA_HOST}" - ENV_NAME="PRODUCTION" - ;; - *) - echo_error "Unknown environment: $TARGET_ENV. Use 'dev', 'rca' or 'pra'" - ;; -esac - -echo_info "Deployment flow: ${ENV_NAME}" - -# ===================================== -# Création de l'archive selon la source -# ===================================== - -TIMESTAMP=$(date +%s) -ARCHIVE_NAME="web-deploy-${TIMESTAMP}.tar.gz" -TEMP_ARCHIVE="/tmp/${ARCHIVE_NAME}" - -if [ "$SOURCE_TYPE" = "local_build" ]; then - # DEV: Build Svelte et créer une archive - echo_step "Building Svelte app for DEV..." - - # Variables du projet - BUILD_DIR="dist" - SERVER_DIR="server" - LOCAL_DEPLOY_DIR="deploy" - - # Installer les dépendances si nécessaire - if [ ! -d "node_modules" ] || [ ! -f "package-lock.json" ]; then - echo_info "Installing dependencies..." - npm install || echo_error "npm install failed" - fi - - # Build du frontend principal - echo_info "Building frontend..." - npm run build || echo_error "Build failed" - - # Vérifier que le build a réussi - if [ ! -d "$BUILD_DIR" ]; then - echo_error "Build directory not found" - fi - - # Préparer le package de déploiement - echo_info "Preparing deployment package..." - rm -rf $LOCAL_DEPLOY_DIR - mkdir -p $LOCAL_DEPLOY_DIR - - # Copier les fichiers frontend - cp -r $BUILD_DIR/* $LOCAL_DEPLOY_DIR/ - - # Préparer le dossier serveur si nécessaire - if [ -d "$SERVER_DIR" ]; then - echo_info "Preparing server files..." - mkdir -p $LOCAL_DEPLOY_DIR/server - cp -r $SERVER_DIR/package.json $LOCAL_DEPLOY_DIR/server/ 2>/dev/null || echo_warning "package.json not found" - cp -r $SERVER_DIR/server.js $LOCAL_DEPLOY_DIR/server/ 2>/dev/null || echo_warning "server.js not found" - cp -r $SERVER_DIR/.env $LOCAL_DEPLOY_DIR/server/ 2>/dev/null || echo_warning ".env not found" - mkdir -p $LOCAL_DEPLOY_DIR/server/logs - fi - - # Créer l'archive - echo_info "Creating archive..." - COPYFILE_DISABLE=1 tar --exclude=".*" -czf "${TEMP_ARCHIVE}" -C $LOCAL_DEPLOY_DIR . || echo_error "Failed to create archive" - - create_local_backup "${TEMP_ARCHIVE}" "dev" - - # Nettoyer - rm -rf $LOCAL_DEPLOY_DIR - -elif [ "$SOURCE_TYPE" = "local_container" ]; then - # RCA: Créer une archive depuis le container local - echo_step "Creating archive from local container ${SOURCE_CONTAINER}..." - - echo_info "Switching to Incus project ${INCUS_PROJECT}..." - incus project switch ${INCUS_PROJECT} || echo_error "Failed to switch project" - - # Créer l'archive directement depuis le container local - incus exec ${SOURCE_CONTAINER} -- tar -czf /tmp/${ARCHIVE_NAME} -C ${WEB_PATH} . || echo_error "Failed to create archive from container" - - # Récupérer l'archive depuis le container - incus file pull ${SOURCE_CONTAINER}/tmp/${ARCHIVE_NAME} ${TEMP_ARCHIVE} || echo_error "Failed to pull archive from container" - incus exec ${SOURCE_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} - - create_local_backup "${TEMP_ARCHIVE}" "to-rca" - -elif [ "$SOURCE_TYPE" = "remote_container" ]; then - # PRA: Créer une archive depuis un container distant - echo_step "Creating archive from remote container ${SOURCE_CONTAINER} on ${SOURCE_HOST}..." - - # Créer l'archive sur le serveur source - ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} " - incus project switch ${INCUS_PROJECT} && - incus exec ${SOURCE_CONTAINER} -- tar -czf /tmp/${ARCHIVE_NAME} -C ${WEB_PATH} . - " || echo_error "Failed to create archive on remote" - - # Extraire l'archive du container vers l'hôte - ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} " - incus file pull ${SOURCE_CONTAINER}/tmp/${ARCHIVE_NAME} /tmp/${ARCHIVE_NAME} && - incus exec ${SOURCE_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} - " || echo_error "Failed to extract archive from remote container" - - # Copier l'archive vers la machine locale pour backup - scp -i ${HOST_KEY} -P ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST}:/tmp/${ARCHIVE_NAME} ${TEMP_ARCHIVE} || echo_error "Failed to copy archive locally" - - create_local_backup "${TEMP_ARCHIVE}" "to-pra" -fi - -ARCHIVE_SIZE=$(du -h "${TEMP_ARCHIVE}" | cut -f1) -echo_info "Archive size: ${ARCHIVE_SIZE}" - -# ===================================== -# Déploiement selon la destination -# ===================================== - -if [ "$DEST_HOST" = "local" ]; then - # Déploiement sur container local (DEV) - echo_step "Deploying to local container ${DEST_CONTAINER}..." - - echo_info "Switching to Incus project ${INCUS_PROJECT}..." - incus project switch ${INCUS_PROJECT} || echo_error "Failed to switch to project ${INCUS_PROJECT}" - - echo_info "Pushing archive to container..." - incus file push "${TEMP_ARCHIVE}" ${DEST_CONTAINER}/tmp/${ARCHIVE_NAME} || echo_error "Failed to push archive to container" - - echo_info "Preparing deployment directory..." - incus exec ${DEST_CONTAINER} -- mkdir -p ${WEB_PATH} || echo_error "Failed to create deployment directory" - incus exec ${DEST_CONTAINER} -- rm -rf ${WEB_PATH}/* || echo_warning "Could not clean deployment directory" - - echo_info "Extracting archive..." - incus exec ${DEST_CONTAINER} -- tar -xzf /tmp/${ARCHIVE_NAME} -C ${WEB_PATH}/ || echo_error "Failed to extract archive" - - echo_info "Setting permissions..." - incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_GROUP} ${WEB_PATH} - incus exec ${DEST_CONTAINER} -- find ${WEB_PATH} -type d -exec chmod 755 {} \; - incus exec ${DEST_CONTAINER} -- find ${WEB_PATH} -type f -exec chmod 644 {} \; - - # Permissions spéciales pour les dossiers server - incus exec ${DEST_CONTAINER} -- sh -c " - if [ -f ${WEB_PATH}/server/server.js ]; then - chmod +x ${WEB_PATH}/server/server.js - fi - if [ -d ${WEB_PATH}/server/logs ]; then - chmod 775 ${WEB_PATH}/server/logs - fi - " || true - - # Installer les dépendances du serveur si présent - echo_info "Installing server dependencies if needed..." - incus exec ${DEST_CONTAINER} -- sh -c " - if [ -d ${WEB_PATH}/server ] && [ -f ${WEB_PATH}/server/package.json ]; then - cd ${WEB_PATH}/server && npm install --production - fi - " || echo_warning "Server dependencies installation skipped" - - echo_info "Cleaning up..." - incus exec ${DEST_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} - -else - # Déploiement sur container distant (RCA ou PRA) - echo_step "Deploying to remote container ${DEST_CONTAINER} on ${DEST_HOST}..." - - # Créer une sauvegarde sur le serveur de destination - BACKUP_TIMESTAMP=$(date +"%Y%m%d_%H%M%S") - REMOTE_BACKUP_DIR="${WEB_PATH}_backup_${BACKUP_TIMESTAMP}" - - echo_info "Creating backup on destination..." - ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} " - incus project switch ${INCUS_PROJECT} && - incus exec ${DEST_CONTAINER} -- test -d ${WEB_PATH} && - incus exec ${DEST_CONTAINER} -- cp -r ${WEB_PATH} ${REMOTE_BACKUP_DIR} && - echo 'Backup created: ${REMOTE_BACKUP_DIR}' - " || echo_warning "No existing installation to backup" - - # Transférer l'archive vers le serveur de destination - echo_info "Transferring archive to ${DEST_HOST}..." - - if [ "$SOURCE_TYPE" = "local_container" ]; then - # Pour RCA: copier depuis local vers distant - scp -i ${HOST_KEY} -P ${HOST_PORT} ${TEMP_ARCHIVE} ${HOST_USER}@${DEST_HOST}:/tmp/${ARCHIVE_NAME} || echo_error "Failed to copy archive to destination" - else - # Pour PRA: copier de serveur à serveur - ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} " - scp -i ${HOST_KEY} -P ${HOST_PORT} /tmp/${ARCHIVE_NAME} ${HOST_USER}@${DEST_HOST}:/tmp/${ARCHIVE_NAME} - " || echo_error "Failed to transfer archive between servers" - - # Nettoyer sur le serveur source - ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${SOURCE_HOST} "rm -f /tmp/${ARCHIVE_NAME}" - fi - - # Déployer sur le container de destination - echo_info "Extracting on destination container..." - ssh -i ${HOST_KEY} -p ${HOST_PORT} ${HOST_USER}@${DEST_HOST} " - set -euo pipefail - - # Pousser l'archive dans le container - incus project switch ${INCUS_PROJECT} && - incus file push /tmp/${ARCHIVE_NAME} ${DEST_CONTAINER}/tmp/${ARCHIVE_NAME} && - - # Nettoyer et recréer le dossier - incus exec ${DEST_CONTAINER} -- rm -rf ${WEB_PATH} && - incus exec ${DEST_CONTAINER} -- mkdir -p ${WEB_PATH} && - - # Extraire l'archive - incus exec ${DEST_CONTAINER} -- tar -xzf /tmp/${ARCHIVE_NAME} -C ${WEB_PATH}/ && - - # Permissions - incus exec ${DEST_CONTAINER} -- chown -R ${FINAL_OWNER}:${FINAL_GROUP} ${WEB_PATH} && - incus exec ${DEST_CONTAINER} -- find ${WEB_PATH} -type d -exec chmod 755 {} \; && - incus exec ${DEST_CONTAINER} -- find ${WEB_PATH} -type f -exec chmod 644 {} \; && - - # Permissions spéciales pour server - incus exec ${DEST_CONTAINER} -- sh -c ' - if [ -f ${WEB_PATH}/server/server.js ]; then - chmod +x ${WEB_PATH}/server/server.js - fi - if [ -d ${WEB_PATH}/server/logs ]; then - chmod 775 ${WEB_PATH}/server/logs - fi - ' || true && - - # Installer les dépendances du serveur si présent - incus exec ${DEST_CONTAINER} -- sh -c ' - if [ -d ${WEB_PATH}/server ] && [ -f ${WEB_PATH}/server/package.json ]; then - cd ${WEB_PATH}/server && npm install --production - fi - ' || echo 'Server dependencies skipped' && - - # Nettoyage - incus exec ${DEST_CONTAINER} -- rm -f /tmp/${ARCHIVE_NAME} && - rm -f /tmp/${ARCHIVE_NAME} - " || echo_error "Deployment failed on destination" - - echo_info "Remote backup saved: ${REMOTE_BACKUP_DIR} on ${DEST_CONTAINER}" -fi - -# Nettoyage local -rm -f "${TEMP_ARCHIVE}" - -# ===================================== -# Résumé final -# ===================================== - -echo_step "Deployment completed successfully!" -echo_info "Environment: ${ENV_NAME}" - -if [ "$TARGET_ENV" = "dev" ]; then - echo_info "Built and deployed Svelte web app to container ${DEST_CONTAINER}" -elif [ "$TARGET_ENV" = "rca" ]; then - echo_info "Delivered from ${SOURCE_CONTAINER} (local) to ${DEST_CONTAINER} on ${DEST_HOST}" -elif [ "$TARGET_ENV" = "pra" ]; then - echo_info "Delivered from ${SOURCE_CONTAINER} on ${SOURCE_HOST} to ${DEST_CONTAINER} on ${DEST_HOST}" -fi - -echo_info "Deployment completed at: $(date)" - -# Journaliser le déploiement -echo "$(date '+%Y-%m-%d %H:%M:%S') - Web app deployed to ${ENV_NAME} (${DEST_CONTAINER})" >> ~/.geo_deploy_history \ No newline at end of file diff --git a/web/deploy/assets/index-CMFopX_9.js b/web/deploy/assets/index-CMFopX_9.js deleted file mode 100755 index c800c405..00000000 --- a/web/deploy/assets/index-CMFopX_9.js +++ /dev/null @@ -1,65 +0,0 @@ -var Ps=Object.defineProperty;var js=(e,s,t)=>s in e?Ps(e,s,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[s]=t;var rt=(e,s,t)=>js(e,typeof s!="symbol"?s+"":s,t);(function(){const s=document.createElement("link").relList;if(s&&s.supports&&s.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))i(o);new MutationObserver(o=>{for(const n of o)if(n.type==="childList")for(const a of n.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&i(a)}).observe(document,{childList:!0,subtree:!0});function t(o){const n={};return o.integrity&&(n.integrity=o.integrity),o.referrerPolicy&&(n.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?n.credentials="include":o.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function i(o){if(o.ep)return;o.ep=!0;const n=t(o);fetch(o.href,n)}})();const Tt=!1;var gt=Array.isArray,Ns=Array.prototype.indexOf,Us=Array.from,Ds=Object.defineProperty,Ve=Object.getOwnPropertyDescriptor,Wt=Object.getOwnPropertyDescriptors,Vs=Object.prototype,Is=Array.prototype,bt=Object.getPrototypeOf,zt=Object.isExtensible;function Rs(e){return e()}function Qe(e){for(var s=0;s{i.d=!0})}function X(e){const s=S;if(s!==null){const a=s.e;if(a!==null){var t=y,i=x;s.e=null;try{for(var o=0;o{var r=x;ie(n);var d=u();return ie(r),d};return i&&t.set("length",pe(e.length)),new Proxy(e,{defineProperty(u,r,d){(!("value"in d)||d.configurable===!1||d.enumerable===!1||d.writable===!1)&&Ks();var f=t.get(r);return f===void 0?f=a(()=>{var p=pe(d.value);return t.set(r,p),p}):q(f,d.value,!0),!0},deleteProperty(u,r){var d=t.get(r);if(d===void 0){if(r in u){const v=a(()=>pe(I));t.set(r,v),lt(o)}}else{if(i&&typeof r=="string"){var f=t.get("length"),p=Number(r);Number.isInteger(p)&&p{var b=Ue(p?u[r]:I),w=pe(b);return w}),t.set(r,f)),f!==void 0){var v=l(f);return v===I?void 0:v}return Reflect.get(u,r,d)},getOwnPropertyDescriptor(u,r){var d=Reflect.getOwnPropertyDescriptor(u,r);if(d&&"value"in d){var f=t.get(r);f&&(d.value=l(f))}else if(d===void 0){var p=t.get(r),v=p==null?void 0:p.v;if(p!==void 0&&v!==I)return{enumerable:!0,configurable:!0,value:v,writable:!0}}return d},has(u,r){var v;if(r===Ie)return!0;var d=t.get(r),f=d!==void 0&&d.v!==I||Reflect.has(u,r);if(d!==void 0||y!==null&&(!f||(v=Ve(u,r))!=null&&v.writable)){d===void 0&&(d=a(()=>{var h=f?Ue(u[r]):I,b=pe(h);return b}),t.set(r,d));var p=l(d);if(p===I)return!1}return f},set(u,r,d,f){var A;var p=t.get(r),v=r in u;if(i&&r==="length")for(var h=d;hpe(I)),t.set(h+"",b))}if(p===void 0)(!v||(A=Ve(u,r))!=null&&A.writable)&&(p=a(()=>pe(void 0)),q(p,Ue(d)),t.set(r,p));else{v=p.v!==I;var w=a(()=>Ue(d));q(p,w)}var k=Reflect.getOwnPropertyDescriptor(u,r);if(k!=null&&k.set&&k.set.call(f,d),!v){if(i&&typeof r=="string"){var g=t.get("length"),_=Number(r);Number.isInteger(_)&&_>=g.v&&q(g,_+1)}lt(o)}return!0},ownKeys(u){l(o);var r=Reflect.ownKeys(u).filter(p=>{var v=t.get(p);return v===void 0||v.v!==I});for(var[d,f]of t)f.v!==I&&!(d in u)&&r.push(d);return r},setPrototypeOf(){Ys()}})}function lt(e,s=1){q(e,e.v+s)}function kt(e){var s=K|re,t=x!==null&&(x.f&K)!==0?x:null;return y===null||t!==null&&(t.f&O)!==0?s|=O:y.f|=Zt,{ctx:S,deps:null,effects:null,equals:Jt,f:s,fn:e,reactions:null,rv:0,v:null,wv:0,parent:t??y,ac:null}}function me(e){const s=kt(e);return s.equals=Xt,s}function ts(e){var s=e.effects;if(s!==null){e.effects=null;for(var t=0;tl(e))),s}function q(e,s,t=!1){x!==null&&(!se||(x.f&Gt)!==0)&&Ke()&&(x.f&(K|xt|Gt))!==0&&!(z!=null&&z[1].includes(e)&&z[0]===x)&&Zs();let i=t?Ue(s):s;return ii(e,i)}function ii(e,s){if(!e.equals(s)){var t=e.v;Te?Fe.set(e,s):Fe.set(e,t),e.v=s,(e.f&K)!==0&&((e.f&re)!==0&&ss(e),oe(e,(e.f&O)===0?Y:_e)),e.wv=ws(),os(e,re),Ke()&&y!==null&&(y.f&Y)!==0&&(y.f&(le|Le))===0&&(W===null?bi([e]):W.push(e))}return s}function os(e,s){var t=e.reactions;if(t!==null)for(var i=Ke(),o=t.length,n=0;nnew Promise(i=>{t.outro?vt(s,()=>{we(s),i(void 0)}):(we(s),i(void 0))})}function cs(e){return Me(Kt,e,!1)}function At(e){return Me(tt,e,!0)}function ae(e,s=[],t=kt){const i=s.map(t);return ds(()=>e(...i.map(l)))}function ds(e,s=0){var t=Me(tt|xt|s,e,!0);return t}function pt(e,s=!0){return Me(tt|le,e,!0,s)}function ps(e){var s=e.teardown;if(s!==null){const t=Te,i=x;Ut(!0),ie(null);try{s.call(null)}finally{Ut(t),ie(i)}}}function vs(e,s=!1){var o;var t=e.first;for(e.first=e.last=null;t!==null;){(o=t.ac)==null||o.abort(Qt);var i=t.next;(t.f&Le)!==0?t.parent=null:we(t,s),t=i}}function ci(e){for(var s=e.first;s!==null;){var t=s.next;(s.f&le)===0&&we(s),s=t}}function we(e,s=!0){var t=!1;(s||(e.f&Fs)!==0)&&e.nodes_start!==null&&e.nodes_end!==null&&(di(e.nodes_start,e.nodes_end),t=!0),vs(e,s&&!t),et(e,0),oe(e,_t);var i=e.transitions;if(i!==null)for(const n of i)n.stop();ps(e);var o=e.parent;o!==null&&o.first!==null&&fs(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes_start=e.nodes_end=e.ac=null}function di(e,s){for(;e!==null;){var t=e===s?null:qt(e);e.remove(),e=t}}function fs(e){var s=e.parent,t=e.prev,i=e.next;t!==null&&(t.next=i),i!==null&&(i.prev=t),s!==null&&(s.first===e&&(s.first=i),s.last===e&&(s.last=t))}function vt(e,s){var t=[];ms(e,t,!0),pi(t,()=>{we(e),s&&s()})}function pi(e,s){var t=e.length;if(t>0){var i=()=>--t||s();for(var o of e)o.out(i)}else s()}function ms(e,s,t){if((e.f&Ee)===0){if(e.f^=Ee,e.transitions!==null)for(const a of e.transitions)(a.is_global||t)&&s.push(a);for(var i=e.first;i!==null;){var o=i.next,n=(i.f&yt)!==0||(i.f&le)!==0;ms(i,s,n?t:!1),i=o}}}function Nt(e){hs(e,!0)}function hs(e,s){if((e.f&Ee)!==0){e.f^=Ee;for(var t=e.first;t!==null;){var i=t.next,o=(t.f&yt)!==0||(t.f&le)!==0;hs(t,o?s:!1),t=i}if(e.transitions!==null)for(const n of e.transitions)(n.is_global||s)&&n.in()}}let Oe=[],ft=[];function gs(){var e=Oe;Oe=[],Qe(e)}function vi(){var e=ft;ft=[],Qe(e)}function fi(e){Oe.length===0&&queueMicrotask(gs),Oe.push(e)}function mi(){Oe.length>0&&gs(),ft.length>0&&vi()}function hi(e){var s=y;if((s.f&Yt)===0){if((s.f&wt)===0)throw e;s.fn(e)}else bs(e,s)}function bs(e,s){for(;s!==null;){if((s.f&wt)!==0)try{s.b.error(e);return}catch{}s=s.parent}throw e}let Be=!1,$e=null,xe=!1,Te=!1;function Ut(e){Te=e}let Re=[];let x=null,se=!1;function ie(e){x=e}let y=null;function fe(e){y=e}let z=null;function gi(e){x!==null&&x.f&ct&&(z===null?z=[x,[e]]:z[1].push(e))}let D=null,F=0,W=null;function bi(e){W=e}let xs=1,Xe=0,ve=!1;function ws(){return++xs}function st(e){var p;var s=e.f;if((s&re)!==0)return!0;if((s&_e)!==0){var t=e.deps,i=(s&O)!==0;if(t!==null){var o,n,a=(s&Je)!==0,u=i&&y!==null&&!ve,r=t.length;if(a||u){var d=e,f=d.parent;for(o=0;oe.wv)return!0}(!i||y!==null&&!ve)&&oe(e,Y)}return!1}function _s(e,s,t=!0){var i=e.reactions;if(i!==null)for(var o=0;o0)for(p.length=F+D.length,v=0;v0;){s++>1e3&&wi();var t=Re,i=t.length;Re=[];for(var o=0;o{Promise.resolve().then(()=>{var s;if(!e.defaultPrevented)for(const t of e.target.elements)(s=t.__on_r)==null||s.call(t)})},{capture:!0}))}function Cs(e){var s=x,t=y;ie(null),fe(null);try{return e()}finally{ie(s),fe(t)}}function qs(e,s,t,i=t){e.addEventListener(s,()=>Cs(t));const o=e.__on_r;o?e.__on_r=()=>{o(),i(!0)}:e.__on_r=()=>i(!0),Li()}const Mi=new Set,Vt=new Set;function Ti(e,s,t,i={}){function o(n){if(i.capture||De.call(s,n),!n.cancelBubble)return Cs(()=>t==null?void 0:t.call(this,n))}return e.startsWith("pointer")||e.startsWith("touch")||e==="wheel"?fi(()=>{s.addEventListener(e,o,i)}):s.addEventListener(e,o,i),o}function C(e,s,t,i,o){var n={capture:i,passive:o},a=Ti(e,s,t,n);(s===document.body||s===window||s===document||s instanceof HTMLMediaElement)&&us(()=>{s.removeEventListener(e,a,n)})}function De(e){var _;var s=this,t=s.ownerDocument,i=e.type,o=((_=e.composedPath)==null?void 0:_.call(e))||[],n=o[0]||e.target,a=0,u=e.__root;if(u){var r=o.indexOf(u);if(r!==-1&&(s===document||s===window)){e.__root=s;return}var d=o.indexOf(s);if(d===-1)return;r<=d&&(a=r)}if(n=o[a]||e.target,n!==s){Ds(e,"currentTarget",{configurable:!0,get(){return n||t}});var f=x,p=y;ie(null),fe(null);try{for(var v,h=[];n!==null;){var b=n.assignedSlot||n.parentNode||n.host||null;try{var w=n["__"+i];if(w!=null&&(!n.disabled||e.target===n))if(gt(w)){var[k,...g]=w;k.apply(n,[e,...g])}else w.call(n,e)}catch(A){v?h.push(A):v=A}if(e.cancelBubble||b===s||b===null)break;n=b}if(v){for(let A of h)queueMicrotask(()=>{throw A});throw v}}finally{e.__root=s,delete e.currentTarget,ie(f),fe(p)}}}function As(e){var s=document.createElement("template");return s.innerHTML=e.replaceAll("",""),s.content}function ht(e,s){var t=y;t.nodes_start===null&&(t.nodes_start=e,t.nodes_end=s)}function R(e,s){var t=(s&Xs)!==0,i=(s&ei)!==0,o,n=!e.startsWith("");return()=>{o===void 0&&(o=As(n?e:""+e),t||(o=Se(o)));var a=i||ns?document.importNode(o,!0):o.cloneNode(!0);if(t){var u=Se(a),r=a.lastChild;ht(u,r)}else ht(a,a);return a}}function zi(e,s,t="svg"){var i=!e.startsWith(""),o=`<${t}>${i?e:""+e}`,n;return()=>{if(!n){var a=As(o),u=Se(a);n=Se(u)}var r=n.cloneNode(!0);return ht(r,r),r}}function Es(e,s){return zi(e,s,"svg")}function N(e,s){e!==null&&e.before(s)}function it(e,s){var t=s==null?"":typeof s=="object"?s+"":s;t!==(e.__t??(e.__t=e.nodeValue))&&(e.__t=t,e.nodeValue=t+"")}function Gi(e,s){return Pi(e,s)}const qe=new Map;function Pi(e,{target:s,anchor:t,props:i={},events:o,context:n,intro:a=!0}){oi();var u=new Set,r=p=>{for(var v=0;v{var p=t??s.appendChild(ni());return pt(()=>{if(n){J({});var v=S;v.c=n}o&&(i.$$events=o),d=e(p,i)||{},n&&X()}),()=>{var b;for(var v of u){s.removeEventListener(v,De);var h=qe.get(v);--h===0?(document.removeEventListener(v,De),qe.delete(v)):qe.set(v,h)}Vt.delete(r),p!==t&&((b=p.parentNode)==null||b.removeChild(p))}});return ji.set(d,f),d}let ji=new WeakMap;function te(e,s,[t,i]=[0,0]){var o=e,n=null,a=null,u=I,r=t>0?yt:0,d=!1;const f=(v,h=!0)=>{d=!0,p(h,v)},p=(v,h)=>{u!==(u=v)&&(u?(n?Nt(n):h&&(n=pt(()=>h(o))),a&&vt(a,()=>{a=null})):(a?Nt(a):h&&(a=pt(()=>h(o,[t+1,i]))),n&&vt(n,()=>{n=null})))};ds(()=>{d=!1,s(f),d||p(null,null)},r)}const It=[...` -\r\f \v\uFEFF`];function Ni(e,s,t){var i=e==null?"":""+e;if(s&&(i=i?i+" "+s:s),t){for(var o in t)if(t[o])i=i?i+" "+o:o;else if(i.length)for(var n=o.length,a=0;(a=i.indexOf(o,a))>=0;){var u=a+n;(a===0||It.includes(i[a-1]))&&(u===i.length||It.includes(i[u]))?i=(a===0?"":i.substring(0,a))+i.substring(u+1):a=u}}return i===""?null:i}function L(e,s,t,i,o,n){var a=e.__className;if(a!==t||a===void 0){var u=Ni(t,i,n);u==null?e.removeAttribute("class"):e.className=u,e.__className=t}else if(n&&o!==n)for(var r in n){var d=!!n[r];(o==null||d!==!!o[r])&&e.classList.toggle(r,d)}return n}const Ui=Symbol("is custom element"),Di=Symbol("is html");function Ae(e,s,t,i){var o=Vi(e);o[s]!==(o[s]=t)&&(t==null?e.removeAttribute(s):typeof t!="string"&&Ii(e).includes(s)?e[s]=t:e.setAttribute(s,t))}function Vi(e){return e.__attributes??(e.__attributes={[Ui]:e.nodeName.includes("-"),[Di]:e.namespaceURI===ti})}var Rt=new Map;function Ii(e){var s=Rt.get(e.nodeName);if(s)return s;Rt.set(e.nodeName,s=[]);for(var t,i=e,o=Element.prototype;o!==i;){t=Wt(i);for(var n in t)t[n].set&&s.push(n);i=bt(i)}return s}function Ne(e,s,t=s){var i=Ke();qs(e,"input",o=>{var n=o?e.defaultValue:e.value;if(n=at(e)?ut(n):n,t(n),i&&n!==(n=s())){var a=e.selectionStart,u=e.selectionEnd;e.value=n??"",u!==null&&(e.selectionStart=a,e.selectionEnd=Math.min(u,e.value.length))}}),Ye(s)==null&&e.value&&t(at(e)?ut(e.value):e.value),At(()=>{var o=s();at(e)&&o===ut(e.value)||e.type==="date"&&!o&&!e.value||o!==e.value&&(e.value=o??"")})}function Ri(e,s,t=s){qs(e,"change",i=>{var o=i?e.defaultChecked:e.checked;t(o)}),Ye(s)==null&&t(e.checked),At(()=>{var i=s();e.checked=!!i})}function at(e){var s=e.type;return s==="number"||s==="range"}function ut(e){return e===""?null:+e}function Fi(e){return function(...s){var t=s[0];return t.stopPropagation(),e==null?void 0:e.apply(this,s)}}function j(e){return function(...s){var t=s[0];return t.preventDefault(),e==null?void 0:e.apply(this,s)}}function ne(e=!1){const s=S,t=s.l.u;if(!t)return;let i=()=>Ai(s.s);if(e){let o=0,n={};const a=kt(()=>{let u=!1;const r=s.s;for(const d in r)r[d]!==n[d]&&(n[d]=r[d],u=!0);return u&&o++,o});i=()=>l(a)}t.b.length&&ai(()=>{Ft(s,i),Qe(t.b)}),dt(()=>{const o=Ye(()=>t.m.map(Rs));return()=>{for(const n of o)typeof n=="function"&&n()}}),t.a.length&&dt(()=>{Ft(s,i),Qe(t.a)})}function Ft(e,s){if(e.l.s)for(const t of e.l.s)l(t);s()}function he(e){S===null&&es(),We&&S.l!==null?$i(S).m.push(e):dt(()=>{const s=Ye(e);if(typeof s=="function")return s})}function Oi(e,s,{bubbles:t=!1,cancelable:i=!1}={}){return new CustomEvent(e,{detail:s,bubbles:t,cancelable:i})}function Bi(){const e=S;return e===null&&es(),(s,t,i)=>{var n;const o=(n=e.s.$$events)==null?void 0:n[s];if(o){const a=gt(o)?o.slice():[o],u=Oi(s,t,i);for(const r of a)r.call(e.x,u);return!u.defaultPrevented}return!0}}function $i(e){var s=e.l;return s.u??(s.u={a:[],b:[],m:[]})}const Hi="5";var Ht;typeof window<"u"&&((Ht=window.__svelte??(window.__svelte={})).v??(Ht.v=new Set)).add(Hi);Js();var Wi=Es(''),Ki=Es(''),Yi=R('
'),Zi=R(`
`);function Qi(e,s){J(s,!1);let t=B(window.location.hash.slice(1)||"accueil"),i=B(!1),o=B("");function n(){const E=window.location.hostname;let P="";E==="dev.geosector.fr"||E.includes("localhost")?P="dapp":E==="rec.geosector.fr"?P="rapp":P="app";const de=E.split(".");if(de.length>=2){const Ze=de.slice(Math.max(de.length-2,0)).join(".");return`https://${P}.${Ze}`}return`https://${P}.geosector.fr`}function a(E){q(t,E),window.history.pushState({},"",`/${E}`),window.dispatchEvent(new PopStateEvent("popstate")),r()}function u(E){E.stopPropagation(),q(i,!l(i))}function r(){q(i,!1)}typeof window<"u"&&window.addEventListener("popstate",()=>{q(t,window.location.pathname.slice(1)||"accueil")}),he(()=>{q(o,n());const E=P=>{const de=document.getElementById("mobile-menu"),Ze=document.getElementById("burger-button");l(i)&&de&&Ze&&!de.contains(P.target)&&!Ze.contains(P.target)&&r()};return setTimeout(()=>{document.addEventListener("click",E)},100),()=>{document.removeEventListener("click",E)}}),ne();var d=Zi(),f=c(d),p=c(f),v=c(p),h=c(v),b=c(h),w=m(v,2),k=c(w),g=c(k);{var _=E=>{var P=Wi();N(E,P)},A=E=>{var P=Ki();N(E,P)};te(g,E=>{l(i)?E(_):E(A,!1)})}var M=m(w,2),T=c(M),Z=c(T),U=c(Z),G=c(U),$=m(U,2),ge=c($),ze=m($,2),ye=c(ze),ee=m(T,2),ue=c(ee),ce=m(ue,2),Ge=m(ce,2),H=m(f,2),Pe=c(H),be=c(Pe),je=c(be),Q=c(je),ke=c(Q),V=m(Q,2),Lt=c(V),Ls=m(V,2),Mt=c(Ls),Ms=m(be,2),ot=c(Ms),nt=m(ot,2),Ts=m(nt,2),zs=m(H,2);{var Gs=E=>{var P=Yi();C("click",P,r),C("keydown",P,de=>de.key==="Escape"&&r()),N(E,P)};te(zs,E=>{l(i)&&E(Gs)})}ae(()=>{L(G,1,`text-[#002C66] hover:text-blue-500 transition-colors ${l(t)==="accueil"?"font-bold border-b-2 border-[#002C66]":""}`),L(ge,1,`text-[#002C66] hover:text-blue-500 transition-colors ${l(t)==="fonctionnalites"?"font-bold border-b-2 border-[#002C66]":""}`),L(ye,1,`text-[#002C66] hover:text-blue-500 transition-colors ${l(t)==="contact"?"font-bold border-b-2 border-[#002C66]":""}`),Ae(ue,"href",`${l(o)??""}/?action=login&type=user`),Ae(ce,"href",`${l(o)??""}/?action=login&type=admin`),Ae(Ge,"href",`${l(o)??""}/?action=register`),L(H,1,`xl:hidden fixed top-0 right-0 h-screen w-4/5 max-w-xs bg-white shadow-lg transform transition-transform duration-300 ease-in-out z-40 ${l(i)?"translate-x-0":"translate-x-full"}`),L(ke,1,`block text-lg text-[#002C66] hover:text-blue-500 transition-colors ${l(t)==="accueil"?"font-bold":""}`),L(Lt,1,`block text-lg text-[#002C66] hover:text-blue-500 transition-colors ${l(t)==="fonctionnalites"?"font-bold":""}`),L(Mt,1,`block text-lg text-[#002C66] hover:text-blue-500 transition-colors ${l(t)==="contact"?"font-bold":""}`),Ae(ot,"href",`${l(o)??""}/?action=login&type=user`),Ae(nt,"href",`${l(o)??""}/?action=login&type=admin`),Ae(Ts,"href",`${l(o)??""}/?action=register`)}),C("click",b,j(()=>a("accueil"))),C("click",w,Fi(u)),C("click",G,j(()=>a("accueil"))),C("click",ge,j(()=>a("fonctionnalites"))),C("click",ye,j(()=>a("contact"))),C("click",ue,()=>{sessionStorage.setItem("loginType","user")}),C("click",ce,()=>{sessionStorage.setItem("loginType","admin")}),C("click",ke,j(()=>a("accueil"))),C("click",Lt,j(()=>a("fonctionnalites"))),C("click",Mt,j(()=>a("contact"))),C("click",ot,()=>{sessionStorage.setItem("loginType","user")}),C("click",nt,()=>{sessionStorage.setItem("loginType","admin")}),N(e,d),X()}var Ji=R(``);function Xi(e,s){J(s,!1);function t(G){window.location.hash=G,window.scrollTo(0,0)}ne();var i=Ji(),o=c(i),n=c(o),a=m(c(n),2),u=m(c(a),2),r=c(u),d=m(c(r),2),f=m(r,2),p=m(c(f),2),v=m(f,2),h=m(c(v),2),b=m(v,4),w=m(c(b),2),k=m(b,2),g=m(c(k),2),_=m(k,2),A=m(c(_),2),M=m(n,2),T=c(M),Z=c(T),U=c(Z);ae(G=>it(U,`© ${G??""} Geosector. Tous droits réservés.`),[()=>new Date().getFullYear()],me),C("click",d,j(()=>t("accueil"))),C("click",p,j(()=>t("fonctionnalites"))),C("click",h,j(()=>t("contact"))),C("click",w,j(()=>t("mentions-legales"))),C("click",g,j(()=>t("politique-confidentialite"))),C("click",A,j(()=>t("conditions-utilisation"))),N(e,i),X()}var eo=R(`
`);function to(e,s){J(s,!1);const t=Bi();function i(){const f=new Date;f.setDate(f.getDate()+2),document.cookie=`geosector_cookies_accepted=true; expires=${f.toUTCString()}; path=/; SameSite=Lax`,t("consent",{accepted:!0})}function o(){const f=new Date;f.setDate(f.getDate()+2),document.cookie=`geosector_cookies_refused=true; expires=${f.toUTCString()}; path=/; SameSite=Lax`,t("consent",{accepted:!1})}ne();var n=eo(),a=c(n),u=m(c(a),4),r=c(u),d=m(r,2);C("click",r,o),C("click",d,i),N(e,n),X()}function He(e){const s=document.cookie.split("; ").find(t=>t.startsWith(`${e}=`));return s?s.split("=")[1]:null}function so(){return He("geosector_cookies_accepted")!==null||He("geosector_cookies_refused")!==null}function Ot(){if(He("geosector_cookies_accepted")==="true"){const e=window.location.hash.slice(1)||"accueil";console.log("Suivi anonyme activé - "+new Date().toISOString()),console.log("Page courante: "+e)}}function Bt(){He("geosector_cookies_refused")==="true"&&console.log("Suivi anonyme désactivé - "+new Date().toISOString())}const Ss="geosector_last_tracking";function $t(e){if(He("geosector_cookies_accepted")!=="true"){console.log("Suivi désactivé : cookies non acceptés");return}if(!io()){console.log("Suivi différé : déjà suivi dans les 2 derniers jours");return}localStorage.setItem(Ss,new Date().toISOString()),console.log(`Page consultée: ${e} - ${new Date().toISOString()}`)}function io(){const e=localStorage.getItem(Ss);if(!e)return!0;const s=new Date(e),i=Math.abs(new Date-s);return Math.ceil(i/(1e3*60*60*24))>=2}var oo=R(`

Gestion efficace de vos distributions de calendriers

Une application puissante et intuitive pour optimiser vos tournées et améliorer votre productivité.

Dashboard Geosector

Interface de gestion

Mobile App

Interface mobile

Pourquoi choisir Geosector ?

Optimisation des tournées

Grace au mode Terrain, Geosector aide le membre à traiter les adresses à finaliser proche de lui.

Simplicité d'utilisation

Interface intuitive conçue pour faciliter la gestion quotidienne de vos distributions.

Sécurité des données

Vos données sont protégées en conformité au RGPD et sauvegardées régulièrement.

Ce que nos clients disent

TP

Trystan PAPIN

Trésorier de l'amicale des SP du Malesherbois

"Bonjour, Je confirme l’utilisation de l’application Geosector pour l’amicale des SP de Malesherbes. Superbe application encore merci à vous !"

ML

Marie Leroy

Responsable opérations, LogiExpress

"L'interface intuitive de Geosector nous a permis de former rapidement nos équipes. La visualisation en temps réel des tournées est un atout majeur pour notre activité quotidienne."

`);function no(e,s){J(s,!1);let t=B(!1);he(()=>{q(t,!0)}),ne();var i=oo(),o=c(i),n=c(o),a=c(n),u=c(a),r=c(u);let d;var f=m(r,2);let p;var v=m(f,2);let h;var b=m(u,2);let w;ae((k,g,_,A)=>{d=L(r,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,d,k),p=L(f,1,"text-xl mb-8 transition-all duration-700 delay-300 text-[#002C66]",null,p,g),h=L(v,1,"transition-all duration-700 delay-500",null,h,_),w=L(b,1,"md:w-1/2 transition-all duration-700 delay-700 relative flex justify-center md:justify-end",null,w,A)},[()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-x-0":l(t),"opacity-100":l(t),"translate-x-10":!l(t),"opacity-0":!l(t)})],me),N(e,i),X()}var ro=R(`

Fonctionnalités

Découvrez les outils puissants qui font de Geosector la solution idéale pour la gestion de vos distributions.

Fonctionnalités principales

Cartographie avancée

Visualisez vos tournées sur des cartes interactives avec des données en temps réel sur le trafic et les conditions météorologiques.

  • Cartes détaillées avec points d'intérêt
  • Suivi GPS en temps réel
  • Alertes de trafic et d'incidents

Optimisation des itinéraires

Nos algorithmes avancés calculent les itinéraires les plus efficaces en tenant compte de multiples facteurs.

  • Réduction des coûts de carburant jusqu'à 30%
  • Prise en compte des contraintes horaires
  • Adaptation dynamique aux conditions réelles

Planification intelligente

Planifiez vos tournées à l'avance et adaptez-les facilement en fonction des imprévus.

  • Calendrier interactif avec vue mensuelle/hebdomadaire/quotidienne
  • Gestion des priorités et des urgences
  • Notifications automatiques pour les changements

Rapports et analyses

Obtenez des insights précieux sur vos opérations grâce à nos outils d'analyse avancés.

  • Tableaux de bord personnalisables
  • Exportation des données en plusieurs formats
  • Indicateurs de performance clés (KPIs)

Application mobile

Emportez Geosector partout avec vous

Notre application mobile offre toutes les fonctionnalités essentielles pour gérer vos distributions en déplacement.

Interface adaptée aux mobiles

Expérience utilisateur optimisée pour les écrans tactiles et la navigation mobile.

Mode hors ligne

Continuez à travailler même sans connexion internet, avec synchronisation automatique.

Notifications push

Restez informé des changements importants et des mises à jour en temps réel.

Télécharger sur l'App Store Télécharger sur Google Play
Capture d'écran de l'application mobile

Prêt à optimiser vos distributions ?

Rejoignez les milliers d'entreprises qui font confiance à Geosector pour améliorer leur efficacité opérationnelle.

Demander une démo
`);function lo(e,s){J(s,!1);let t=B(!1);he(()=>{q(t,!0)}),ne();var i=ro(),o=c(i),n=c(o),a=c(n),u=c(a);let r;var d=m(u,2);let f;ae((p,v)=>{r=L(u,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,r,p),f=L(d,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,f,v)},[()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)})],me),N(e,i),X()}var ao=R('

Message envoyé avec succès !

Nous vous répondrons dans les plus brefs délais.

'),uo=R('
'),co=R(`

Contactez-nous

Notre équipe est à votre disposition pour répondre à toutes vos questions et vous accompagner dans votre projet.

Nos coordonnées

Téléphone

+33 (0)1 23 45 67 89

Email

contact@geosector.fr

Horaires d'ouverture

Lundi - Vendredi: 9h00 - 18h00
Samedi - Dimanche: Fermé

Suivez-nous

Envoyez-nous un message

Questions fréquentes

Comment puis-je obtenir une démonstration de Geosector ?

Vous pouvez demander une démonstration en remplissant le formulaire de contact ci-dessus ou en nous appelant directement. Un de nos conseillers vous contactera pour organiser une session personnalisée.

Combien de temps dure la période d'essai ?

Nous proposons une période d'essai gratuite de 14 jours avec toutes les fonctionnalités disponibles. Aucune carte de crédit n'est requise pour commencer votre essai.

Proposez-vous des formations pour utiliser votre logiciel ?

Oui, nous proposons des sessions de formation complètes pour vous aider à tirer le meilleur parti de Geosector. Ces formations peuvent être réalisées en ligne ou dans vos locaux selon vos préférences.

Quels types de support technique proposez-vous ?

Nous offrons un support technique par email, téléphone et chat en direct pendant les heures de bureau. Nos clients avec des forfaits premium bénéficient d'un support 24/7.

`);function po(e,s){J(s,!1);let t=B(!1),i=B({nom:"",email:"",telephone:"",entreprise:"",message:"",newsletter:!1}),o=B(!1);function n(){q(o,!0),console.log("Formulaire soumis:",l(i))}he(()=>{q(t,!0)}),ne();var a=co(),u=c(a),r=c(u),d=c(r),f=c(d);let p;var v=m(f,2);let h;var b=m(u,2),w=c(b),k=c(w),g=c(k),_=c(g),A=m(c(_),2),M=m(c(A),2);{var T=U=>{var G=ao();N(U,G)},Z=U=>{var G=uo(),$=c(G),ge=c($),ze=m(c(ge),2),ye=m(ge,2),ee=m(c(ye),2),ue=m($,2),ce=c(ue),Ge=m(c(ce),2),H=m(ce,2),Pe=m(c(H),2),be=m(ue,2),je=m(c(be),2),Q=m(be,2),ke=c(Q);Ne(ze,()=>l(i).nom,V=>Ce(i,l(i).nom=V)),Ne(ee,()=>l(i).email,V=>Ce(i,l(i).email=V)),Ne(Ge,()=>l(i).telephone,V=>Ce(i,l(i).telephone=V)),Ne(Pe,()=>l(i).entreprise,V=>Ce(i,l(i).entreprise=V)),Ne(je,()=>l(i).message,V=>Ce(i,l(i).message=V)),Ri(ke,()=>l(i).newsletter,V=>Ce(i,l(i).newsletter=V)),C("submit",G,j(n)),N(U,G)};te(M,U=>{l(o)?U(T):U(Z,!1)})}ae((U,G)=>{p=L(f,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,p,U),h=L(v,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,h,G)},[()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)})],me),N(e,a),X()}var vo=R(`

Politique de confidentialité

Protection de vos données personnelles et respect de votre vie privée

Introduction

Cette politique de confidentialité s'applique à l'application Geosector, disponible sur le Web, iOS et Android, - ainsi qu'à tous les services associés (collectivement désignés par "Geosector", "nous", "notre" ou "nos").

Chez Geosector, nous accordons une grande importance à la protection de vos données personnelles. - Cette politique décrit quelles informations nous collectons, comment nous les utilisons, - et quels choix vous avez concernant ces données.

Cette politique de confidentialité doit être lue conjointement avec nos Conditions d'utilisation, qui régissent votre utilisation de notre application.

Quelles informations collectons-nous ?

1. Informations que vous nous fournissez

  • Informations de compte : Lors de l'inscription, nous collectons votre nom, prénom, adresse e-mail, et mot de passe.
  • Informations de profil : Vous pouvez nous fournir des informations supplémentaires comme votre fonction, l'organisation à laquelle vous appartenez, et votre photo de profil.
  • Contenu utilisateur : Les informations que vous créez, téléchargez ou partagez via notre application, notamment les secteurs géographiques, les passages, et les commentaires.
  • Communications : Lorsque vous nous contactez, nous conservons un historique de ces communications.

2. Informations collectées automatiquement

  • Données d'utilisation : Informations sur vos interactions avec notre application, comme les fonctionnalités utilisées, les pages visitées et le temps passé.
  • Informations sur l'appareil : Type d'appareil, système d'exploitation, version de l'application, langue, fuseau horaire et autres caractéristiques techniques.
  • Données de localisation : Avec votre consentement, nous collectons des données de géolocalisation précises pour vous permettre d'utiliser les fonctionnalités cartographiques et de secteurs.
  • Cookies et technologies similaires : Sur notre version web, nous utilisons des cookies et des technologies similaires pour améliorer votre expérience. Pour plus d'informations, consultez notre politique relative aux cookies.

Comment utilisons-nous vos informations ?

Nous utilisons vos informations pour les finalités suivantes :

  • Fournir, maintenir et améliorer notre application et ses fonctionnalités
  • Créer et gérer votre compte
  • Traiter vos transactions et paiements
  • Vous envoyer des informations techniques, des mises à jour, des alertes de sécurité et des messages administratifs
  • Répondre à vos commentaires et questions et vous fournir un support client
  • Communiquer avec vous à propos de produits, services, offres et événements
  • Surveiller et analyser les tendances, l'utilisation et les activités liées à notre application
  • Détecter, prévenir et résoudre les problèmes techniques et de sécurité
  • Se conformer aux obligations légales

Base légale du traitement (pour les utilisateurs de l'EEE et du Royaume-Uni)

Pour les utilisateurs de l'Espace économique européen (EEE) et du Royaume-Uni, nous traitons vos données personnelles sur les bases légales suivantes :

  • Exécution d'un contrat : Lorsque le traitement est nécessaire pour l'exécution d'un contrat auquel vous êtes partie ou pour prendre des mesures à votre demande avant de conclure un contrat.
  • Intérêts légitimes : Lorsque le traitement est nécessaire pour nos intérêts légitimes ou ceux d'un tiers, et que ces intérêts ne sont pas supplantés par vos intérêts ou droits fondamentaux.
  • Consentement : Lorsque vous avez donné votre consentement au traitement de vos données personnelles pour une ou plusieurs finalités spécifiques.
  • Obligation légale : Lorsque le traitement est nécessaire pour respecter une obligation légale à laquelle nous sommes soumis.

Comment partageons-nous vos informations ?

Nous pouvons partager vos informations personnelles avec les tiers suivants :

  • Prestataires de services : Nous travaillons avec des prestataires de services tiers qui fournissent des services tels que l'hébergement, l'analyse, le traitement des paiements et le support client.
  • Partenaires professionnels : Nous pouvons partager des informations avec nos partenaires commerciaux pour offrir certains produits, services ou promotions.
  • Conformité légale : Nous pouvons divulguer vos informations si nous estimons de bonne foi que cette divulgation est nécessaire pour se conformer à la loi, protéger nos droits ou assurer votre sécurité.
  • Transactions d'entreprise : En cas de fusion, acquisition, restructuration ou vente d'actifs, vos informations peuvent être transférées dans le cadre de cette transaction.

Nous ne vendons pas vos données personnelles à des tiers.

Transferts internationaux de données

Vos informations peuvent être transférées et traitées dans des pays autres que celui où vous résidez. - Ces pays peuvent avoir des lois sur la protection des données différentes de celles de votre pays.

Si nous transférons des données personnelles provenant de l'EEE, du Royaume-Uni ou de la Suisse vers des pays - n'offrant pas un niveau de protection adéquat selon les autorités compétentes, nous utilisons des - mécanismes de transfert légalement reconnus, tels que les clauses contractuelles types approuvées par la Commission européenne.

Vos droits et choix

Selon votre lieu de résidence, vous pouvez disposer de certains droits concernant vos données personnelles :

  • Accès et portabilité : Vous pouvez accéder à vos informations personnelles et en obtenir une copie dans un format structuré, couramment utilisé et lisible par machine.
  • Correction : Vous pouvez mettre à jour ou corriger vos informations personnelles si elles sont inexactes ou incomplètes.
  • Suppression : Vous pouvez demander la suppression de vos données personnelles dans certaines circonstances.
  • Restriction et opposition : Vous pouvez demander la restriction du traitement de vos données personnelles ou vous opposer à leur traitement dans certaines circonstances.
  • Consentement : Lorsque le traitement est basé sur votre consentement, vous pouvez retirer ce consentement à tout moment.
  • Réclamation : Vous avez le droit d'introduire une réclamation auprès d'une autorité de protection des données.

Pour exercer ces droits, contactez-nous à l'adresse indiquée dans la section "Nous contacter" ci-dessous. - Notez que ces droits peuvent être soumis à des limitations et exceptions prévues par la loi applicable.

Conservation des données

Nous conservons vos données personnelles aussi longtemps que nécessaire pour atteindre les finalités décrites dans cette politique, - sauf si une période de conservation plus longue est requise ou permise par la loi. - Les critères utilisés pour déterminer nos périodes de conservation comprennent :

  • La durée pendant laquelle nous entretenons une relation continue avec vous et vous fournissons l'application
  • Si nous avons une obligation légale à laquelle nous sommes soumis
  • Si la conservation est souhaitable compte tenu de notre position juridique (par exemple, concernant les délais de prescription applicables, les litiges ou les enquêtes réglementaires)

Sécurité des données

Nous mettons en œuvre des mesures de sécurité techniques et organisationnelles appropriées pour protéger vos données personnelles - contre la perte accidentelle, l'utilisation non autorisée, l'altération et la divulgation. - Ces mesures comprennent le chiffrement des données, les contrôles d'accès, les pare-feu et les audits de sécurité réguliers.

Cependant, aucun système de sécurité n'est impénétrable et nous ne pouvons garantir la sécurité absolue de vos informations. - Il est important que vous preniez des précautions pour protéger votre mot de passe et votre appareil.

Protection de la vie privée des enfants

Notre application n'est pas destinée aux personnes âgées de moins de 16 ans et nous ne collectons pas sciemment - des données personnelles auprès d'enfants de moins de 16 ans. Si vous êtes parent ou tuteur et que vous pensez - que votre enfant nous a fourni des informations personnelles, veuillez nous contacter.

Modifications de cette politique

Nous pouvons modifier cette politique de confidentialité de temps à autre. Si nous apportons des modifications importantes, - nous vous en informerons par e-mail ou par une notification dans notre application avant que les modifications - ne prennent effet. Nous vous encourageons à consulter régulièrement cette politique pour rester informé de - nos pratiques en matière de protection des données.

Nous contacter

Si vous avez des questions concernant cette politique de confidentialité ou nos pratiques en matière de protection des données, - veuillez nous contacter à l'adresse suivante :

Geosector
E-mail : privacy@geosector.fr
Adresse : [Adresse de l'entreprise]
Téléphone : +33 (0)1 23 45 67 89

Informations spécifiques aux plateformes

Application iOS (Apple App Store)

En utilisant notre application via l'App Store d'Apple, vous reconnaissez qu'Apple n'est pas responsable de nos pratiques - en matière de protection des données. Veuillez consulter la politique de confidentialité d'Apple pour plus d'informations - sur la façon dont Apple peut collecter et traiter vos données.

Application Android (Google Play)

En utilisant notre application via Google Play, vous reconnaissez que Google n'est pas responsable de nos pratiques - en matière de protection des données. Veuillez consulter la politique de confidentialité de Google pour plus d'informations - sur la façon dont Google peut collecter et traiter vos données.

Permissions des applications mobiles

Notre application peut demander certaines permissions sur votre appareil mobile, notamment :

  • Localisation : Pour les fonctionnalités basées sur la localisation, comme l'affichage des secteurs et la navigation
  • Stockage : Pour stocker des données localement sur votre appareil
  • Appareil photo : Pour scanner des codes QR ou prendre des photos
  • Notifications : Pour vous envoyer des alertes et des mises à jour importantes

Vous pouvez gérer ces permissions à tout moment dans les paramètres de votre appareil, mais notez que - la désactivation de certaines permissions peut limiter les fonctionnalités de l'application.

`);function fo(e,s){J(s,!1);let t=B(!1);function i(M){window.location.hash=M,window.scrollTo(0,0)}he(()=>{q(t,!0)}),ne();var o=vo(),n=c(o),a=c(n),u=c(a),r=c(u);let d;var f=m(r,2);let p;var v=m(n,2),h=c(v),b=c(h),w=c(b),k=c(w),g=c(k),_=m(k,8),A=m(c(_));ae((M,T,Z)=>{d=L(r,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,d,M),p=L(f,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,p,T),it(g,`Dernière mise à jour : ${Z??""}`)},[()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>new Date().toLocaleDateString("fr-FR",{year:"numeric",month:"long",day:"numeric"})],me),C("click",A,j(()=>i("conditions-utilisation"))),N(e,o),X()}var mo=R(`

Conditions d'utilisation

Règles et modalités d'utilisation de l'application Geosector

1. Préambule

Les présentes conditions générales d'utilisation (ci-après dénommées "CGU") régissent l'utilisation de l'application Geosector (ci-après dénommée l'"Application"), - accessible via le Web à l'adresse app.geosector.fr, ainsi que sur les plateformes iOS (Apple App Store) et Android (Google Play Store).

Geosector est une application dédiée à la gestion de secteurs géographiques et de passages, permettant à ses utilisateurs d'optimiser - leurs distributions et tournées. L'application est exploitée par [Nom de la société], dont le siège social est situé à [Adresse complète], - immatriculée au Registre du Commerce et des Sociétés de [Ville] sous le numéro [Numéro RCS].

En utilisant notre Application, vous acceptez de vous conformer aux présentes CGU. Si vous n'acceptez pas ces conditions, - veuillez ne pas utiliser l'Application.

2. Définitions

Dans les présentes CGU, les termes suivants ont la signification qui leur est attribuée ci-dessous :

  • "Application" désigne l'application Geosector, accessible via le Web, iOS et Android.
  • "Compte" désigne l'espace personnel de l'Utilisateur sur l'Application.
  • "Contenu" désigne toutes les informations et données (y compris les textes, images, vidéos, etc.) accessibles ou générées via l'Application.
  • "Fonctionnalités" désigne les services et outils proposés par l'Application.
  • "Utilisateur" désigne toute personne physique ou morale ayant accès à l'Application.
  • "Données Personnelles" désigne toute information se rapportant à une personne physique identifiée ou identifiable.

3. Inscription et compte utilisateur

3.1 Conditions d'inscription

Pour utiliser l'ensemble des Fonctionnalités de l'Application, l'Utilisateur doit créer un Compte en fournissant - les informations requises. L'Utilisateur s'engage à fournir des informations exactes, complètes et à jour. - Toute fausse déclaration peut entraîner la suspension ou la suppression du Compte.

3.2 Sécurité du compte

L'Utilisateur est responsable de la confidentialité de ses identifiants de connexion (nom d'utilisateur et mot de passe) - et s'engage à ne pas les communiquer à des tiers. Toute connexion effectuée en utilisant les identifiants de l'Utilisateur - sera présumée avoir été effectuée par celui-ci.

3.3 Suspension ou suppression de compte

Geosector se réserve le droit de suspendre ou de supprimer un Compte en cas de :

  • Non-respect des présentes CGU
  • Inactivité prolongée
  • Utilisation frauduleuse ou abusive de l'Application
  • Non-paiement des services payants
  • Demande de l'Utilisateur

4. Utilisation de l'Application

4.1 Licence d'utilisation

Sous réserve du respect des présentes CGU, Geosector accorde à l'Utilisateur une licence limitée, non exclusive, - non transférable et révocable pour accéder et utiliser l'Application à des fins professionnelles ou personnelles.

4.2 Restrictions d'utilisation

L'Utilisateur s'engage à ne pas :

  • Utiliser l'Application à des fins illégales ou interdites par les présentes CGU
  • Tenter de perturber le fonctionnement de l'Application ou d'accéder aux données d'autres Utilisateurs
  • Utiliser des robots, spiders, scrapers ou autres moyens automatisés pour accéder à l'Application
  • Contourner les mesures de sécurité de l'Application
  • Reproduire, copier, vendre, revendre ou exploiter toute partie de l'Application sans autorisation écrite préalable
  • Utiliser l'Application d'une manière qui pourrait endommager, désactiver, surcharger ou altérer les serveurs ou les réseaux

4.3 Contenu de l'Utilisateur

En publiant, téléchargeant, ou partageant du Contenu via l'Application, l'Utilisateur accorde à Geosector une licence mondiale, - non exclusive, transférable, libre de redevances pour utiliser, reproduire, modifier, adapter, publier, traduire et distribuer ce Contenu - dans le cadre de l'exploitation et de l'amélioration de l'Application.

L'Utilisateur garantit qu'il dispose des droits nécessaires sur le Contenu qu'il partage et que ce Contenu n'enfreint pas - les droits de tiers ni les lois applicables.

5. Services payants et abonnements

5.1 Offres et tarifs

Certaines Fonctionnalités de l'Application peuvent être soumises à paiement. Les offres et tarifs sont disponibles sur le site web - de Geosector ou directement dans l'Application. Geosector se réserve le droit de modifier ses offres et tarifs à tout moment, - moyennant un préavis raisonnable.

5.2 Paiement et facturation

Les paiements sont effectués par carte bancaire ou tout autre moyen proposé dans l'Application. Pour les abonnements, - le paiement est automatiquement renouvelé à la fin de chaque période, sauf résiliation par l'Utilisateur avant la date de renouvellement.

Une facture électronique est mise à disposition de l'Utilisateur pour chaque paiement effectué.

5.3 Politique de remboursement

Conformément à la législation applicable, l'Utilisateur bénéficie d'un droit de rétractation de 14 jours à compter de la souscription - à un service payant, sauf si l'exécution du service a commencé avec son accord avant la fin de ce délai.

Aucun remboursement ne sera accordé après l'expiration du délai de rétractation, sauf en cas de dysfonctionnement majeur de l'Application - imputable à Geosector.

6. Propriété intellectuelle

6.1 Droits de Geosector

L'Application, y compris son contenu, sa structure, ses fonctionnalités, son code source, ses interfaces, son design, - ses logos et ses marques, est la propriété exclusive de Geosector ou de ses concédants de licence. - Ces éléments sont protégés par les lois relatives à la propriété intellectuelle.

6.2 Droits des Utilisateurs

L'Utilisateur conserve tous les droits de propriété intellectuelle sur le Contenu qu'il crée et partage via l'Application, - sous réserve de la licence accordée à Geosector conformément à l'article 4.3.

6.3 Signalement d'une violation

Si vous pensez que votre contenu a été utilisé d'une manière qui constitue une violation de vos droits de propriété intellectuelle, - veuillez nous contacter à l'adresse suivante : [adresse email].

7. Confidentialité et données personnelles

La collecte et le traitement des Données Personnelles des Utilisateurs sont régis par notre Politique de Confidentialité, - disponible à l'adresse suivante : Politique de confidentialité.

8. Limitation de responsabilité

8.1 Disponibilité de l'Application

Geosector s'efforce de maintenir l'Application accessible 24 heures sur 24 et 7 jours sur 7. Cependant, l'accès peut être - temporairement suspendu, sans préavis, en raison de maintenance technique, de mise à jour ou pour toute autre raison.

Geosector ne peut être tenu responsable de tout dommage résultant de l'indisponibilité temporaire de l'Application.

8.2 Contenus et services tiers

L'Application peut contenir des liens vers des sites web ou services tiers. Geosector n'exerce aucun contrôle sur ces sites et services - et n'assume aucune responsabilité quant à leur contenu ou leurs pratiques.

8.3 Limitation générale de responsabilité

Dans toute la mesure permise par la loi applicable, Geosector ne pourra être tenu responsable de tout dommage indirect, - spécial, accessoire, consécutif ou punitif, y compris les pertes de profits, de revenus, de données ou d'opportunités commerciales, - résultant de l'utilisation ou de l'impossibilité d'utiliser l'Application.

La responsabilité totale de Geosector envers l'Utilisateur pour toute réclamation découlant des présentes CGU ne pourra excéder - le montant payé par l'Utilisateur à Geosector au cours des douze (12) mois précédant le fait générateur de la responsabilité.

9. Modifications des CGU

Geosector se réserve le droit de modifier les présentes CGU à tout moment. Les Utilisateurs seront informés des modifications - par le biais d'une notification dans l'Application ou par e-mail.

Les modifications prendront effet à la date indiquée dans la notification. En continuant à utiliser l'Application après cette date, - l'Utilisateur accepte les CGU modifiées.

Si l'Utilisateur n'accepte pas les modifications, il doit cesser d'utiliser l'Application et, le cas échéant, supprimer son Compte.

10. Résiliation

10.1 Résiliation par l'Utilisateur

L'Utilisateur peut, à tout moment, cesser d'utiliser l'Application et supprimer son Compte en suivant la procédure prévue à cet effet - dans les paramètres de l'Application.

10.2 Résiliation par Geosector

Geosector peut, à sa discrétion, suspendre ou résilier l'accès de l'Utilisateur à l'Application en cas de violation des présentes CGU, - sans préjudice de tout autre droit ou recours.

10.3 Conséquences de la résiliation

En cas de résiliation, l'Utilisateur perd l'accès à son Compte et à toutes les Fonctionnalités de l'Application. - Les sections des présentes CGU relatives à la propriété intellectuelle, à la limitation de responsabilité et au règlement des litiges - survivront à la résiliation.

11. Dispositions spécifiques aux applications mobiles

11.1 Application iOS (Apple App Store)

Si vous téléchargez l'Application via l'App Store d'Apple, vous reconnaissez et acceptez que :

  • Ces CGU sont conclues entre vous et Geosector, et non avec Apple
  • Apple n'a aucune obligation de fournir des services de maintenance ou d'assistance concernant l'Application
  • En cas de non-conformité de l'Application avec une garantie applicable, vous pouvez en informer Apple, qui pourra vous rembourser le prix d'achat
  • Apple n'est pas responsable du traitement des réclamations ou de la responsabilité liée à l'Application
  • En cas de réclamation d'un tiers selon laquelle l'Application enfreint ses droits de propriété intellectuelle, Apple n'est pas responsable de l'enquête, de la défense, du règlement et de la décharge de cette réclamation
  • Vous devez vous conformer aux conditions d'utilisation de l'App Store d'Apple lors de l'utilisation de l'Application

11.2 Application Android (Google Play)

Si vous téléchargez l'Application via Google Play, vous reconnaissez et acceptez que :

  • Ces CGU sont conclues entre vous et Geosector, et non avec Google
  • L'utilisation de l'Application doit respecter les conditions d'utilisation de Google Play
  • Google n'a aucune obligation de fournir des services de maintenance ou d'assistance concernant l'Application

12. Dispositions diverses

12.1 Droit applicable et juridiction compétente

Les présentes CGU sont régies par le droit français. Tout litige relatif à leur interprétation ou à leur exécution relève, - à défaut d'accord amiable, de la compétence exclusive des tribunaux français compétents.

12.2 Indépendance des clauses

Si une ou plusieurs dispositions des présentes CGU sont tenues pour non valides ou déclarées comme telles en application d'une loi, - d'un règlement ou à la suite d'une décision définitive d'une juridiction compétente, les autres stipulations garderont toute leur force - et leur portée.

12.3 Non-renonciation

Le fait pour Geosector de ne pas se prévaloir d'un manquement de l'Utilisateur à l'une quelconque des obligations visées dans les présentes CGU - ne saurait être interprété comme une renonciation à s'en prévaloir ultérieurement.

12.4 Communication

Toute notification ou communication dans le cadre des présentes CGU doit être adressée à Geosector par e-mail à l'adresse suivante : - [adresse email] ou par courrier postal à l'adresse suivante : [adresse postale].

13. Contact

Pour toute question concernant les présentes CGU, veuillez nous contacter à :

Geosector
E-mail : support@geosector.fr
Adresse : [Adresse de l'entreprise]
Téléphone : +33 (0)1 23 45 67 89

`);function ho(e,s){J(s,!1);let t=B(!1);function i(M){window.location.hash=M,window.scrollTo(0,0)}he(()=>{q(t,!0)}),ne();var o=mo(),n=c(o),a=c(n),u=c(a),r=c(u);let d;var f=m(r,2);let p;var v=m(n,2),h=c(v),b=c(h),w=c(b),k=c(w),g=c(k),_=m(k,84),A=m(c(_));ae((M,T,Z)=>{d=L(r,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,d,M),p=L(f,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,p,T),it(g,`Dernière mise à jour : ${Z??""}`)},[()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>new Date().toLocaleDateString("fr-FR",{year:"numeric",month:"long",day:"numeric"})],me),C("click",A,j(()=>i("politique-confidentialite"))),N(e,o),X()}var go=R(`

Mentions Légales

Informations juridiques relatives à notre site web et application mobile

1. Éditeur du site et de l'application

Le site web et l'application mobile Geosector sont édités par :

Geosector

SIRET : [Votre numéro SIRET]

Adresse : [Votre adresse]

Email : contact@geosector.fr

Téléphone : [Votre numéro de téléphone]

Directeur de la publication : [Nom du directeur de publication]

2. Hébergement

Le site web et l'application mobile Geosector sont hébergés par :

[Nom de l'hébergeur]

Adresse : [Adresse de l'hébergeur]

Site web : [Site web de l'hébergeur]

Email : [Email de l'hébergeur]

Téléphone : [Téléphone de l'hébergeur]

3. Propriété intellectuelle

L'ensemble du contenu du site web et de l'application mobile Geosector, incluant sans limitation les textes, graphiques, images, logos, icônes, photographies, est la propriété exclusive de Geosector et est protégé par les lois françaises et internationales relatives à la propriété intellectuelle.

Toute reproduction, représentation, modification, publication, transmission, adaptation, totale ou partielle des éléments du site ou de l'application, quel que soit le moyen ou le procédé utilisé, est interdite sans autorisation écrite préalable de Geosector.

Toute utilisation non autorisée des contenus, œuvres ou marques constitue une contrefaçon sanctionnée par le Code de la propriété intellectuelle.

4. Liens hypertextes

Le site web et l'application Geosector peuvent contenir des liens hypertextes vers d'autres sites internet ou applications.

Geosector n'a pas la possibilité de vérifier le contenu des sites ainsi visités, et n'assumera en conséquence aucune responsabilité de ce fait.

La création de liens hypertextes vers le site web ou l'application Geosector est soumise à l'accord préalable de l'éditeur.

5. Limitation de responsabilité

Geosector s'efforce d'assurer au mieux de ses possibilités l'exactitude et la mise à jour des informations diffusées sur son site web et son application mobile, dont elle se réserve le droit de corriger, à tout moment et sans préavis, le contenu.

Toutefois, Geosector ne peut garantir l'exactitude, la précision ou l'exhaustivité des informations mises à disposition sur son site web et son application.

En conséquence, Geosector décline toute responsabilité :

  • Pour toute imprécision, inexactitude ou omission portant sur des informations disponibles sur le site web ou l'application ;
  • Pour tous dommages résultant d'une intrusion frauduleuse d'un tiers ayant entraîné une modification des informations ou éléments mis à disposition sur le site web ou l'application ;
  • Et plus généralement, pour tous dommages, directs ou indirects, qu'elles qu'en soient les causes, origines, natures ou conséquences, provoqués en raison de l'accès de quiconque au site web ou à l'application ou de l'impossibilité d'y accéder, ainsi que l'utilisation du site web ou de l'application et/ou du crédit accordé à une quelconque information provenant directement ou indirectement de ces derniers.

6. Loi applicable et juridiction

Les présentes mentions légales sont régies par la loi française. En cas de litige, les tribunaux français seront seuls compétents.

Pour toute question relative à l'application des présentes mentions légales, vous pouvez nous contacter à l'adresse email : contact@geosector.fr

7. Modifications

Geosector se réserve le droit de modifier les présentes mentions légales à tout moment. L'utilisateur est invité à les consulter régulièrement.

`);function bo(e,s){J(s,!1);let t=B(!1);he(()=>{q(t,!0)}),ne();var i=go(),o=c(i),n=c(o),a=c(n),u=c(a);let r;var d=m(u,2);let f;var p=m(o,2),v=c(p),h=c(v),b=m(c(h),12),w=m(c(b),2),k=m(c(w),2),g=c(k);ae((_,A,M)=>{r=L(u,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,r,_),f=L(d,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,f,A),it(g,`Dernière mise à jour : ${M??""}`)},[()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>new Date().toLocaleDateString("fr-FR",{year:"numeric",month:"long",day:"numeric"})],me),N(e,i),X()}var xo=R(`

Page non trouvée

La page que vous recherchez n'existe pas.

Retour à l'accueil
`),wo=R('
',1);function _o(e,s){J(s,!1);let t=B("accueil"),i=B(!1);function o(){const g=window.location.pathname.slice(1)||"accueil";q(t,g),$t(l(t))}function n(g){g.detail.accepted?Ot():Bt(),q(i,!1)}he(async()=>(o(),document.addEventListener("click",g=>{const _=g.target.closest("a");if(!_||!_.href.startsWith(window.location.origin)||_.target||_.hasAttribute("download")||_.getAttribute("rel")==="external")return;g.preventDefault();const M=new URL(_.href).pathname;window.history.pushState({},"",M);const T=M.slice(1)||"accueil";q(t,T),$t(l(t))}),window.addEventListener("popstate",o),await Ci(),so()?(Ot(),Bt()):q(i,!0),()=>{window.removeEventListener("popstate",o)})),ne();var a=wo(),u=m(ri(a),2);let r;var d=c(u);Qi(d,{});var f=m(d,2),p=c(f);{var v=g=>{no(g,{})},h=(g,_)=>{{var A=T=>{lo(T,{})},M=(T,Z)=>{{var U=$=>{po($,{})},G=($,ge)=>{{var ze=ee=>{fo(ee,{})},ye=(ee,ue)=>{{var ce=H=>{ho(H,{})},Ge=(H,Pe)=>{{var be=Q=>{bo(Q,{})},je=Q=>{var ke=xo();N(Q,ke)};te(H,Q=>{l(t)==="mentions-legales"?Q(be):Q(je,!1)},Pe)}};te(ee,H=>{l(t)==="conditions-utilisation"?H(ce):H(Ge,!1)},ue)}};te($,ee=>{l(t)==="politique-confidentialite"?ee(ze):ee(ye,!1)},ge)}};te(T,$=>{l(t)==="contact"?$(U):$(G,!1)},Z)}};te(g,T=>{l(t)==="fonctionnalites"?T(A):T(M,!1)},_)}};te(p,g=>{l(t)==="accueil"?g(v):g(h,!1)})}var b=m(f,2);Xi(b,{});var w=m(u,2);{var k=g=>{to(g,{$$events:{consent:n}})};te(w,g=>{l(i)&&g(k)})}ae(g=>r=L(u,1,"flex flex-col min-h-screen relative",null,r,g),[()=>({"blur-effect":l(i)})],me),N(e,a),X()}Gi(_o,{target:document.getElementById("app")}); diff --git a/web/deploy/assets/index-WF7yUNnB.css b/web/deploy/assets/index-WF7yUNnB.css deleted file mode 100755 index e22b32d2..00000000 --- a/web/deploy/assets/index-WF7yUNnB.css +++ /dev/null @@ -1 +0,0 @@ -*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Figtree,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.-bottom-4{bottom:-1rem}.-right-4{right:-1rem}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[-10\]{z-index:-10}.z-\[-20\]{z-index:-20}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-\[300px\]{height:300px}.h-\[400px\]{height:400px}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-4{width:1rem}.w-4\/5{width:80%}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-\[150px\]{width:150px}.w-full{width:100%}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-xs{max-width:20rem}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-10{--tw-translate-x: 2.5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-10{--tw-translate-y: 2.5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-12{gap:3rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1.5rem * var(--tw-space-x-reverse));margin-left:calc(1.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-\[\#002C66\]{--tw-border-opacity: 1;border-color:rgb(0 44 102 / var(--tw-border-opacity, 1))}.border-\[\#4CAF50\]{--tw-border-opacity: 1;border-color:rgb(76 175 80 / var(--tw-border-opacity, 1))}.border-blue-300{--tw-border-opacity: 1;border-color:rgb(147 197 253 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-green-400{--tw-border-opacity: 1;border-color:rgb(74 222 128 / var(--tw-border-opacity, 1))}.border-red-600{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity, 1))}.bg-\[\#E3170A\]{--tw-bg-opacity: 1;background-color:rgb(227 23 10 / var(--tw-bg-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-900{--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-opacity-50{--tw-bg-opacity: .5}.bg-opacity-70{--tw-bg-opacity: .7}.bg-opacity-90{--tw-bg-opacity: .9}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-blue-900{--tw-gradient-from: #1e3a8a var(--tw-gradient-from-position);--tw-gradient-to: rgb(30 58 138 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-white{--tw-gradient-to: #fff var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pl-6{padding-left:1.5rem}.pt-20{padding-top:5rem}.pt-4{padding-top:1rem}.pt-8{padding-top:2rem}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.italic{font-style:italic}.not-italic{font-style:normal}.text-\[\#002C66\]{--tw-text-opacity: 1;color:rgb(0 44 102 / var(--tw-text-opacity, 1))}.text-\[\#4CAF50\]{--tw-text-opacity: 1;color:rgb(76 175 80 / var(--tw-text-opacity, 1))}.text-blue-200{--tw-text-opacity: 1;color:rgb(191 219 254 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-blue-500\/20{--tw-shadow-color: rgb(59 130 246 / .2);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.delay-300{transition-delay:.3s}.delay-500{transition-delay:.5s}.delay-700{transition-delay:.7s}.duration-300{transition-duration:.3s}.duration-700{transition-duration:.7s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}@font-face{font-family:Figtree;src:url(/fonts/Figtree-VariableFont_wght.ttf) format("truetype");font-weight:100 900;font-style:normal;font-display:swap}:root{font-family:Figtree,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;line-height:1.5;font-weight:400;color:#333;background-color:#fff;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}html,body{height:100%;margin:0;padding:0;scroll-behavior:smooth}body{min-width:320px;min-height:100vh}#app{display:flex;flex-direction:column;min-height:100vh}.aspect-w-16{position:relative;padding-bottom:calc(var(--tw-aspect-h) / var(--tw-aspect-w) * 100%);--tw-aspect-w: 16}.aspect-h-9{--tw-aspect-h: 9}.aspect-w-16>*{position:absolute;height:100%;width:100%;top:0;right:0;bottom:0;left:0}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.animate-fadeIn{animation:fadeIn .5s ease-in-out}.container{width:100%;max-width:1280px;margin-left:auto;margin-right:auto}*:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}@media (max-width: 640px){h1{font-size:2rem!important}h2{font-size:1.5rem!important}h3{font-size:1.25rem!important}}.blur-effect{filter:blur(4px);pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}@keyframes slideUp{0%{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}.cookie-modal{animation:slideUp .3s ease-out forwards}.hover\:scale-105:hover{--tw-scale-x: 1.05;--tw-scale-y: 1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-\[\#4CAF50\]:hover{--tw-bg-opacity: 1;background-color:rgb(76 175 80 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-200:hover{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-300:hover{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:text-blue-300:hover{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.hover\:text-blue-500:hover{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}@media (min-width: 640px){.sm\:flex-row{flex-direction:row}}@media (min-width: 768px){.md\:mb-0{margin-bottom:0}.md\:mr-6{margin-right:1.5rem}.md\:h-\[400px\]{height:400px}.md\:h-\[500px\]{height:500px}.md\:w-1\/2{width:50%}.md\:w-\[200px\]{width:200px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:justify-end{justify-content:flex-end}.md\:pr-8{padding-right:2rem}.md\:text-5xl{font-size:3rem;line-height:1}}@media (min-width: 1280px){.xl\:flex{display:flex}.xl\:hidden{display:none}} diff --git a/web/deploy/favicon-16.png b/web/deploy/favicon-16.png deleted file mode 100755 index f9441458..00000000 Binary files a/web/deploy/favicon-16.png and /dev/null differ diff --git a/web/deploy/favicon-32.png b/web/deploy/favicon-32.png deleted file mode 100755 index f5dffe2c..00000000 Binary files a/web/deploy/favicon-32.png and /dev/null differ diff --git a/web/deploy/favicon-64.png b/web/deploy/favicon-64.png deleted file mode 100755 index 53b6ae36..00000000 Binary files a/web/deploy/favicon-64.png and /dev/null differ diff --git a/web/deploy/favicon.png b/web/deploy/favicon.png deleted file mode 100755 index f5dffe2c..00000000 Binary files a/web/deploy/favicon.png and /dev/null differ diff --git a/web/deploy/fonts/Figtree-VariableFont_wght.ttf b/web/deploy/fonts/Figtree-VariableFont_wght.ttf deleted file mode 100755 index 06f9fe57..00000000 Binary files a/web/deploy/fonts/Figtree-VariableFont_wght.ttf and /dev/null differ diff --git a/web/deploy/fonts/Kallisto-Bold.otf b/web/deploy/fonts/Kallisto-Bold.otf deleted file mode 100755 index fb7595a9..00000000 Binary files a/web/deploy/fonts/Kallisto-Bold.otf and /dev/null differ diff --git a/web/deploy/fonts/Kallisto-Medium.otf b/web/deploy/fonts/Kallisto-Medium.otf deleted file mode 100755 index b0f8ad77..00000000 Binary files a/web/deploy/fonts/Kallisto-Medium.otf and /dev/null differ diff --git a/web/deploy/fonts/Kallisto-Thin.otf b/web/deploy/fonts/Kallisto-Thin.otf deleted file mode 100755 index a94f0934..00000000 Binary files a/web/deploy/fonts/Kallisto-Thin.otf and /dev/null differ diff --git a/web/deploy/icons/Icon-152.png b/web/deploy/icons/Icon-152.png deleted file mode 100755 index 4043aa2d..00000000 Binary files a/web/deploy/icons/Icon-152.png and /dev/null differ diff --git a/web/deploy/icons/Icon-167.png b/web/deploy/icons/Icon-167.png deleted file mode 100755 index 1fcc514a..00000000 Binary files a/web/deploy/icons/Icon-167.png and /dev/null differ diff --git a/web/deploy/icons/Icon-180.png b/web/deploy/icons/Icon-180.png deleted file mode 100755 index d2b40c1e..00000000 Binary files a/web/deploy/icons/Icon-180.png and /dev/null differ diff --git a/web/deploy/icons/Icon-192.png b/web/deploy/icons/Icon-192.png deleted file mode 100755 index 34447be7..00000000 Binary files a/web/deploy/icons/Icon-192.png and /dev/null differ diff --git a/web/deploy/icons/Icon-512.png b/web/deploy/icons/Icon-512.png deleted file mode 100755 index 058f9806..00000000 Binary files a/web/deploy/icons/Icon-512.png and /dev/null differ diff --git a/web/deploy/icons/Icon-maskable-192.png b/web/deploy/icons/Icon-maskable-192.png deleted file mode 100755 index 34447be7..00000000 Binary files a/web/deploy/icons/Icon-maskable-192.png and /dev/null differ diff --git a/web/deploy/icons/Icon-maskable-512.png b/web/deploy/icons/Icon-maskable-512.png deleted file mode 100755 index 058f9806..00000000 Binary files a/web/deploy/icons/Icon-maskable-512.png and /dev/null differ diff --git a/web/deploy/images/Logo-geosector-horizontal.svg b/web/deploy/images/Logo-geosector-horizontal.svg deleted file mode 100755 index 53d598ca..00000000 --- a/web/deploy/images/Logo-geosector-horizontal.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/deploy/images/Logo-geosector-vertical.svg b/web/deploy/images/Logo-geosector-vertical.svg deleted file mode 100755 index f9aca059..00000000 --- a/web/deploy/images/Logo-geosector-vertical.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/deploy/images/app-screenshot.png b/web/deploy/images/app-screenshot.png deleted file mode 100755 index 03802abb..00000000 --- a/web/deploy/images/app-screenshot.png +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/deploy/images/geosector-icon.png b/web/deploy/images/geosector-icon.png deleted file mode 100755 index 532e85c9..00000000 Binary files a/web/deploy/images/geosector-icon.png and /dev/null differ diff --git a/web/deploy/images/geosector-logo.png b/web/deploy/images/geosector-logo.png deleted file mode 100755 index fdf990ca..00000000 Binary files a/web/deploy/images/geosector-logo.png and /dev/null differ diff --git a/web/deploy/images/geosector-logo.svg b/web/deploy/images/geosector-logo.svg deleted file mode 100755 index e69de29b..00000000 diff --git a/web/deploy/images/icon-geosector.svg b/web/deploy/images/icon-geosector.svg deleted file mode 100755 index 1fbeeabb..00000000 --- a/web/deploy/images/icon-geosector.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/deploy/index.html b/web/deploy/index.html deleted file mode 100755 index 992b88f7..00000000 --- a/web/deploy/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - Geosector - Gestion efficace de vos distributions - - - - -
- - diff --git a/web/deploy/vite.svg b/web/deploy/vite.svg deleted file mode 100755 index e7b8dfb1..00000000 --- a/web/deploy/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/dist/.htaccess b/web/dist/.htaccess deleted file mode 100644 index 02e9d601..00000000 --- a/web/dist/.htaccess +++ /dev/null @@ -1,12 +0,0 @@ -# Configuration pour le mode histoire (HTML5 History API) - - RewriteEngine On - RewriteBase / - - # Si le fichier ou répertoire demandé existe, servir directement - RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d - - # Sinon, rediriger vers index.html pour permettre au routeur client de gérer - RewriteRule . /index.html [L] - diff --git a/web/dist/assets/index-CMFopX_9.js b/web/dist/assets/index-CMFopX_9.js deleted file mode 100644 index c800c405..00000000 --- a/web/dist/assets/index-CMFopX_9.js +++ /dev/null @@ -1,65 +0,0 @@ -var Ps=Object.defineProperty;var js=(e,s,t)=>s in e?Ps(e,s,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[s]=t;var rt=(e,s,t)=>js(e,typeof s!="symbol"?s+"":s,t);(function(){const s=document.createElement("link").relList;if(s&&s.supports&&s.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))i(o);new MutationObserver(o=>{for(const n of o)if(n.type==="childList")for(const a of n.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&i(a)}).observe(document,{childList:!0,subtree:!0});function t(o){const n={};return o.integrity&&(n.integrity=o.integrity),o.referrerPolicy&&(n.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?n.credentials="include":o.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function i(o){if(o.ep)return;o.ep=!0;const n=t(o);fetch(o.href,n)}})();const Tt=!1;var gt=Array.isArray,Ns=Array.prototype.indexOf,Us=Array.from,Ds=Object.defineProperty,Ve=Object.getOwnPropertyDescriptor,Wt=Object.getOwnPropertyDescriptors,Vs=Object.prototype,Is=Array.prototype,bt=Object.getPrototypeOf,zt=Object.isExtensible;function Rs(e){return e()}function Qe(e){for(var s=0;s{i.d=!0})}function X(e){const s=S;if(s!==null){const a=s.e;if(a!==null){var t=y,i=x;s.e=null;try{for(var o=0;o{var r=x;ie(n);var d=u();return ie(r),d};return i&&t.set("length",pe(e.length)),new Proxy(e,{defineProperty(u,r,d){(!("value"in d)||d.configurable===!1||d.enumerable===!1||d.writable===!1)&&Ks();var f=t.get(r);return f===void 0?f=a(()=>{var p=pe(d.value);return t.set(r,p),p}):q(f,d.value,!0),!0},deleteProperty(u,r){var d=t.get(r);if(d===void 0){if(r in u){const v=a(()=>pe(I));t.set(r,v),lt(o)}}else{if(i&&typeof r=="string"){var f=t.get("length"),p=Number(r);Number.isInteger(p)&&p{var b=Ue(p?u[r]:I),w=pe(b);return w}),t.set(r,f)),f!==void 0){var v=l(f);return v===I?void 0:v}return Reflect.get(u,r,d)},getOwnPropertyDescriptor(u,r){var d=Reflect.getOwnPropertyDescriptor(u,r);if(d&&"value"in d){var f=t.get(r);f&&(d.value=l(f))}else if(d===void 0){var p=t.get(r),v=p==null?void 0:p.v;if(p!==void 0&&v!==I)return{enumerable:!0,configurable:!0,value:v,writable:!0}}return d},has(u,r){var v;if(r===Ie)return!0;var d=t.get(r),f=d!==void 0&&d.v!==I||Reflect.has(u,r);if(d!==void 0||y!==null&&(!f||(v=Ve(u,r))!=null&&v.writable)){d===void 0&&(d=a(()=>{var h=f?Ue(u[r]):I,b=pe(h);return b}),t.set(r,d));var p=l(d);if(p===I)return!1}return f},set(u,r,d,f){var A;var p=t.get(r),v=r in u;if(i&&r==="length")for(var h=d;hpe(I)),t.set(h+"",b))}if(p===void 0)(!v||(A=Ve(u,r))!=null&&A.writable)&&(p=a(()=>pe(void 0)),q(p,Ue(d)),t.set(r,p));else{v=p.v!==I;var w=a(()=>Ue(d));q(p,w)}var k=Reflect.getOwnPropertyDescriptor(u,r);if(k!=null&&k.set&&k.set.call(f,d),!v){if(i&&typeof r=="string"){var g=t.get("length"),_=Number(r);Number.isInteger(_)&&_>=g.v&&q(g,_+1)}lt(o)}return!0},ownKeys(u){l(o);var r=Reflect.ownKeys(u).filter(p=>{var v=t.get(p);return v===void 0||v.v!==I});for(var[d,f]of t)f.v!==I&&!(d in u)&&r.push(d);return r},setPrototypeOf(){Ys()}})}function lt(e,s=1){q(e,e.v+s)}function kt(e){var s=K|re,t=x!==null&&(x.f&K)!==0?x:null;return y===null||t!==null&&(t.f&O)!==0?s|=O:y.f|=Zt,{ctx:S,deps:null,effects:null,equals:Jt,f:s,fn:e,reactions:null,rv:0,v:null,wv:0,parent:t??y,ac:null}}function me(e){const s=kt(e);return s.equals=Xt,s}function ts(e){var s=e.effects;if(s!==null){e.effects=null;for(var t=0;tl(e))),s}function q(e,s,t=!1){x!==null&&(!se||(x.f&Gt)!==0)&&Ke()&&(x.f&(K|xt|Gt))!==0&&!(z!=null&&z[1].includes(e)&&z[0]===x)&&Zs();let i=t?Ue(s):s;return ii(e,i)}function ii(e,s){if(!e.equals(s)){var t=e.v;Te?Fe.set(e,s):Fe.set(e,t),e.v=s,(e.f&K)!==0&&((e.f&re)!==0&&ss(e),oe(e,(e.f&O)===0?Y:_e)),e.wv=ws(),os(e,re),Ke()&&y!==null&&(y.f&Y)!==0&&(y.f&(le|Le))===0&&(W===null?bi([e]):W.push(e))}return s}function os(e,s){var t=e.reactions;if(t!==null)for(var i=Ke(),o=t.length,n=0;nnew Promise(i=>{t.outro?vt(s,()=>{we(s),i(void 0)}):(we(s),i(void 0))})}function cs(e){return Me(Kt,e,!1)}function At(e){return Me(tt,e,!0)}function ae(e,s=[],t=kt){const i=s.map(t);return ds(()=>e(...i.map(l)))}function ds(e,s=0){var t=Me(tt|xt|s,e,!0);return t}function pt(e,s=!0){return Me(tt|le,e,!0,s)}function ps(e){var s=e.teardown;if(s!==null){const t=Te,i=x;Ut(!0),ie(null);try{s.call(null)}finally{Ut(t),ie(i)}}}function vs(e,s=!1){var o;var t=e.first;for(e.first=e.last=null;t!==null;){(o=t.ac)==null||o.abort(Qt);var i=t.next;(t.f&Le)!==0?t.parent=null:we(t,s),t=i}}function ci(e){for(var s=e.first;s!==null;){var t=s.next;(s.f&le)===0&&we(s),s=t}}function we(e,s=!0){var t=!1;(s||(e.f&Fs)!==0)&&e.nodes_start!==null&&e.nodes_end!==null&&(di(e.nodes_start,e.nodes_end),t=!0),vs(e,s&&!t),et(e,0),oe(e,_t);var i=e.transitions;if(i!==null)for(const n of i)n.stop();ps(e);var o=e.parent;o!==null&&o.first!==null&&fs(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes_start=e.nodes_end=e.ac=null}function di(e,s){for(;e!==null;){var t=e===s?null:qt(e);e.remove(),e=t}}function fs(e){var s=e.parent,t=e.prev,i=e.next;t!==null&&(t.next=i),i!==null&&(i.prev=t),s!==null&&(s.first===e&&(s.first=i),s.last===e&&(s.last=t))}function vt(e,s){var t=[];ms(e,t,!0),pi(t,()=>{we(e),s&&s()})}function pi(e,s){var t=e.length;if(t>0){var i=()=>--t||s();for(var o of e)o.out(i)}else s()}function ms(e,s,t){if((e.f&Ee)===0){if(e.f^=Ee,e.transitions!==null)for(const a of e.transitions)(a.is_global||t)&&s.push(a);for(var i=e.first;i!==null;){var o=i.next,n=(i.f&yt)!==0||(i.f&le)!==0;ms(i,s,n?t:!1),i=o}}}function Nt(e){hs(e,!0)}function hs(e,s){if((e.f&Ee)!==0){e.f^=Ee;for(var t=e.first;t!==null;){var i=t.next,o=(t.f&yt)!==0||(t.f&le)!==0;hs(t,o?s:!1),t=i}if(e.transitions!==null)for(const n of e.transitions)(n.is_global||s)&&n.in()}}let Oe=[],ft=[];function gs(){var e=Oe;Oe=[],Qe(e)}function vi(){var e=ft;ft=[],Qe(e)}function fi(e){Oe.length===0&&queueMicrotask(gs),Oe.push(e)}function mi(){Oe.length>0&&gs(),ft.length>0&&vi()}function hi(e){var s=y;if((s.f&Yt)===0){if((s.f&wt)===0)throw e;s.fn(e)}else bs(e,s)}function bs(e,s){for(;s!==null;){if((s.f&wt)!==0)try{s.b.error(e);return}catch{}s=s.parent}throw e}let Be=!1,$e=null,xe=!1,Te=!1;function Ut(e){Te=e}let Re=[];let x=null,se=!1;function ie(e){x=e}let y=null;function fe(e){y=e}let z=null;function gi(e){x!==null&&x.f&ct&&(z===null?z=[x,[e]]:z[1].push(e))}let D=null,F=0,W=null;function bi(e){W=e}let xs=1,Xe=0,ve=!1;function ws(){return++xs}function st(e){var p;var s=e.f;if((s&re)!==0)return!0;if((s&_e)!==0){var t=e.deps,i=(s&O)!==0;if(t!==null){var o,n,a=(s&Je)!==0,u=i&&y!==null&&!ve,r=t.length;if(a||u){var d=e,f=d.parent;for(o=0;oe.wv)return!0}(!i||y!==null&&!ve)&&oe(e,Y)}return!1}function _s(e,s,t=!0){var i=e.reactions;if(i!==null)for(var o=0;o0)for(p.length=F+D.length,v=0;v0;){s++>1e3&&wi();var t=Re,i=t.length;Re=[];for(var o=0;o{Promise.resolve().then(()=>{var s;if(!e.defaultPrevented)for(const t of e.target.elements)(s=t.__on_r)==null||s.call(t)})},{capture:!0}))}function Cs(e){var s=x,t=y;ie(null),fe(null);try{return e()}finally{ie(s),fe(t)}}function qs(e,s,t,i=t){e.addEventListener(s,()=>Cs(t));const o=e.__on_r;o?e.__on_r=()=>{o(),i(!0)}:e.__on_r=()=>i(!0),Li()}const Mi=new Set,Vt=new Set;function Ti(e,s,t,i={}){function o(n){if(i.capture||De.call(s,n),!n.cancelBubble)return Cs(()=>t==null?void 0:t.call(this,n))}return e.startsWith("pointer")||e.startsWith("touch")||e==="wheel"?fi(()=>{s.addEventListener(e,o,i)}):s.addEventListener(e,o,i),o}function C(e,s,t,i,o){var n={capture:i,passive:o},a=Ti(e,s,t,n);(s===document.body||s===window||s===document||s instanceof HTMLMediaElement)&&us(()=>{s.removeEventListener(e,a,n)})}function De(e){var _;var s=this,t=s.ownerDocument,i=e.type,o=((_=e.composedPath)==null?void 0:_.call(e))||[],n=o[0]||e.target,a=0,u=e.__root;if(u){var r=o.indexOf(u);if(r!==-1&&(s===document||s===window)){e.__root=s;return}var d=o.indexOf(s);if(d===-1)return;r<=d&&(a=r)}if(n=o[a]||e.target,n!==s){Ds(e,"currentTarget",{configurable:!0,get(){return n||t}});var f=x,p=y;ie(null),fe(null);try{for(var v,h=[];n!==null;){var b=n.assignedSlot||n.parentNode||n.host||null;try{var w=n["__"+i];if(w!=null&&(!n.disabled||e.target===n))if(gt(w)){var[k,...g]=w;k.apply(n,[e,...g])}else w.call(n,e)}catch(A){v?h.push(A):v=A}if(e.cancelBubble||b===s||b===null)break;n=b}if(v){for(let A of h)queueMicrotask(()=>{throw A});throw v}}finally{e.__root=s,delete e.currentTarget,ie(f),fe(p)}}}function As(e){var s=document.createElement("template");return s.innerHTML=e.replaceAll("",""),s.content}function ht(e,s){var t=y;t.nodes_start===null&&(t.nodes_start=e,t.nodes_end=s)}function R(e,s){var t=(s&Xs)!==0,i=(s&ei)!==0,o,n=!e.startsWith("");return()=>{o===void 0&&(o=As(n?e:""+e),t||(o=Se(o)));var a=i||ns?document.importNode(o,!0):o.cloneNode(!0);if(t){var u=Se(a),r=a.lastChild;ht(u,r)}else ht(a,a);return a}}function zi(e,s,t="svg"){var i=!e.startsWith(""),o=`<${t}>${i?e:""+e}`,n;return()=>{if(!n){var a=As(o),u=Se(a);n=Se(u)}var r=n.cloneNode(!0);return ht(r,r),r}}function Es(e,s){return zi(e,s,"svg")}function N(e,s){e!==null&&e.before(s)}function it(e,s){var t=s==null?"":typeof s=="object"?s+"":s;t!==(e.__t??(e.__t=e.nodeValue))&&(e.__t=t,e.nodeValue=t+"")}function Gi(e,s){return Pi(e,s)}const qe=new Map;function Pi(e,{target:s,anchor:t,props:i={},events:o,context:n,intro:a=!0}){oi();var u=new Set,r=p=>{for(var v=0;v{var p=t??s.appendChild(ni());return pt(()=>{if(n){J({});var v=S;v.c=n}o&&(i.$$events=o),d=e(p,i)||{},n&&X()}),()=>{var b;for(var v of u){s.removeEventListener(v,De);var h=qe.get(v);--h===0?(document.removeEventListener(v,De),qe.delete(v)):qe.set(v,h)}Vt.delete(r),p!==t&&((b=p.parentNode)==null||b.removeChild(p))}});return ji.set(d,f),d}let ji=new WeakMap;function te(e,s,[t,i]=[0,0]){var o=e,n=null,a=null,u=I,r=t>0?yt:0,d=!1;const f=(v,h=!0)=>{d=!0,p(h,v)},p=(v,h)=>{u!==(u=v)&&(u?(n?Nt(n):h&&(n=pt(()=>h(o))),a&&vt(a,()=>{a=null})):(a?Nt(a):h&&(a=pt(()=>h(o,[t+1,i]))),n&&vt(n,()=>{n=null})))};ds(()=>{d=!1,s(f),d||p(null,null)},r)}const It=[...` -\r\f \v\uFEFF`];function Ni(e,s,t){var i=e==null?"":""+e;if(s&&(i=i?i+" "+s:s),t){for(var o in t)if(t[o])i=i?i+" "+o:o;else if(i.length)for(var n=o.length,a=0;(a=i.indexOf(o,a))>=0;){var u=a+n;(a===0||It.includes(i[a-1]))&&(u===i.length||It.includes(i[u]))?i=(a===0?"":i.substring(0,a))+i.substring(u+1):a=u}}return i===""?null:i}function L(e,s,t,i,o,n){var a=e.__className;if(a!==t||a===void 0){var u=Ni(t,i,n);u==null?e.removeAttribute("class"):e.className=u,e.__className=t}else if(n&&o!==n)for(var r in n){var d=!!n[r];(o==null||d!==!!o[r])&&e.classList.toggle(r,d)}return n}const Ui=Symbol("is custom element"),Di=Symbol("is html");function Ae(e,s,t,i){var o=Vi(e);o[s]!==(o[s]=t)&&(t==null?e.removeAttribute(s):typeof t!="string"&&Ii(e).includes(s)?e[s]=t:e.setAttribute(s,t))}function Vi(e){return e.__attributes??(e.__attributes={[Ui]:e.nodeName.includes("-"),[Di]:e.namespaceURI===ti})}var Rt=new Map;function Ii(e){var s=Rt.get(e.nodeName);if(s)return s;Rt.set(e.nodeName,s=[]);for(var t,i=e,o=Element.prototype;o!==i;){t=Wt(i);for(var n in t)t[n].set&&s.push(n);i=bt(i)}return s}function Ne(e,s,t=s){var i=Ke();qs(e,"input",o=>{var n=o?e.defaultValue:e.value;if(n=at(e)?ut(n):n,t(n),i&&n!==(n=s())){var a=e.selectionStart,u=e.selectionEnd;e.value=n??"",u!==null&&(e.selectionStart=a,e.selectionEnd=Math.min(u,e.value.length))}}),Ye(s)==null&&e.value&&t(at(e)?ut(e.value):e.value),At(()=>{var o=s();at(e)&&o===ut(e.value)||e.type==="date"&&!o&&!e.value||o!==e.value&&(e.value=o??"")})}function Ri(e,s,t=s){qs(e,"change",i=>{var o=i?e.defaultChecked:e.checked;t(o)}),Ye(s)==null&&t(e.checked),At(()=>{var i=s();e.checked=!!i})}function at(e){var s=e.type;return s==="number"||s==="range"}function ut(e){return e===""?null:+e}function Fi(e){return function(...s){var t=s[0];return t.stopPropagation(),e==null?void 0:e.apply(this,s)}}function j(e){return function(...s){var t=s[0];return t.preventDefault(),e==null?void 0:e.apply(this,s)}}function ne(e=!1){const s=S,t=s.l.u;if(!t)return;let i=()=>Ai(s.s);if(e){let o=0,n={};const a=kt(()=>{let u=!1;const r=s.s;for(const d in r)r[d]!==n[d]&&(n[d]=r[d],u=!0);return u&&o++,o});i=()=>l(a)}t.b.length&&ai(()=>{Ft(s,i),Qe(t.b)}),dt(()=>{const o=Ye(()=>t.m.map(Rs));return()=>{for(const n of o)typeof n=="function"&&n()}}),t.a.length&&dt(()=>{Ft(s,i),Qe(t.a)})}function Ft(e,s){if(e.l.s)for(const t of e.l.s)l(t);s()}function he(e){S===null&&es(),We&&S.l!==null?$i(S).m.push(e):dt(()=>{const s=Ye(e);if(typeof s=="function")return s})}function Oi(e,s,{bubbles:t=!1,cancelable:i=!1}={}){return new CustomEvent(e,{detail:s,bubbles:t,cancelable:i})}function Bi(){const e=S;return e===null&&es(),(s,t,i)=>{var n;const o=(n=e.s.$$events)==null?void 0:n[s];if(o){const a=gt(o)?o.slice():[o],u=Oi(s,t,i);for(const r of a)r.call(e.x,u);return!u.defaultPrevented}return!0}}function $i(e){var s=e.l;return s.u??(s.u={a:[],b:[],m:[]})}const Hi="5";var Ht;typeof window<"u"&&((Ht=window.__svelte??(window.__svelte={})).v??(Ht.v=new Set)).add(Hi);Js();var Wi=Es(''),Ki=Es(''),Yi=R('
'),Zi=R(`
`);function Qi(e,s){J(s,!1);let t=B(window.location.hash.slice(1)||"accueil"),i=B(!1),o=B("");function n(){const E=window.location.hostname;let P="";E==="dev.geosector.fr"||E.includes("localhost")?P="dapp":E==="rec.geosector.fr"?P="rapp":P="app";const de=E.split(".");if(de.length>=2){const Ze=de.slice(Math.max(de.length-2,0)).join(".");return`https://${P}.${Ze}`}return`https://${P}.geosector.fr`}function a(E){q(t,E),window.history.pushState({},"",`/${E}`),window.dispatchEvent(new PopStateEvent("popstate")),r()}function u(E){E.stopPropagation(),q(i,!l(i))}function r(){q(i,!1)}typeof window<"u"&&window.addEventListener("popstate",()=>{q(t,window.location.pathname.slice(1)||"accueil")}),he(()=>{q(o,n());const E=P=>{const de=document.getElementById("mobile-menu"),Ze=document.getElementById("burger-button");l(i)&&de&&Ze&&!de.contains(P.target)&&!Ze.contains(P.target)&&r()};return setTimeout(()=>{document.addEventListener("click",E)},100),()=>{document.removeEventListener("click",E)}}),ne();var d=Zi(),f=c(d),p=c(f),v=c(p),h=c(v),b=c(h),w=m(v,2),k=c(w),g=c(k);{var _=E=>{var P=Wi();N(E,P)},A=E=>{var P=Ki();N(E,P)};te(g,E=>{l(i)?E(_):E(A,!1)})}var M=m(w,2),T=c(M),Z=c(T),U=c(Z),G=c(U),$=m(U,2),ge=c($),ze=m($,2),ye=c(ze),ee=m(T,2),ue=c(ee),ce=m(ue,2),Ge=m(ce,2),H=m(f,2),Pe=c(H),be=c(Pe),je=c(be),Q=c(je),ke=c(Q),V=m(Q,2),Lt=c(V),Ls=m(V,2),Mt=c(Ls),Ms=m(be,2),ot=c(Ms),nt=m(ot,2),Ts=m(nt,2),zs=m(H,2);{var Gs=E=>{var P=Yi();C("click",P,r),C("keydown",P,de=>de.key==="Escape"&&r()),N(E,P)};te(zs,E=>{l(i)&&E(Gs)})}ae(()=>{L(G,1,`text-[#002C66] hover:text-blue-500 transition-colors ${l(t)==="accueil"?"font-bold border-b-2 border-[#002C66]":""}`),L(ge,1,`text-[#002C66] hover:text-blue-500 transition-colors ${l(t)==="fonctionnalites"?"font-bold border-b-2 border-[#002C66]":""}`),L(ye,1,`text-[#002C66] hover:text-blue-500 transition-colors ${l(t)==="contact"?"font-bold border-b-2 border-[#002C66]":""}`),Ae(ue,"href",`${l(o)??""}/?action=login&type=user`),Ae(ce,"href",`${l(o)??""}/?action=login&type=admin`),Ae(Ge,"href",`${l(o)??""}/?action=register`),L(H,1,`xl:hidden fixed top-0 right-0 h-screen w-4/5 max-w-xs bg-white shadow-lg transform transition-transform duration-300 ease-in-out z-40 ${l(i)?"translate-x-0":"translate-x-full"}`),L(ke,1,`block text-lg text-[#002C66] hover:text-blue-500 transition-colors ${l(t)==="accueil"?"font-bold":""}`),L(Lt,1,`block text-lg text-[#002C66] hover:text-blue-500 transition-colors ${l(t)==="fonctionnalites"?"font-bold":""}`),L(Mt,1,`block text-lg text-[#002C66] hover:text-blue-500 transition-colors ${l(t)==="contact"?"font-bold":""}`),Ae(ot,"href",`${l(o)??""}/?action=login&type=user`),Ae(nt,"href",`${l(o)??""}/?action=login&type=admin`),Ae(Ts,"href",`${l(o)??""}/?action=register`)}),C("click",b,j(()=>a("accueil"))),C("click",w,Fi(u)),C("click",G,j(()=>a("accueil"))),C("click",ge,j(()=>a("fonctionnalites"))),C("click",ye,j(()=>a("contact"))),C("click",ue,()=>{sessionStorage.setItem("loginType","user")}),C("click",ce,()=>{sessionStorage.setItem("loginType","admin")}),C("click",ke,j(()=>a("accueil"))),C("click",Lt,j(()=>a("fonctionnalites"))),C("click",Mt,j(()=>a("contact"))),C("click",ot,()=>{sessionStorage.setItem("loginType","user")}),C("click",nt,()=>{sessionStorage.setItem("loginType","admin")}),N(e,d),X()}var Ji=R(``);function Xi(e,s){J(s,!1);function t(G){window.location.hash=G,window.scrollTo(0,0)}ne();var i=Ji(),o=c(i),n=c(o),a=m(c(n),2),u=m(c(a),2),r=c(u),d=m(c(r),2),f=m(r,2),p=m(c(f),2),v=m(f,2),h=m(c(v),2),b=m(v,4),w=m(c(b),2),k=m(b,2),g=m(c(k),2),_=m(k,2),A=m(c(_),2),M=m(n,2),T=c(M),Z=c(T),U=c(Z);ae(G=>it(U,`© ${G??""} Geosector. Tous droits réservés.`),[()=>new Date().getFullYear()],me),C("click",d,j(()=>t("accueil"))),C("click",p,j(()=>t("fonctionnalites"))),C("click",h,j(()=>t("contact"))),C("click",w,j(()=>t("mentions-legales"))),C("click",g,j(()=>t("politique-confidentialite"))),C("click",A,j(()=>t("conditions-utilisation"))),N(e,i),X()}var eo=R(`
`);function to(e,s){J(s,!1);const t=Bi();function i(){const f=new Date;f.setDate(f.getDate()+2),document.cookie=`geosector_cookies_accepted=true; expires=${f.toUTCString()}; path=/; SameSite=Lax`,t("consent",{accepted:!0})}function o(){const f=new Date;f.setDate(f.getDate()+2),document.cookie=`geosector_cookies_refused=true; expires=${f.toUTCString()}; path=/; SameSite=Lax`,t("consent",{accepted:!1})}ne();var n=eo(),a=c(n),u=m(c(a),4),r=c(u),d=m(r,2);C("click",r,o),C("click",d,i),N(e,n),X()}function He(e){const s=document.cookie.split("; ").find(t=>t.startsWith(`${e}=`));return s?s.split("=")[1]:null}function so(){return He("geosector_cookies_accepted")!==null||He("geosector_cookies_refused")!==null}function Ot(){if(He("geosector_cookies_accepted")==="true"){const e=window.location.hash.slice(1)||"accueil";console.log("Suivi anonyme activé - "+new Date().toISOString()),console.log("Page courante: "+e)}}function Bt(){He("geosector_cookies_refused")==="true"&&console.log("Suivi anonyme désactivé - "+new Date().toISOString())}const Ss="geosector_last_tracking";function $t(e){if(He("geosector_cookies_accepted")!=="true"){console.log("Suivi désactivé : cookies non acceptés");return}if(!io()){console.log("Suivi différé : déjà suivi dans les 2 derniers jours");return}localStorage.setItem(Ss,new Date().toISOString()),console.log(`Page consultée: ${e} - ${new Date().toISOString()}`)}function io(){const e=localStorage.getItem(Ss);if(!e)return!0;const s=new Date(e),i=Math.abs(new Date-s);return Math.ceil(i/(1e3*60*60*24))>=2}var oo=R(`

Gestion efficace de vos distributions de calendriers

Une application puissante et intuitive pour optimiser vos tournées et améliorer votre productivité.

Dashboard Geosector

Interface de gestion

Mobile App

Interface mobile

Pourquoi choisir Geosector ?

Optimisation des tournées

Grace au mode Terrain, Geosector aide le membre à traiter les adresses à finaliser proche de lui.

Simplicité d'utilisation

Interface intuitive conçue pour faciliter la gestion quotidienne de vos distributions.

Sécurité des données

Vos données sont protégées en conformité au RGPD et sauvegardées régulièrement.

Ce que nos clients disent

TP

Trystan PAPIN

Trésorier de l'amicale des SP du Malesherbois

"Bonjour, Je confirme l’utilisation de l’application Geosector pour l’amicale des SP de Malesherbes. Superbe application encore merci à vous !"

ML

Marie Leroy

Responsable opérations, LogiExpress

"L'interface intuitive de Geosector nous a permis de former rapidement nos équipes. La visualisation en temps réel des tournées est un atout majeur pour notre activité quotidienne."

`);function no(e,s){J(s,!1);let t=B(!1);he(()=>{q(t,!0)}),ne();var i=oo(),o=c(i),n=c(o),a=c(n),u=c(a),r=c(u);let d;var f=m(r,2);let p;var v=m(f,2);let h;var b=m(u,2);let w;ae((k,g,_,A)=>{d=L(r,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,d,k),p=L(f,1,"text-xl mb-8 transition-all duration-700 delay-300 text-[#002C66]",null,p,g),h=L(v,1,"transition-all duration-700 delay-500",null,h,_),w=L(b,1,"md:w-1/2 transition-all duration-700 delay-700 relative flex justify-center md:justify-end",null,w,A)},[()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-x-0":l(t),"opacity-100":l(t),"translate-x-10":!l(t),"opacity-0":!l(t)})],me),N(e,i),X()}var ro=R(`

Fonctionnalités

Découvrez les outils puissants qui font de Geosector la solution idéale pour la gestion de vos distributions.

Fonctionnalités principales

Cartographie avancée

Visualisez vos tournées sur des cartes interactives avec des données en temps réel sur le trafic et les conditions météorologiques.

  • Cartes détaillées avec points d'intérêt
  • Suivi GPS en temps réel
  • Alertes de trafic et d'incidents

Optimisation des itinéraires

Nos algorithmes avancés calculent les itinéraires les plus efficaces en tenant compte de multiples facteurs.

  • Réduction des coûts de carburant jusqu'à 30%
  • Prise en compte des contraintes horaires
  • Adaptation dynamique aux conditions réelles

Planification intelligente

Planifiez vos tournées à l'avance et adaptez-les facilement en fonction des imprévus.

  • Calendrier interactif avec vue mensuelle/hebdomadaire/quotidienne
  • Gestion des priorités et des urgences
  • Notifications automatiques pour les changements

Rapports et analyses

Obtenez des insights précieux sur vos opérations grâce à nos outils d'analyse avancés.

  • Tableaux de bord personnalisables
  • Exportation des données en plusieurs formats
  • Indicateurs de performance clés (KPIs)

Application mobile

Emportez Geosector partout avec vous

Notre application mobile offre toutes les fonctionnalités essentielles pour gérer vos distributions en déplacement.

Interface adaptée aux mobiles

Expérience utilisateur optimisée pour les écrans tactiles et la navigation mobile.

Mode hors ligne

Continuez à travailler même sans connexion internet, avec synchronisation automatique.

Notifications push

Restez informé des changements importants et des mises à jour en temps réel.

Télécharger sur l'App Store Télécharger sur Google Play
Capture d'écran de l'application mobile

Prêt à optimiser vos distributions ?

Rejoignez les milliers d'entreprises qui font confiance à Geosector pour améliorer leur efficacité opérationnelle.

Demander une démo
`);function lo(e,s){J(s,!1);let t=B(!1);he(()=>{q(t,!0)}),ne();var i=ro(),o=c(i),n=c(o),a=c(n),u=c(a);let r;var d=m(u,2);let f;ae((p,v)=>{r=L(u,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,r,p),f=L(d,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,f,v)},[()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)})],me),N(e,i),X()}var ao=R('

Message envoyé avec succès !

Nous vous répondrons dans les plus brefs délais.

'),uo=R('
'),co=R(`

Contactez-nous

Notre équipe est à votre disposition pour répondre à toutes vos questions et vous accompagner dans votre projet.

Nos coordonnées

Téléphone

+33 (0)1 23 45 67 89

Email

contact@geosector.fr

Horaires d'ouverture

Lundi - Vendredi: 9h00 - 18h00
Samedi - Dimanche: Fermé

Suivez-nous

Envoyez-nous un message

Questions fréquentes

Comment puis-je obtenir une démonstration de Geosector ?

Vous pouvez demander une démonstration en remplissant le formulaire de contact ci-dessus ou en nous appelant directement. Un de nos conseillers vous contactera pour organiser une session personnalisée.

Combien de temps dure la période d'essai ?

Nous proposons une période d'essai gratuite de 14 jours avec toutes les fonctionnalités disponibles. Aucune carte de crédit n'est requise pour commencer votre essai.

Proposez-vous des formations pour utiliser votre logiciel ?

Oui, nous proposons des sessions de formation complètes pour vous aider à tirer le meilleur parti de Geosector. Ces formations peuvent être réalisées en ligne ou dans vos locaux selon vos préférences.

Quels types de support technique proposez-vous ?

Nous offrons un support technique par email, téléphone et chat en direct pendant les heures de bureau. Nos clients avec des forfaits premium bénéficient d'un support 24/7.

`);function po(e,s){J(s,!1);let t=B(!1),i=B({nom:"",email:"",telephone:"",entreprise:"",message:"",newsletter:!1}),o=B(!1);function n(){q(o,!0),console.log("Formulaire soumis:",l(i))}he(()=>{q(t,!0)}),ne();var a=co(),u=c(a),r=c(u),d=c(r),f=c(d);let p;var v=m(f,2);let h;var b=m(u,2),w=c(b),k=c(w),g=c(k),_=c(g),A=m(c(_),2),M=m(c(A),2);{var T=U=>{var G=ao();N(U,G)},Z=U=>{var G=uo(),$=c(G),ge=c($),ze=m(c(ge),2),ye=m(ge,2),ee=m(c(ye),2),ue=m($,2),ce=c(ue),Ge=m(c(ce),2),H=m(ce,2),Pe=m(c(H),2),be=m(ue,2),je=m(c(be),2),Q=m(be,2),ke=c(Q);Ne(ze,()=>l(i).nom,V=>Ce(i,l(i).nom=V)),Ne(ee,()=>l(i).email,V=>Ce(i,l(i).email=V)),Ne(Ge,()=>l(i).telephone,V=>Ce(i,l(i).telephone=V)),Ne(Pe,()=>l(i).entreprise,V=>Ce(i,l(i).entreprise=V)),Ne(je,()=>l(i).message,V=>Ce(i,l(i).message=V)),Ri(ke,()=>l(i).newsletter,V=>Ce(i,l(i).newsletter=V)),C("submit",G,j(n)),N(U,G)};te(M,U=>{l(o)?U(T):U(Z,!1)})}ae((U,G)=>{p=L(f,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,p,U),h=L(v,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,h,G)},[()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)})],me),N(e,a),X()}var vo=R(`

Politique de confidentialité

Protection de vos données personnelles et respect de votre vie privée

Introduction

Cette politique de confidentialité s'applique à l'application Geosector, disponible sur le Web, iOS et Android, - ainsi qu'à tous les services associés (collectivement désignés par "Geosector", "nous", "notre" ou "nos").

Chez Geosector, nous accordons une grande importance à la protection de vos données personnelles. - Cette politique décrit quelles informations nous collectons, comment nous les utilisons, - et quels choix vous avez concernant ces données.

Cette politique de confidentialité doit être lue conjointement avec nos Conditions d'utilisation, qui régissent votre utilisation de notre application.

Quelles informations collectons-nous ?

1. Informations que vous nous fournissez

  • Informations de compte : Lors de l'inscription, nous collectons votre nom, prénom, adresse e-mail, et mot de passe.
  • Informations de profil : Vous pouvez nous fournir des informations supplémentaires comme votre fonction, l'organisation à laquelle vous appartenez, et votre photo de profil.
  • Contenu utilisateur : Les informations que vous créez, téléchargez ou partagez via notre application, notamment les secteurs géographiques, les passages, et les commentaires.
  • Communications : Lorsque vous nous contactez, nous conservons un historique de ces communications.

2. Informations collectées automatiquement

  • Données d'utilisation : Informations sur vos interactions avec notre application, comme les fonctionnalités utilisées, les pages visitées et le temps passé.
  • Informations sur l'appareil : Type d'appareil, système d'exploitation, version de l'application, langue, fuseau horaire et autres caractéristiques techniques.
  • Données de localisation : Avec votre consentement, nous collectons des données de géolocalisation précises pour vous permettre d'utiliser les fonctionnalités cartographiques et de secteurs.
  • Cookies et technologies similaires : Sur notre version web, nous utilisons des cookies et des technologies similaires pour améliorer votre expérience. Pour plus d'informations, consultez notre politique relative aux cookies.

Comment utilisons-nous vos informations ?

Nous utilisons vos informations pour les finalités suivantes :

  • Fournir, maintenir et améliorer notre application et ses fonctionnalités
  • Créer et gérer votre compte
  • Traiter vos transactions et paiements
  • Vous envoyer des informations techniques, des mises à jour, des alertes de sécurité et des messages administratifs
  • Répondre à vos commentaires et questions et vous fournir un support client
  • Communiquer avec vous à propos de produits, services, offres et événements
  • Surveiller et analyser les tendances, l'utilisation et les activités liées à notre application
  • Détecter, prévenir et résoudre les problèmes techniques et de sécurité
  • Se conformer aux obligations légales

Base légale du traitement (pour les utilisateurs de l'EEE et du Royaume-Uni)

Pour les utilisateurs de l'Espace économique européen (EEE) et du Royaume-Uni, nous traitons vos données personnelles sur les bases légales suivantes :

  • Exécution d'un contrat : Lorsque le traitement est nécessaire pour l'exécution d'un contrat auquel vous êtes partie ou pour prendre des mesures à votre demande avant de conclure un contrat.
  • Intérêts légitimes : Lorsque le traitement est nécessaire pour nos intérêts légitimes ou ceux d'un tiers, et que ces intérêts ne sont pas supplantés par vos intérêts ou droits fondamentaux.
  • Consentement : Lorsque vous avez donné votre consentement au traitement de vos données personnelles pour une ou plusieurs finalités spécifiques.
  • Obligation légale : Lorsque le traitement est nécessaire pour respecter une obligation légale à laquelle nous sommes soumis.

Comment partageons-nous vos informations ?

Nous pouvons partager vos informations personnelles avec les tiers suivants :

  • Prestataires de services : Nous travaillons avec des prestataires de services tiers qui fournissent des services tels que l'hébergement, l'analyse, le traitement des paiements et le support client.
  • Partenaires professionnels : Nous pouvons partager des informations avec nos partenaires commerciaux pour offrir certains produits, services ou promotions.
  • Conformité légale : Nous pouvons divulguer vos informations si nous estimons de bonne foi que cette divulgation est nécessaire pour se conformer à la loi, protéger nos droits ou assurer votre sécurité.
  • Transactions d'entreprise : En cas de fusion, acquisition, restructuration ou vente d'actifs, vos informations peuvent être transférées dans le cadre de cette transaction.

Nous ne vendons pas vos données personnelles à des tiers.

Transferts internationaux de données

Vos informations peuvent être transférées et traitées dans des pays autres que celui où vous résidez. - Ces pays peuvent avoir des lois sur la protection des données différentes de celles de votre pays.

Si nous transférons des données personnelles provenant de l'EEE, du Royaume-Uni ou de la Suisse vers des pays - n'offrant pas un niveau de protection adéquat selon les autorités compétentes, nous utilisons des - mécanismes de transfert légalement reconnus, tels que les clauses contractuelles types approuvées par la Commission européenne.

Vos droits et choix

Selon votre lieu de résidence, vous pouvez disposer de certains droits concernant vos données personnelles :

  • Accès et portabilité : Vous pouvez accéder à vos informations personnelles et en obtenir une copie dans un format structuré, couramment utilisé et lisible par machine.
  • Correction : Vous pouvez mettre à jour ou corriger vos informations personnelles si elles sont inexactes ou incomplètes.
  • Suppression : Vous pouvez demander la suppression de vos données personnelles dans certaines circonstances.
  • Restriction et opposition : Vous pouvez demander la restriction du traitement de vos données personnelles ou vous opposer à leur traitement dans certaines circonstances.
  • Consentement : Lorsque le traitement est basé sur votre consentement, vous pouvez retirer ce consentement à tout moment.
  • Réclamation : Vous avez le droit d'introduire une réclamation auprès d'une autorité de protection des données.

Pour exercer ces droits, contactez-nous à l'adresse indiquée dans la section "Nous contacter" ci-dessous. - Notez que ces droits peuvent être soumis à des limitations et exceptions prévues par la loi applicable.

Conservation des données

Nous conservons vos données personnelles aussi longtemps que nécessaire pour atteindre les finalités décrites dans cette politique, - sauf si une période de conservation plus longue est requise ou permise par la loi. - Les critères utilisés pour déterminer nos périodes de conservation comprennent :

  • La durée pendant laquelle nous entretenons une relation continue avec vous et vous fournissons l'application
  • Si nous avons une obligation légale à laquelle nous sommes soumis
  • Si la conservation est souhaitable compte tenu de notre position juridique (par exemple, concernant les délais de prescription applicables, les litiges ou les enquêtes réglementaires)

Sécurité des données

Nous mettons en œuvre des mesures de sécurité techniques et organisationnelles appropriées pour protéger vos données personnelles - contre la perte accidentelle, l'utilisation non autorisée, l'altération et la divulgation. - Ces mesures comprennent le chiffrement des données, les contrôles d'accès, les pare-feu et les audits de sécurité réguliers.

Cependant, aucun système de sécurité n'est impénétrable et nous ne pouvons garantir la sécurité absolue de vos informations. - Il est important que vous preniez des précautions pour protéger votre mot de passe et votre appareil.

Protection de la vie privée des enfants

Notre application n'est pas destinée aux personnes âgées de moins de 16 ans et nous ne collectons pas sciemment - des données personnelles auprès d'enfants de moins de 16 ans. Si vous êtes parent ou tuteur et que vous pensez - que votre enfant nous a fourni des informations personnelles, veuillez nous contacter.

Modifications de cette politique

Nous pouvons modifier cette politique de confidentialité de temps à autre. Si nous apportons des modifications importantes, - nous vous en informerons par e-mail ou par une notification dans notre application avant que les modifications - ne prennent effet. Nous vous encourageons à consulter régulièrement cette politique pour rester informé de - nos pratiques en matière de protection des données.

Nous contacter

Si vous avez des questions concernant cette politique de confidentialité ou nos pratiques en matière de protection des données, - veuillez nous contacter à l'adresse suivante :

Geosector
E-mail : privacy@geosector.fr
Adresse : [Adresse de l'entreprise]
Téléphone : +33 (0)1 23 45 67 89

Informations spécifiques aux plateformes

Application iOS (Apple App Store)

En utilisant notre application via l'App Store d'Apple, vous reconnaissez qu'Apple n'est pas responsable de nos pratiques - en matière de protection des données. Veuillez consulter la politique de confidentialité d'Apple pour plus d'informations - sur la façon dont Apple peut collecter et traiter vos données.

Application Android (Google Play)

En utilisant notre application via Google Play, vous reconnaissez que Google n'est pas responsable de nos pratiques - en matière de protection des données. Veuillez consulter la politique de confidentialité de Google pour plus d'informations - sur la façon dont Google peut collecter et traiter vos données.

Permissions des applications mobiles

Notre application peut demander certaines permissions sur votre appareil mobile, notamment :

  • Localisation : Pour les fonctionnalités basées sur la localisation, comme l'affichage des secteurs et la navigation
  • Stockage : Pour stocker des données localement sur votre appareil
  • Appareil photo : Pour scanner des codes QR ou prendre des photos
  • Notifications : Pour vous envoyer des alertes et des mises à jour importantes

Vous pouvez gérer ces permissions à tout moment dans les paramètres de votre appareil, mais notez que - la désactivation de certaines permissions peut limiter les fonctionnalités de l'application.

`);function fo(e,s){J(s,!1);let t=B(!1);function i(M){window.location.hash=M,window.scrollTo(0,0)}he(()=>{q(t,!0)}),ne();var o=vo(),n=c(o),a=c(n),u=c(a),r=c(u);let d;var f=m(r,2);let p;var v=m(n,2),h=c(v),b=c(h),w=c(b),k=c(w),g=c(k),_=m(k,8),A=m(c(_));ae((M,T,Z)=>{d=L(r,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,d,M),p=L(f,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,p,T),it(g,`Dernière mise à jour : ${Z??""}`)},[()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>new Date().toLocaleDateString("fr-FR",{year:"numeric",month:"long",day:"numeric"})],me),C("click",A,j(()=>i("conditions-utilisation"))),N(e,o),X()}var mo=R(`

Conditions d'utilisation

Règles et modalités d'utilisation de l'application Geosector

1. Préambule

Les présentes conditions générales d'utilisation (ci-après dénommées "CGU") régissent l'utilisation de l'application Geosector (ci-après dénommée l'"Application"), - accessible via le Web à l'adresse app.geosector.fr, ainsi que sur les plateformes iOS (Apple App Store) et Android (Google Play Store).

Geosector est une application dédiée à la gestion de secteurs géographiques et de passages, permettant à ses utilisateurs d'optimiser - leurs distributions et tournées. L'application est exploitée par [Nom de la société], dont le siège social est situé à [Adresse complète], - immatriculée au Registre du Commerce et des Sociétés de [Ville] sous le numéro [Numéro RCS].

En utilisant notre Application, vous acceptez de vous conformer aux présentes CGU. Si vous n'acceptez pas ces conditions, - veuillez ne pas utiliser l'Application.

2. Définitions

Dans les présentes CGU, les termes suivants ont la signification qui leur est attribuée ci-dessous :

  • "Application" désigne l'application Geosector, accessible via le Web, iOS et Android.
  • "Compte" désigne l'espace personnel de l'Utilisateur sur l'Application.
  • "Contenu" désigne toutes les informations et données (y compris les textes, images, vidéos, etc.) accessibles ou générées via l'Application.
  • "Fonctionnalités" désigne les services et outils proposés par l'Application.
  • "Utilisateur" désigne toute personne physique ou morale ayant accès à l'Application.
  • "Données Personnelles" désigne toute information se rapportant à une personne physique identifiée ou identifiable.

3. Inscription et compte utilisateur

3.1 Conditions d'inscription

Pour utiliser l'ensemble des Fonctionnalités de l'Application, l'Utilisateur doit créer un Compte en fournissant - les informations requises. L'Utilisateur s'engage à fournir des informations exactes, complètes et à jour. - Toute fausse déclaration peut entraîner la suspension ou la suppression du Compte.

3.2 Sécurité du compte

L'Utilisateur est responsable de la confidentialité de ses identifiants de connexion (nom d'utilisateur et mot de passe) - et s'engage à ne pas les communiquer à des tiers. Toute connexion effectuée en utilisant les identifiants de l'Utilisateur - sera présumée avoir été effectuée par celui-ci.

3.3 Suspension ou suppression de compte

Geosector se réserve le droit de suspendre ou de supprimer un Compte en cas de :

  • Non-respect des présentes CGU
  • Inactivité prolongée
  • Utilisation frauduleuse ou abusive de l'Application
  • Non-paiement des services payants
  • Demande de l'Utilisateur

4. Utilisation de l'Application

4.1 Licence d'utilisation

Sous réserve du respect des présentes CGU, Geosector accorde à l'Utilisateur une licence limitée, non exclusive, - non transférable et révocable pour accéder et utiliser l'Application à des fins professionnelles ou personnelles.

4.2 Restrictions d'utilisation

L'Utilisateur s'engage à ne pas :

  • Utiliser l'Application à des fins illégales ou interdites par les présentes CGU
  • Tenter de perturber le fonctionnement de l'Application ou d'accéder aux données d'autres Utilisateurs
  • Utiliser des robots, spiders, scrapers ou autres moyens automatisés pour accéder à l'Application
  • Contourner les mesures de sécurité de l'Application
  • Reproduire, copier, vendre, revendre ou exploiter toute partie de l'Application sans autorisation écrite préalable
  • Utiliser l'Application d'une manière qui pourrait endommager, désactiver, surcharger ou altérer les serveurs ou les réseaux

4.3 Contenu de l'Utilisateur

En publiant, téléchargeant, ou partageant du Contenu via l'Application, l'Utilisateur accorde à Geosector une licence mondiale, - non exclusive, transférable, libre de redevances pour utiliser, reproduire, modifier, adapter, publier, traduire et distribuer ce Contenu - dans le cadre de l'exploitation et de l'amélioration de l'Application.

L'Utilisateur garantit qu'il dispose des droits nécessaires sur le Contenu qu'il partage et que ce Contenu n'enfreint pas - les droits de tiers ni les lois applicables.

5. Services payants et abonnements

5.1 Offres et tarifs

Certaines Fonctionnalités de l'Application peuvent être soumises à paiement. Les offres et tarifs sont disponibles sur le site web - de Geosector ou directement dans l'Application. Geosector se réserve le droit de modifier ses offres et tarifs à tout moment, - moyennant un préavis raisonnable.

5.2 Paiement et facturation

Les paiements sont effectués par carte bancaire ou tout autre moyen proposé dans l'Application. Pour les abonnements, - le paiement est automatiquement renouvelé à la fin de chaque période, sauf résiliation par l'Utilisateur avant la date de renouvellement.

Une facture électronique est mise à disposition de l'Utilisateur pour chaque paiement effectué.

5.3 Politique de remboursement

Conformément à la législation applicable, l'Utilisateur bénéficie d'un droit de rétractation de 14 jours à compter de la souscription - à un service payant, sauf si l'exécution du service a commencé avec son accord avant la fin de ce délai.

Aucun remboursement ne sera accordé après l'expiration du délai de rétractation, sauf en cas de dysfonctionnement majeur de l'Application - imputable à Geosector.

6. Propriété intellectuelle

6.1 Droits de Geosector

L'Application, y compris son contenu, sa structure, ses fonctionnalités, son code source, ses interfaces, son design, - ses logos et ses marques, est la propriété exclusive de Geosector ou de ses concédants de licence. - Ces éléments sont protégés par les lois relatives à la propriété intellectuelle.

6.2 Droits des Utilisateurs

L'Utilisateur conserve tous les droits de propriété intellectuelle sur le Contenu qu'il crée et partage via l'Application, - sous réserve de la licence accordée à Geosector conformément à l'article 4.3.

6.3 Signalement d'une violation

Si vous pensez que votre contenu a été utilisé d'une manière qui constitue une violation de vos droits de propriété intellectuelle, - veuillez nous contacter à l'adresse suivante : [adresse email].

7. Confidentialité et données personnelles

La collecte et le traitement des Données Personnelles des Utilisateurs sont régis par notre Politique de Confidentialité, - disponible à l'adresse suivante : Politique de confidentialité.

8. Limitation de responsabilité

8.1 Disponibilité de l'Application

Geosector s'efforce de maintenir l'Application accessible 24 heures sur 24 et 7 jours sur 7. Cependant, l'accès peut être - temporairement suspendu, sans préavis, en raison de maintenance technique, de mise à jour ou pour toute autre raison.

Geosector ne peut être tenu responsable de tout dommage résultant de l'indisponibilité temporaire de l'Application.

8.2 Contenus et services tiers

L'Application peut contenir des liens vers des sites web ou services tiers. Geosector n'exerce aucun contrôle sur ces sites et services - et n'assume aucune responsabilité quant à leur contenu ou leurs pratiques.

8.3 Limitation générale de responsabilité

Dans toute la mesure permise par la loi applicable, Geosector ne pourra être tenu responsable de tout dommage indirect, - spécial, accessoire, consécutif ou punitif, y compris les pertes de profits, de revenus, de données ou d'opportunités commerciales, - résultant de l'utilisation ou de l'impossibilité d'utiliser l'Application.

La responsabilité totale de Geosector envers l'Utilisateur pour toute réclamation découlant des présentes CGU ne pourra excéder - le montant payé par l'Utilisateur à Geosector au cours des douze (12) mois précédant le fait générateur de la responsabilité.

9. Modifications des CGU

Geosector se réserve le droit de modifier les présentes CGU à tout moment. Les Utilisateurs seront informés des modifications - par le biais d'une notification dans l'Application ou par e-mail.

Les modifications prendront effet à la date indiquée dans la notification. En continuant à utiliser l'Application après cette date, - l'Utilisateur accepte les CGU modifiées.

Si l'Utilisateur n'accepte pas les modifications, il doit cesser d'utiliser l'Application et, le cas échéant, supprimer son Compte.

10. Résiliation

10.1 Résiliation par l'Utilisateur

L'Utilisateur peut, à tout moment, cesser d'utiliser l'Application et supprimer son Compte en suivant la procédure prévue à cet effet - dans les paramètres de l'Application.

10.2 Résiliation par Geosector

Geosector peut, à sa discrétion, suspendre ou résilier l'accès de l'Utilisateur à l'Application en cas de violation des présentes CGU, - sans préjudice de tout autre droit ou recours.

10.3 Conséquences de la résiliation

En cas de résiliation, l'Utilisateur perd l'accès à son Compte et à toutes les Fonctionnalités de l'Application. - Les sections des présentes CGU relatives à la propriété intellectuelle, à la limitation de responsabilité et au règlement des litiges - survivront à la résiliation.

11. Dispositions spécifiques aux applications mobiles

11.1 Application iOS (Apple App Store)

Si vous téléchargez l'Application via l'App Store d'Apple, vous reconnaissez et acceptez que :

  • Ces CGU sont conclues entre vous et Geosector, et non avec Apple
  • Apple n'a aucune obligation de fournir des services de maintenance ou d'assistance concernant l'Application
  • En cas de non-conformité de l'Application avec une garantie applicable, vous pouvez en informer Apple, qui pourra vous rembourser le prix d'achat
  • Apple n'est pas responsable du traitement des réclamations ou de la responsabilité liée à l'Application
  • En cas de réclamation d'un tiers selon laquelle l'Application enfreint ses droits de propriété intellectuelle, Apple n'est pas responsable de l'enquête, de la défense, du règlement et de la décharge de cette réclamation
  • Vous devez vous conformer aux conditions d'utilisation de l'App Store d'Apple lors de l'utilisation de l'Application

11.2 Application Android (Google Play)

Si vous téléchargez l'Application via Google Play, vous reconnaissez et acceptez que :

  • Ces CGU sont conclues entre vous et Geosector, et non avec Google
  • L'utilisation de l'Application doit respecter les conditions d'utilisation de Google Play
  • Google n'a aucune obligation de fournir des services de maintenance ou d'assistance concernant l'Application

12. Dispositions diverses

12.1 Droit applicable et juridiction compétente

Les présentes CGU sont régies par le droit français. Tout litige relatif à leur interprétation ou à leur exécution relève, - à défaut d'accord amiable, de la compétence exclusive des tribunaux français compétents.

12.2 Indépendance des clauses

Si une ou plusieurs dispositions des présentes CGU sont tenues pour non valides ou déclarées comme telles en application d'une loi, - d'un règlement ou à la suite d'une décision définitive d'une juridiction compétente, les autres stipulations garderont toute leur force - et leur portée.

12.3 Non-renonciation

Le fait pour Geosector de ne pas se prévaloir d'un manquement de l'Utilisateur à l'une quelconque des obligations visées dans les présentes CGU - ne saurait être interprété comme une renonciation à s'en prévaloir ultérieurement.

12.4 Communication

Toute notification ou communication dans le cadre des présentes CGU doit être adressée à Geosector par e-mail à l'adresse suivante : - [adresse email] ou par courrier postal à l'adresse suivante : [adresse postale].

13. Contact

Pour toute question concernant les présentes CGU, veuillez nous contacter à :

Geosector
E-mail : support@geosector.fr
Adresse : [Adresse de l'entreprise]
Téléphone : +33 (0)1 23 45 67 89

`);function ho(e,s){J(s,!1);let t=B(!1);function i(M){window.location.hash=M,window.scrollTo(0,0)}he(()=>{q(t,!0)}),ne();var o=mo(),n=c(o),a=c(n),u=c(a),r=c(u);let d;var f=m(r,2);let p;var v=m(n,2),h=c(v),b=c(h),w=c(b),k=c(w),g=c(k),_=m(k,84),A=m(c(_));ae((M,T,Z)=>{d=L(r,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,d,M),p=L(f,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,p,T),it(g,`Dernière mise à jour : ${Z??""}`)},[()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>new Date().toLocaleDateString("fr-FR",{year:"numeric",month:"long",day:"numeric"})],me),C("click",A,j(()=>i("politique-confidentialite"))),N(e,o),X()}var go=R(`

Mentions Légales

Informations juridiques relatives à notre site web et application mobile

1. Éditeur du site et de l'application

Le site web et l'application mobile Geosector sont édités par :

Geosector

SIRET : [Votre numéro SIRET]

Adresse : [Votre adresse]

Email : contact@geosector.fr

Téléphone : [Votre numéro de téléphone]

Directeur de la publication : [Nom du directeur de publication]

2. Hébergement

Le site web et l'application mobile Geosector sont hébergés par :

[Nom de l'hébergeur]

Adresse : [Adresse de l'hébergeur]

Site web : [Site web de l'hébergeur]

Email : [Email de l'hébergeur]

Téléphone : [Téléphone de l'hébergeur]

3. Propriété intellectuelle

L'ensemble du contenu du site web et de l'application mobile Geosector, incluant sans limitation les textes, graphiques, images, logos, icônes, photographies, est la propriété exclusive de Geosector et est protégé par les lois françaises et internationales relatives à la propriété intellectuelle.

Toute reproduction, représentation, modification, publication, transmission, adaptation, totale ou partielle des éléments du site ou de l'application, quel que soit le moyen ou le procédé utilisé, est interdite sans autorisation écrite préalable de Geosector.

Toute utilisation non autorisée des contenus, œuvres ou marques constitue une contrefaçon sanctionnée par le Code de la propriété intellectuelle.

4. Liens hypertextes

Le site web et l'application Geosector peuvent contenir des liens hypertextes vers d'autres sites internet ou applications.

Geosector n'a pas la possibilité de vérifier le contenu des sites ainsi visités, et n'assumera en conséquence aucune responsabilité de ce fait.

La création de liens hypertextes vers le site web ou l'application Geosector est soumise à l'accord préalable de l'éditeur.

5. Limitation de responsabilité

Geosector s'efforce d'assurer au mieux de ses possibilités l'exactitude et la mise à jour des informations diffusées sur son site web et son application mobile, dont elle se réserve le droit de corriger, à tout moment et sans préavis, le contenu.

Toutefois, Geosector ne peut garantir l'exactitude, la précision ou l'exhaustivité des informations mises à disposition sur son site web et son application.

En conséquence, Geosector décline toute responsabilité :

  • Pour toute imprécision, inexactitude ou omission portant sur des informations disponibles sur le site web ou l'application ;
  • Pour tous dommages résultant d'une intrusion frauduleuse d'un tiers ayant entraîné une modification des informations ou éléments mis à disposition sur le site web ou l'application ;
  • Et plus généralement, pour tous dommages, directs ou indirects, qu'elles qu'en soient les causes, origines, natures ou conséquences, provoqués en raison de l'accès de quiconque au site web ou à l'application ou de l'impossibilité d'y accéder, ainsi que l'utilisation du site web ou de l'application et/ou du crédit accordé à une quelconque information provenant directement ou indirectement de ces derniers.

6. Loi applicable et juridiction

Les présentes mentions légales sont régies par la loi française. En cas de litige, les tribunaux français seront seuls compétents.

Pour toute question relative à l'application des présentes mentions légales, vous pouvez nous contacter à l'adresse email : contact@geosector.fr

7. Modifications

Geosector se réserve le droit de modifier les présentes mentions légales à tout moment. L'utilisateur est invité à les consulter régulièrement.

`);function bo(e,s){J(s,!1);let t=B(!1);he(()=>{q(t,!0)}),ne();var i=go(),o=c(i),n=c(o),a=c(n),u=c(a);let r;var d=m(u,2);let f;var p=m(o,2),v=c(p),h=c(v),b=m(c(h),12),w=m(c(b),2),k=m(c(w),2),g=c(k);ae((_,A,M)=>{r=L(u,1,"text-4xl md:text-5xl font-bold mb-6 transition-all duration-700 text-[#002C66]",null,r,_),f=L(d,1,"text-xl max-w-3xl mx-auto transition-all duration-700 delay-300 text-[#002C66]",null,f,A),it(g,`Dernière mise à jour : ${M??""}`)},[()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>({"translate-y-0":l(t),"opacity-100":l(t),"translate-y-10":!l(t),"opacity-0":!l(t)}),()=>new Date().toLocaleDateString("fr-FR",{year:"numeric",month:"long",day:"numeric"})],me),N(e,i),X()}var xo=R(`

Page non trouvée

La page que vous recherchez n'existe pas.

Retour à l'accueil
`),wo=R('
',1);function _o(e,s){J(s,!1);let t=B("accueil"),i=B(!1);function o(){const g=window.location.pathname.slice(1)||"accueil";q(t,g),$t(l(t))}function n(g){g.detail.accepted?Ot():Bt(),q(i,!1)}he(async()=>(o(),document.addEventListener("click",g=>{const _=g.target.closest("a");if(!_||!_.href.startsWith(window.location.origin)||_.target||_.hasAttribute("download")||_.getAttribute("rel")==="external")return;g.preventDefault();const M=new URL(_.href).pathname;window.history.pushState({},"",M);const T=M.slice(1)||"accueil";q(t,T),$t(l(t))}),window.addEventListener("popstate",o),await Ci(),so()?(Ot(),Bt()):q(i,!0),()=>{window.removeEventListener("popstate",o)})),ne();var a=wo(),u=m(ri(a),2);let r;var d=c(u);Qi(d,{});var f=m(d,2),p=c(f);{var v=g=>{no(g,{})},h=(g,_)=>{{var A=T=>{lo(T,{})},M=(T,Z)=>{{var U=$=>{po($,{})},G=($,ge)=>{{var ze=ee=>{fo(ee,{})},ye=(ee,ue)=>{{var ce=H=>{ho(H,{})},Ge=(H,Pe)=>{{var be=Q=>{bo(Q,{})},je=Q=>{var ke=xo();N(Q,ke)};te(H,Q=>{l(t)==="mentions-legales"?Q(be):Q(je,!1)},Pe)}};te(ee,H=>{l(t)==="conditions-utilisation"?H(ce):H(Ge,!1)},ue)}};te($,ee=>{l(t)==="politique-confidentialite"?ee(ze):ee(ye,!1)},ge)}};te(T,$=>{l(t)==="contact"?$(U):$(G,!1)},Z)}};te(g,T=>{l(t)==="fonctionnalites"?T(A):T(M,!1)},_)}};te(p,g=>{l(t)==="accueil"?g(v):g(h,!1)})}var b=m(f,2);Xi(b,{});var w=m(u,2);{var k=g=>{to(g,{$$events:{consent:n}})};te(w,g=>{l(i)&&g(k)})}ae(g=>r=L(u,1,"flex flex-col min-h-screen relative",null,r,g),[()=>({"blur-effect":l(i)})],me),N(e,a),X()}Gi(_o,{target:document.getElementById("app")}); diff --git a/web/dist/assets/index-WF7yUNnB.css b/web/dist/assets/index-WF7yUNnB.css deleted file mode 100644 index e22b32d2..00000000 --- a/web/dist/assets/index-WF7yUNnB.css +++ /dev/null @@ -1 +0,0 @@ -*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Figtree,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.-bottom-4{bottom:-1rem}.-right-4{right:-1rem}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[-10\]{z-index:-10}.z-\[-20\]{z-index:-20}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-10{margin-bottom:2.5rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-\[300px\]{height:300px}.h-\[400px\]{height:400px}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-4{width:1rem}.w-4\/5{width:80%}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-\[150px\]{width:150px}.w-full{width:100%}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-xs{max-width:20rem}.flex-shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.translate-x-0{--tw-translate-x: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-10{--tw-translate-x: 2.5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x: 100%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-0{--tw-translate-y: 0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-10{--tw-translate-y: 2.5rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-12{gap:3rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1.5rem * var(--tw-space-x-reverse));margin-left:calc(1.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-\[\#002C66\]{--tw-border-opacity: 1;border-color:rgb(0 44 102 / var(--tw-border-opacity, 1))}.border-\[\#4CAF50\]{--tw-border-opacity: 1;border-color:rgb(76 175 80 / var(--tw-border-opacity, 1))}.border-blue-300{--tw-border-opacity: 1;border-color:rgb(147 197 253 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-green-400{--tw-border-opacity: 1;border-color:rgb(74 222 128 / var(--tw-border-opacity, 1))}.border-red-600{--tw-border-opacity: 1;border-color:rgb(220 38 38 / var(--tw-border-opacity, 1))}.bg-\[\#E3170A\]{--tw-bg-opacity: 1;background-color:rgb(227 23 10 / var(--tw-bg-opacity, 1))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity, 1))}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-900{--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-opacity-50{--tw-bg-opacity: .5}.bg-opacity-70{--tw-bg-opacity: .7}.bg-opacity-90{--tw-bg-opacity: .9}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-blue-900{--tw-gradient-from: #1e3a8a var(--tw-gradient-from-position);--tw-gradient-to: rgb(30 58 138 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-white{--tw-gradient-to: #fff var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-16{padding-top:4rem;padding-bottom:4rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pl-6{padding-left:1.5rem}.pt-20{padding-top:5rem}.pt-4{padding-top:1rem}.pt-8{padding-top:2rem}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.italic{font-style:italic}.not-italic{font-style:normal}.text-\[\#002C66\]{--tw-text-opacity: 1;color:rgb(0 44 102 / var(--tw-text-opacity, 1))}.text-\[\#4CAF50\]{--tw-text-opacity: 1;color:rgb(76 175 80 / var(--tw-text-opacity, 1))}.text-blue-200{--tw-text-opacity: 1;color:rgb(191 219 254 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-600{--tw-text-opacity: 1;color:rgb(37 99 235 / var(--tw-text-opacity, 1))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-700{--tw-text-opacity: 1;color:rgb(21 128 61 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-blue-500\/20{--tw-shadow-color: rgb(59 130 246 / .2);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.delay-300{transition-delay:.3s}.delay-500{transition-delay:.5s}.delay-700{transition-delay:.7s}.duration-300{transition-duration:.3s}.duration-700{transition-duration:.7s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}@font-face{font-family:Figtree;src:url(/fonts/Figtree-VariableFont_wght.ttf) format("truetype");font-weight:100 900;font-style:normal;font-display:swap}:root{font-family:Figtree,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;line-height:1.5;font-weight:400;color:#333;background-color:#fff;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}html,body{height:100%;margin:0;padding:0;scroll-behavior:smooth}body{min-width:320px;min-height:100vh}#app{display:flex;flex-direction:column;min-height:100vh}.aspect-w-16{position:relative;padding-bottom:calc(var(--tw-aspect-h) / var(--tw-aspect-w) * 100%);--tw-aspect-w: 16}.aspect-h-9{--tw-aspect-h: 9}.aspect-w-16>*{position:absolute;height:100%;width:100%;top:0;right:0;bottom:0;left:0}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.animate-fadeIn{animation:fadeIn .5s ease-in-out}.container{width:100%;max-width:1280px;margin-left:auto;margin-right:auto}*:focus-visible{outline:2px solid #3b82f6;outline-offset:2px}@media (max-width: 640px){h1{font-size:2rem!important}h2{font-size:1.5rem!important}h3{font-size:1.25rem!important}}.blur-effect{filter:blur(4px);pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}@keyframes slideUp{0%{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}.cookie-modal{animation:slideUp .3s ease-out forwards}.hover\:scale-105:hover{--tw-scale-x: 1.05;--tw-scale-y: 1.05;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-\[\#4CAF50\]:hover{--tw-bg-opacity: 1;background-color:rgb(76 175 80 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-200:hover{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity, 1))}.hover\:bg-blue-700:hover{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-300:hover{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.hover\:bg-red-700:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.hover\:text-blue-300:hover{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.hover\:text-blue-500:hover{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-blue-500:focus{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1))}@media (min-width: 640px){.sm\:flex-row{flex-direction:row}}@media (min-width: 768px){.md\:mb-0{margin-bottom:0}.md\:mr-6{margin-right:1.5rem}.md\:h-\[400px\]{height:400px}.md\:h-\[500px\]{height:500px}.md\:w-1\/2{width:50%}.md\:w-\[200px\]{width:200px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:justify-end{justify-content:flex-end}.md\:pr-8{padding-right:2rem}.md\:text-5xl{font-size:3rem;line-height:1}}@media (min-width: 1280px){.xl\:flex{display:flex}.xl\:hidden{display:none}} diff --git a/web/dist/favicon-16.png b/web/dist/favicon-16.png deleted file mode 100644 index f9441458..00000000 Binary files a/web/dist/favicon-16.png and /dev/null differ diff --git a/web/dist/favicon-32.png b/web/dist/favicon-32.png deleted file mode 100644 index f5dffe2c..00000000 Binary files a/web/dist/favicon-32.png and /dev/null differ diff --git a/web/dist/favicon-64.png b/web/dist/favicon-64.png deleted file mode 100644 index 53b6ae36..00000000 Binary files a/web/dist/favicon-64.png and /dev/null differ diff --git a/web/dist/favicon.png b/web/dist/favicon.png deleted file mode 100644 index f5dffe2c..00000000 Binary files a/web/dist/favicon.png and /dev/null differ diff --git a/web/dist/fonts/Figtree-VariableFont_wght.ttf b/web/dist/fonts/Figtree-VariableFont_wght.ttf deleted file mode 100644 index 06f9fe57..00000000 Binary files a/web/dist/fonts/Figtree-VariableFont_wght.ttf and /dev/null differ diff --git a/web/dist/fonts/Kallisto-Bold.otf b/web/dist/fonts/Kallisto-Bold.otf deleted file mode 100644 index fb7595a9..00000000 Binary files a/web/dist/fonts/Kallisto-Bold.otf and /dev/null differ diff --git a/web/dist/fonts/Kallisto-Medium.otf b/web/dist/fonts/Kallisto-Medium.otf deleted file mode 100644 index b0f8ad77..00000000 Binary files a/web/dist/fonts/Kallisto-Medium.otf and /dev/null differ diff --git a/web/dist/fonts/Kallisto-Thin.otf b/web/dist/fonts/Kallisto-Thin.otf deleted file mode 100644 index a94f0934..00000000 Binary files a/web/dist/fonts/Kallisto-Thin.otf and /dev/null differ diff --git a/web/dist/icons/Icon-152.png b/web/dist/icons/Icon-152.png deleted file mode 100644 index 4043aa2d..00000000 Binary files a/web/dist/icons/Icon-152.png and /dev/null differ diff --git a/web/dist/icons/Icon-167.png b/web/dist/icons/Icon-167.png deleted file mode 100644 index 1fcc514a..00000000 Binary files a/web/dist/icons/Icon-167.png and /dev/null differ diff --git a/web/dist/icons/Icon-180.png b/web/dist/icons/Icon-180.png deleted file mode 100644 index d2b40c1e..00000000 Binary files a/web/dist/icons/Icon-180.png and /dev/null differ diff --git a/web/dist/icons/Icon-192.png b/web/dist/icons/Icon-192.png deleted file mode 100644 index 34447be7..00000000 Binary files a/web/dist/icons/Icon-192.png and /dev/null differ diff --git a/web/dist/icons/Icon-512.png b/web/dist/icons/Icon-512.png deleted file mode 100644 index 058f9806..00000000 Binary files a/web/dist/icons/Icon-512.png and /dev/null differ diff --git a/web/dist/icons/Icon-maskable-192.png b/web/dist/icons/Icon-maskable-192.png deleted file mode 100644 index 34447be7..00000000 Binary files a/web/dist/icons/Icon-maskable-192.png and /dev/null differ diff --git a/web/dist/icons/Icon-maskable-512.png b/web/dist/icons/Icon-maskable-512.png deleted file mode 100644 index 058f9806..00000000 Binary files a/web/dist/icons/Icon-maskable-512.png and /dev/null differ diff --git a/web/dist/images/Logo-geosector-horizontal.svg b/web/dist/images/Logo-geosector-horizontal.svg deleted file mode 100644 index 53d598ca..00000000 --- a/web/dist/images/Logo-geosector-horizontal.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/dist/images/Logo-geosector-vertical.svg b/web/dist/images/Logo-geosector-vertical.svg deleted file mode 100644 index f9aca059..00000000 --- a/web/dist/images/Logo-geosector-vertical.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/dist/images/app-screenshot.png b/web/dist/images/app-screenshot.png deleted file mode 100644 index 03802abb..00000000 --- a/web/dist/images/app-screenshot.png +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/dist/images/geosector-icon.png b/web/dist/images/geosector-icon.png deleted file mode 100644 index 532e85c9..00000000 Binary files a/web/dist/images/geosector-icon.png and /dev/null differ diff --git a/web/dist/images/geosector-logo.png b/web/dist/images/geosector-logo.png deleted file mode 100644 index fdf990ca..00000000 Binary files a/web/dist/images/geosector-logo.png and /dev/null differ diff --git a/web/dist/images/geosector-logo.svg b/web/dist/images/geosector-logo.svg deleted file mode 100644 index e69de29b..00000000 diff --git a/web/dist/images/icon-geosector.svg b/web/dist/images/icon-geosector.svg deleted file mode 100644 index 1fbeeabb..00000000 --- a/web/dist/images/icon-geosector.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/dist/index.html b/web/dist/index.html deleted file mode 100644 index 992b88f7..00000000 --- a/web/dist/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - Geosector - Gestion efficace de vos distributions - - - - -
- - diff --git a/web/dist/vite.svg b/web/dist/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/web/dist/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/geosector-deploy.tar.gz b/web/geosector-deploy.tar.gz deleted file mode 100755 index 61175cea..00000000 Binary files a/web/geosector-deploy.tar.gz and /dev/null differ diff --git a/web/index.html b/web/index.html deleted file mode 100755 index aceddc2c..00000000 --- a/web/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - Geosector - Gestion efficace de vos distributions - - -
- - - diff --git a/web/jsconfig.json b/web/jsconfig.json deleted file mode 100755 index 5696a2de..00000000 --- a/web/jsconfig.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "compilerOptions": { - "moduleResolution": "bundler", - "target": "ESNext", - "module": "ESNext", - /** - * svelte-preprocess cannot figure out whether you have - * a value or a type, so tell TypeScript to enforce using - * `import type` instead of `import` for Types. - */ - "verbatimModuleSyntax": true, - "isolatedModules": true, - "resolveJsonModule": true, - /** - * To have warnings / errors of the Svelte compiler at the - * correct position, enable source maps by default. - */ - "sourceMap": true, - "esModuleInterop": true, - "skipLibCheck": true, - /** - * Typecheck JS in `.svelte` and `.js` files by default. - * Disable this if you'd like to use dynamic types. - */ - "checkJs": true - }, - /** - * Use global.d.ts instead of compilerOptions.types - * to avoid limiting type declarations. - */ - "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] -} diff --git a/web/livre-web.sh b/web/livre-web.sh deleted file mode 100755 index b16ddef9..00000000 --- a/web/livre-web.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/bin/bash - -# Vérification des arguments -if [ $# -ne 2 ]; then - echo "Usage: $0 " - echo "Example: $0 dva-geo rca-geo" - exit 1 -fi - -HOST_IP="195.154.80.116" -HOST_USER=root -HOST_KEY=/home/pierre/.ssh/id_rsa_mbpi -HOST_PORT=22 - -SOURCE_CONTAINER=$1 -DEST_CONTAINER=$2 -WEB_PATH="/var/www/geosector/web" -TIMESTAMP=$(date +"%Y%m%d_%H%M%S") -BACKUP_DIR="${WEB_PATH}_backup_${TIMESTAMP}" -PROJECT="default" - -echo "🔄 Copie du site web Svelte de $SOURCE_CONTAINER vers $DEST_CONTAINER (projet: $PROJECT)" - -# Vérifier si les containers existent -echo "🔍 Vérification des containers..." -ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus info $SOURCE_CONTAINER --project $PROJECT" > /dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "❌ Erreur: Le container source $SOURCE_CONTAINER n'existe pas ou n'est pas accessible dans le projet $PROJECT" - exit 1 -fi - -ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus info $DEST_CONTAINER --project $PROJECT" > /dev/null 2>&1 -if [ $? -ne 0 ]; then - echo "❌ Erreur: Le container destination $DEST_CONTAINER n'existe pas ou n'est pas accessible dans le projet $PROJECT" - exit 1 -fi - -# Créer une sauvegarde du dossier de destination avant de le remplacer -echo "📦 Création d'une sauvegarde sur $DEST_CONTAINER..." -# Vérifier si le dossier WEB existe -ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $WEB_PATH" -if [ $? -eq 0 ]; then - # Le dossier existe, créer une sauvegarde - ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- cp -r $WEB_PATH $BACKUP_DIR" - echo "✅ Sauvegarde créée dans $BACKUP_DIR" -else - echo "⚠️ Le dossier WEB n'existe pas sur la destination" -fi - -# Copier le dossier WEB entre les containers -echo "📋 Copie des fichiers en cours..." - -# Approche directe: utiliser incus copy pour copier directement entre containers -echo "📤 Transfert direct entre containers..." -# Nettoyer le dossier de destination -ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- rm -rf $WEB_PATH" -ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- mkdir -p $WEB_PATH" - -# Copier directement du container source vers le container destination -ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $SOURCE_CONTAINER --project $PROJECT -- tar -cf - -C $WEB_PATH . | incus exec $DEST_CONTAINER --project $PROJECT -- tar -xf - -C $WEB_PATH" -if [ $? -ne 0 ]; then - echo "❌ Erreur lors du transfert direct entre containers" - echo "⚠️ Tentative de restauration de la sauvegarde..." - # Vérifier si la sauvegarde existe - ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $BACKUP_DIR" - if [ $? -eq 0 ]; then - # La sauvegarde existe, la restaurer - ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- cp -r $BACKUP_DIR $WEB_PATH" - echo "✅ Restauration réussie" - else - echo "❌ Échec de la restauration" - fi - exit 1 -fi - -# Changer le propriétaire et les permissions des fichiers -echo "👤 Application des droits et permissions pour tous les fichiers..." - -# Définir le propriétaire pour tous les fichiers -ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- chown -R nginx:nginx $WEB_PATH" - -# Appliquer les permissions de base pour les dossiers (755) -ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- find $WEB_PATH -type d -exec chmod 755 {} \;" - -# Appliquer les permissions pour les fichiers (644) -ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- find $WEB_PATH -type f -exec chmod 644 {} \;" - -echo "✅ Propriétaire et permissions appliqués avec succès" - -# Vérifier la copie -echo "✅ Vérification de la copie..." -ssh -i $HOST_KEY -p $HOST_PORT $HOST_USER@$HOST_IP "incus exec $DEST_CONTAINER --project $PROJECT -- test -d $WEB_PATH" -if [ $? -eq 0 ]; then - echo "✅ Copie réussie" -else - echo "❌ Erreur: Le dossier WEB n'a pas été copié correctement" -fi - -echo "✅ Opération terminée! Le site web Svelte a été copié de $SOURCE_CONTAINER vers $DEST_CONTAINER" -echo "📁 Une sauvegarde a été créée dans $BACKUP_DIR sur $DEST_CONTAINER" -echo "👤 Les fichiers appartiennent maintenant à l'utilisateur nginx" diff --git a/web/package-lock.json b/web/package-lock.json deleted file mode 100755 index f8d487b3..00000000 --- a/web/package-lock.json +++ /dev/null @@ -1,2847 +0,0 @@ -{ - "name": "geosector", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "geosector", - "version": "0.0.0", - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.3", - "autoprefixer": "^10.4.20", - "svelte": "^5.23.1", - "tailwindcss": "^3.4.9", - "vite": "^6.3.1" - } - }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.2.tgz", - "integrity": "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.2.tgz", - "integrity": "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.2.tgz", - "integrity": "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.2.tgz", - "integrity": "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.2.tgz", - "integrity": "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.2.tgz", - "integrity": "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.2.tgz", - "integrity": "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.2.tgz", - "integrity": "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.2.tgz", - "integrity": "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.2.tgz", - "integrity": "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.2.tgz", - "integrity": "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.2.tgz", - "integrity": "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.2.tgz", - "integrity": "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.2.tgz", - "integrity": "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.2.tgz", - "integrity": "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.2.tgz", - "integrity": "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.2.tgz", - "integrity": "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.2.tgz", - "integrity": "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.2.tgz", - "integrity": "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.2.tgz", - "integrity": "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", - "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^8.9.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.0.tgz", - "integrity": "sha512-wojIS/7GYnJDYIg1higWj2ROA6sSRWvcR1PO/bqEyFr/5UZah26c8Cz4u0NaqjPeVltzsVpt2Tm8d2io0V+4Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", - "debug": "^4.4.1", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.17", - "vitefu": "^1.0.6" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "svelte": "^5.0.0", - "vite": "^6.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", - "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.7" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "svelte": "^5.0.0", - "vite": "^6.0.0" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.179", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.179.tgz", - "integrity": "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/esm-env": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/esrap": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", - "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.6" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.44.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.2.tgz", - "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.44.2", - "@rollup/rollup-android-arm64": "4.44.2", - "@rollup/rollup-darwin-arm64": "4.44.2", - "@rollup/rollup-darwin-x64": "4.44.2", - "@rollup/rollup-freebsd-arm64": "4.44.2", - "@rollup/rollup-freebsd-x64": "4.44.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.44.2", - "@rollup/rollup-linux-arm-musleabihf": "4.44.2", - "@rollup/rollup-linux-arm64-gnu": "4.44.2", - "@rollup/rollup-linux-arm64-musl": "4.44.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.44.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.44.2", - "@rollup/rollup-linux-riscv64-gnu": "4.44.2", - "@rollup/rollup-linux-riscv64-musl": "4.44.2", - "@rollup/rollup-linux-s390x-gnu": "4.44.2", - "@rollup/rollup-linux-x64-gnu": "4.44.2", - "@rollup/rollup-linux-x64-musl": "4.44.2", - "@rollup/rollup-win32-arm64-msvc": "4.44.2", - "@rollup/rollup-win32-ia32-msvc": "4.44.2", - "@rollup/rollup-win32-x64-msvc": "4.44.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svelte": { - "version": "5.35.2", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.35.2.tgz", - "integrity": "sha512-uW/rRXYrhZ7Dh4UQNZ0t+oVGL1dEM+95GavCO8afAk1IY2cPq9BcZv9C3um5aLIya2y8lIeLPxLII9ASGg9Dzw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.5", - "@types/estree": "^1.0.5", - "acorn": "^8.12.1", - "aria-query": "^5.3.1", - "axobject-query": "^4.1.0", - "clsx": "^2.1.1", - "esm-env": "^1.2.1", - "esrap": "^2.1.0", - "is-reference": "^3.0.3", - "locate-character": "^3.0.0", - "magic-string": "^0.30.11", - "zimmerframe": "^1.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tailwindcss": { - "version": "3.4.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", - "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.6", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vitefu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", - "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", - "dev": true, - "license": "MIT", - "workspaces": [ - "tests/deps/*", - "tests/projects/*", - "tests/projects/workspace/packages/*" - ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "dev": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/zimmerframe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", - "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", - "dev": true, - "license": "MIT" - } - } -} diff --git a/web/package.json b/web/package.json deleted file mode 100755 index 872c25aa..00000000 --- a/web/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "geosector", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.3", - "autoprefixer": "^10.4.20", - "svelte": "^5.23.1", - "tailwindcss": "^3.4.9", - "vite": "^6.3.1" - } -} diff --git a/web/postcss.config.js b/web/postcss.config.js deleted file mode 100755 index ba807304..00000000 --- a/web/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {} - } -}; diff --git a/web/public/.htaccess b/web/public/.htaccess deleted file mode 100755 index 02e9d601..00000000 --- a/web/public/.htaccess +++ /dev/null @@ -1,12 +0,0 @@ -# Configuration pour le mode histoire (HTML5 History API) - - RewriteEngine On - RewriteBase / - - # Si le fichier ou répertoire demandé existe, servir directement - RewriteCond %{REQUEST_FILENAME} !-f - RewriteCond %{REQUEST_FILENAME} !-d - - # Sinon, rediriger vers index.html pour permettre au routeur client de gérer - RewriteRule . /index.html [L] - diff --git a/web/public/favicon-16.png b/web/public/favicon-16.png deleted file mode 100755 index f9441458..00000000 Binary files a/web/public/favicon-16.png and /dev/null differ diff --git a/web/public/favicon-32.png b/web/public/favicon-32.png deleted file mode 100755 index f5dffe2c..00000000 Binary files a/web/public/favicon-32.png and /dev/null differ diff --git a/web/public/favicon-64.png b/web/public/favicon-64.png deleted file mode 100755 index 53b6ae36..00000000 Binary files a/web/public/favicon-64.png and /dev/null differ diff --git a/web/public/favicon.png b/web/public/favicon.png deleted file mode 100755 index f5dffe2c..00000000 Binary files a/web/public/favicon.png and /dev/null differ diff --git a/web/public/fonts/Figtree-VariableFont_wght.ttf b/web/public/fonts/Figtree-VariableFont_wght.ttf deleted file mode 100755 index 06f9fe57..00000000 Binary files a/web/public/fonts/Figtree-VariableFont_wght.ttf and /dev/null differ diff --git a/web/public/fonts/Kallisto-Bold.otf b/web/public/fonts/Kallisto-Bold.otf deleted file mode 100755 index fb7595a9..00000000 Binary files a/web/public/fonts/Kallisto-Bold.otf and /dev/null differ diff --git a/web/public/fonts/Kallisto-Medium.otf b/web/public/fonts/Kallisto-Medium.otf deleted file mode 100755 index b0f8ad77..00000000 Binary files a/web/public/fonts/Kallisto-Medium.otf and /dev/null differ diff --git a/web/public/fonts/Kallisto-Thin.otf b/web/public/fonts/Kallisto-Thin.otf deleted file mode 100755 index a94f0934..00000000 Binary files a/web/public/fonts/Kallisto-Thin.otf and /dev/null differ diff --git a/web/public/icons/Icon-152.png b/web/public/icons/Icon-152.png deleted file mode 100755 index 4043aa2d..00000000 Binary files a/web/public/icons/Icon-152.png and /dev/null differ diff --git a/web/public/icons/Icon-167.png b/web/public/icons/Icon-167.png deleted file mode 100755 index 1fcc514a..00000000 Binary files a/web/public/icons/Icon-167.png and /dev/null differ diff --git a/web/public/icons/Icon-180.png b/web/public/icons/Icon-180.png deleted file mode 100755 index d2b40c1e..00000000 Binary files a/web/public/icons/Icon-180.png and /dev/null differ diff --git a/web/public/icons/Icon-192.png b/web/public/icons/Icon-192.png deleted file mode 100755 index 34447be7..00000000 Binary files a/web/public/icons/Icon-192.png and /dev/null differ diff --git a/web/public/icons/Icon-512.png b/web/public/icons/Icon-512.png deleted file mode 100755 index 058f9806..00000000 Binary files a/web/public/icons/Icon-512.png and /dev/null differ diff --git a/web/public/icons/Icon-maskable-192.png b/web/public/icons/Icon-maskable-192.png deleted file mode 100755 index 34447be7..00000000 Binary files a/web/public/icons/Icon-maskable-192.png and /dev/null differ diff --git a/web/public/icons/Icon-maskable-512.png b/web/public/icons/Icon-maskable-512.png deleted file mode 100755 index 058f9806..00000000 Binary files a/web/public/icons/Icon-maskable-512.png and /dev/null differ diff --git a/web/public/images/Logo-geosector-horizontal.svg b/web/public/images/Logo-geosector-horizontal.svg deleted file mode 100755 index 53d598ca..00000000 --- a/web/public/images/Logo-geosector-horizontal.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/public/images/Logo-geosector-vertical.svg b/web/public/images/Logo-geosector-vertical.svg deleted file mode 100755 index f9aca059..00000000 --- a/web/public/images/Logo-geosector-vertical.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/public/images/app-screenshot.png b/web/public/images/app-screenshot.png deleted file mode 100755 index 03802abb..00000000 --- a/web/public/images/app-screenshot.png +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/public/images/geosector-icon.png b/web/public/images/geosector-icon.png deleted file mode 100755 index 532e85c9..00000000 Binary files a/web/public/images/geosector-icon.png and /dev/null differ diff --git a/web/public/images/geosector-logo.pdf b/web/public/images/geosector-logo.pdf deleted file mode 100755 index 1a863038..00000000 Binary files a/web/public/images/geosector-logo.pdf and /dev/null differ diff --git a/web/public/images/geosector-logo.png b/web/public/images/geosector-logo.png deleted file mode 100755 index fdf990ca..00000000 Binary files a/web/public/images/geosector-logo.png and /dev/null differ diff --git a/web/public/images/geosector-logo.svg b/web/public/images/geosector-logo.svg deleted file mode 100755 index e69de29b..00000000 diff --git a/web/public/images/icon-geosector.jpg b/web/public/images/icon-geosector.jpg deleted file mode 100755 index f899eb4e..00000000 Binary files a/web/public/images/icon-geosector.jpg and /dev/null differ diff --git a/web/public/images/icon-geosector.pdf b/web/public/images/icon-geosector.pdf deleted file mode 100755 index 97c8eb4c..00000000 Binary files a/web/public/images/icon-geosector.pdf and /dev/null differ diff --git a/web/public/images/icon-geosector.png b/web/public/images/icon-geosector.png deleted file mode 100755 index 952b79d4..00000000 Binary files a/web/public/images/icon-geosector.png and /dev/null differ diff --git a/web/public/images/icon-geosector.svg b/web/public/images/icon-geosector.svg deleted file mode 100755 index 1fbeeabb..00000000 --- a/web/public/images/icon-geosector.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/web/public/vite.svg b/web/public/vite.svg deleted file mode 100755 index e7b8dfb1..00000000 --- a/web/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/App.svelte b/web/src/App.svelte deleted file mode 100755 index 68e8b378..00000000 --- a/web/src/App.svelte +++ /dev/null @@ -1,148 +0,0 @@ - - -
-
- - - - - - -
-
- -
-
- -
- {#if activePage === 'accueil'} - - {:else if activePage === 'fonctionnalites'} - - {:else if activePage === 'contact'} - - {:else if activePage === 'politique-confidentialite'} - - {:else if activePage === 'conditions-utilisation'} - - {:else if activePage === 'mentions-legales'} - - {:else} - -
-

Page non trouvée

-

La page que vous recherchez n'existe pas.

- Retour à l'accueil -
- {/if} -
- -
-
- -{#if showCookieConsent} - -{/if} diff --git a/web/src/app.css b/web/src/app.css deleted file mode 100755 index 8e961f12..00000000 --- a/web/src/app.css +++ /dev/null @@ -1,116 +0,0 @@ -@import "tailwindcss/base"; -@import "tailwindcss/components"; -@import "tailwindcss/utilities"; - -@font-face { - font-family: 'Figtree'; - src: url('/fonts/Figtree-VariableFont_wght.ttf') format('truetype'); - font-weight: 100 900; - font-style: normal; - font-display: swap; -} - -:root { - font-family: 'Figtree', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - line-height: 1.5; - font-weight: 400; - color: #333333; - background-color: #ffffff; - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -html, body { - height: 100%; - margin: 0; - padding: 0; - scroll-behavior: smooth; -} - -body { - min-width: 320px; - min-height: 100vh; -} - -#app { - display: flex; - flex-direction: column; - min-height: 100vh; -} - -/* Aspect ratio utilities */ -.aspect-w-16 { - position: relative; - padding-bottom: calc(var(--tw-aspect-h) / var(--tw-aspect-w) * 100%); - --tw-aspect-w: 16; -} - -.aspect-h-9 { - --tw-aspect-h: 9; -} - -.aspect-w-16 > * { - position: absolute; - height: 100%; - width: 100%; - top: 0; - right: 0; - bottom: 0; - left: 0; -} - -/* Animation utilities */ -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -.animate-fadeIn { - animation: fadeIn 0.5s ease-in-out; -} - -/* Custom container for sections */ -.container { - width: 100%; - max-width: 1280px; - margin-left: auto; - margin-right: auto; -} - -/* Custom focus styles */ -*:focus-visible { - outline: 2px solid #3b82f6; - outline-offset: 2px; -} - -/* Responsive typography */ -@media (max-width: 640px) { - h1 { - font-size: 2rem !important; - } - h2 { - font-size: 1.5rem !important; - } - h3 { - font-size: 1.25rem !important; - } -} - -/* Cookie consent modal styles */ -.blur-effect { - filter: blur(4px); - pointer-events: none; - user-select: none; -} - -/* Animation pour la modal de cookies */ -@keyframes slideUp { - from { transform: translateY(20px); opacity: 0; } - to { transform: translateY(0); opacity: 1; } -} - -.cookie-modal { - animation: slideUp 0.3s ease-out forwards; -} diff --git a/web/src/assets/svelte.svg b/web/src/assets/svelte.svg deleted file mode 100755 index c5e08481..00000000 --- a/web/src/assets/svelte.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/web/src/components/CookieConsent.svelte b/web/src/components/CookieConsent.svelte deleted file mode 100755 index 03cfde46..00000000 --- a/web/src/components/CookieConsent.svelte +++ /dev/null @@ -1,58 +0,0 @@ - - -
- -
diff --git a/web/src/components/Footer.svelte b/web/src/components/Footer.svelte deleted file mode 100755 index f54a185d..00000000 --- a/web/src/components/Footer.svelte +++ /dev/null @@ -1,103 +0,0 @@ - - - diff --git a/web/src/components/Header.svelte b/web/src/components/Header.svelte deleted file mode 100755 index 35d9e7bc..00000000 --- a/web/src/components/Header.svelte +++ /dev/null @@ -1,194 +0,0 @@ - - -
- - - - - - - {#if mobileMenuOpen} -
e.key === 'Escape' && closeMobileMenu()} role="button" tabindex="0" aria-label="Fermer le menu">
- {/if} -
diff --git a/web/src/lib/Counter.svelte b/web/src/lib/Counter.svelte deleted file mode 100755 index 770c9226..00000000 --- a/web/src/lib/Counter.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/web/src/lib/analyticsService.js b/web/src/lib/analyticsService.js deleted file mode 100755 index 6aad7359..00000000 --- a/web/src/lib/analyticsService.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Service d'analyse pour Geosector - * Ce service permet de suivre l'activité des utilisateurs qui ont accepté les cookies - */ - -import { getCookie } from './cookieService.js'; - -// Clé pour le stockage local du dernier suivi -const LAST_TRACKING_KEY = 'geosector_last_tracking'; - -// Fonction pour enregistrer une visite de page -export function trackPageView(page) { - // Ne suivre que si l'utilisateur a accepté les cookies - if (getCookie('geosector_cookies_accepted') !== 'true') { - console.log('Suivi désactivé : cookies non acceptés'); - return; - } - - // Vérifier si nous devons suivre aujourd'hui (tous les 2 jours) - if (!shouldTrackToday()) { - console.log('Suivi différé : déjà suivi dans les 2 derniers jours'); - return; - } - - // Enregistrer la date du dernier suivi - localStorage.setItem(LAST_TRACKING_KEY, new Date().toISOString()); - - // Ici, vous pouvez implémenter le code réel pour envoyer les données d'analyse - // Par exemple, une requête fetch vers votre propre endpoint d'analyse - console.log(`Page consultée: ${page} - ${new Date().toISOString()}`); - - // Exemple d'implémentation avec un endpoint d'analyse - /* - fetch('/api/analytics/pageview', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - page, - timestamp: new Date().toISOString(), - // Autres données anonymes comme la source de trafic, etc. - referrer: document.referrer || 'direct', - screenSize: `${window.innerWidth}x${window.innerHeight}`, - // Un identifiant de session anonyme (pas d'information personnelle) - sessionId: getAnonymousSessionId() - }), - }).catch(error => { - console.error('Erreur lors du suivi', error); - }); - */ -} - -// Fonction pour vérifier si nous devons suivre aujourd'hui -function shouldTrackToday() { - const lastTracking = localStorage.getItem(LAST_TRACKING_KEY); - - if (!lastTracking) { - return true; // Première visite, nous devons suivre - } - - const lastTrackingDate = new Date(lastTracking); - const now = new Date(); - - // Calculer la différence en millisecondes - const diffTime = Math.abs(now - lastTrackingDate); - // Convertir en jours - const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - - // Ne suivre que tous les 2 jours - return diffDays >= 2; -} - -// Générer un ID de session anonyme -function getAnonymousSessionId() { - // Vérifier si nous avons déjà un ID de session - let sessionId = sessionStorage.getItem('anonymous_session_id'); - - if (!sessionId) { - // Créer un ID de session aléatoire - sessionId = Math.random().toString(36).substring(2, 15); - sessionStorage.setItem('anonymous_session_id', sessionId); - } - - return sessionId; -} - -// Fonction pour suivre un événement spécifique -export function trackEvent(category, action, label = null) { - // Ne suivre que si l'utilisateur a accepté les cookies - if (getCookie('geosector_cookies_accepted') !== 'true') { - return; - } - - // Vérifier si nous devons suivre aujourd'hui - if (!shouldTrackToday()) { - return; - } - - console.log(`Événement: ${category} - ${action} - ${label || 'N/A'}`); - - // Exemple d'implémentation avec un endpoint d'analyse - /* - fetch('/api/analytics/event', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - category, - action, - label, - timestamp: new Date().toISOString(), - sessionId: getAnonymousSessionId() - }), - }).catch(error => { - console.error('Erreur lors du suivi d\'événement', error); - }); - */ -} diff --git a/web/src/lib/cookieService.js b/web/src/lib/cookieService.js deleted file mode 100755 index d7cef4c5..00000000 --- a/web/src/lib/cookieService.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Service de gestion des cookies pour Geosector - */ - -// Vérifie si un cookie existe -export function getCookie(name) { - const cookieValue = document.cookie - .split('; ') - .find(row => row.startsWith(`${name}=`)); - - if (cookieValue) { - return cookieValue.split('=')[1]; - } - - return null; -} - -// Vérifie si l'utilisateur a déjà fait un choix concernant les cookies -export function hasUserMadeCookieChoice() { - return getCookie('geosector_cookies_accepted') !== null || - getCookie('geosector_cookies_refused') !== null; -} - -// Démarre le suivi anonyme si l'utilisateur a accepté les cookies -export function startAnonymousTracking() { - if (getCookie('geosector_cookies_accepted') === 'true') { - // Implémentation du suivi anonyme ici - // Par exemple, initialisation de Google Analytics ou d'un autre service de suivi - - // Enregistrer la page actuelle (peut être utilisé dans d'autres composants) - const currentPage = window.location.hash.slice(1) || 'accueil'; - console.log('Suivi anonyme activé - ' + new Date().toISOString()); - console.log('Page courante: ' + currentPage); - - // Si vous utilisez notre service analyticsService: - // import { trackPageView } from './analyticsService.js'; - // trackPageView(currentPage); - - // On pourrait aussi initialiser Google Analytics ici : - // if (window.gtag) { - // window.gtag('consent', 'update', { - // 'analytics_storage': 'granted' - // }); - // } - } -} - -// Empêche le suivi si l'utilisateur a refusé les cookies -export function stopAnonymousTracking() { - if (getCookie('geosector_cookies_refused') === 'true') { - // Désactiver tout suivi - console.log('Suivi anonyme désactivé - ' + new Date().toISOString()); - - // Si on utilisait Google Analytics : - // if (window.gtag) { - // window.gtag('consent', 'update', { - // 'analytics_storage': 'denied' - // }); - // } - } -} diff --git a/web/src/main.js b/web/src/main.js deleted file mode 100755 index 458c7a8a..00000000 --- a/web/src/main.js +++ /dev/null @@ -1,9 +0,0 @@ -import { mount } from 'svelte' -import './app.css' -import App from './App.svelte' - -const app = mount(App, { - target: document.getElementById('app'), -}) - -export default app diff --git a/web/src/pages/Accueil.svelte b/web/src/pages/Accueil.svelte deleted file mode 100755 index bd813031..00000000 --- a/web/src/pages/Accueil.svelte +++ /dev/null @@ -1,134 +0,0 @@ - - -
- -
-
-
-
-

Gestion efficace de vos distributions de calendriers

-

Une application puissante et intuitive pour optimiser vos tournées et améliorer votre productivité.

- -
- - -
-
-
- -
- - - -

Dashboard Geosector

-

Interface de gestion

-
-
-
- - -
-
- -
- - - -

Mobile App

-

Interface mobile

-
-
-
-
-
-
-
- - -
-
-
-

Pourquoi choisir Geosector ?

- -
-
-
- - - -
-

Optimisation des tournées

-

Grace au mode Terrain, Geosector aide le membre à traiter les adresses à finaliser proche de lui.

-
- -
-
- - - -
-

Simplicité d'utilisation

-

Interface intuitive conçue pour faciliter la gestion quotidienne de vos distributions.

-
- -
-
- - - -
-

Sécurité des données

-

Vos données sont protégées en conformité au RGPD et sauvegardées régulièrement.

-
-
-
-
-
- - -
-
-
-

Ce que nos clients disent

- -
-
-
-
- TP -
-
-

Trystan PAPIN

-

Trésorier de l'amicale des SP du Malesherbois

-
-
-

"Bonjour, Je confirme l’utilisation de l’application Geosector pour l’amicale des SP de Malesherbes. Superbe application encore merci à vous !"

-
- -
-
-
- ML -
-
-

Marie Leroy

-

Responsable opérations, LogiExpress

-
-
-

"L'interface intuitive de Geosector nous a permis de former rapidement nos équipes. La visualisation en temps réel des tournées est un atout majeur pour notre activité quotidienne."

-
-
-
-
-
-
diff --git a/web/src/pages/ConditionsUtilisation.svelte b/web/src/pages/ConditionsUtilisation.svelte deleted file mode 100755 index 9f801698..00000000 --- a/web/src/pages/ConditionsUtilisation.svelte +++ /dev/null @@ -1,310 +0,0 @@ - - -
- -
-
-
-

- Conditions d'utilisation -

-

- Règles et modalités d'utilisation de l'application Geosector -

-
-
-
- - -
-
-
-
-

Dernière mise à jour : {new Date().toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' })}

- -

1. Préambule

-

- Les présentes conditions générales d'utilisation (ci-après dénommées "CGU") régissent l'utilisation de l'application Geosector (ci-après dénommée l'"Application"), - accessible via le Web à l'adresse app.geosector.fr, ainsi que sur les plateformes iOS (Apple App Store) et Android (Google Play Store). -

-

- Geosector est une application dédiée à la gestion de secteurs géographiques et de passages, permettant à ses utilisateurs d'optimiser - leurs distributions et tournées. L'application est exploitée par [Nom de la société], dont le siège social est situé à [Adresse complète], - immatriculée au Registre du Commerce et des Sociétés de [Ville] sous le numéro [Numéro RCS]. -

-

- En utilisant notre Application, vous acceptez de vous conformer aux présentes CGU. Si vous n'acceptez pas ces conditions, - veuillez ne pas utiliser l'Application. -

- -

2. Définitions

-

Dans les présentes CGU, les termes suivants ont la signification qui leur est attribuée ci-dessous :

-
    -
  • "Application" désigne l'application Geosector, accessible via le Web, iOS et Android.
  • -
  • "Compte" désigne l'espace personnel de l'Utilisateur sur l'Application.
  • -
  • "Contenu" désigne toutes les informations et données (y compris les textes, images, vidéos, etc.) accessibles ou générées via l'Application.
  • -
  • "Fonctionnalités" désigne les services et outils proposés par l'Application.
  • -
  • "Utilisateur" désigne toute personne physique ou morale ayant accès à l'Application.
  • -
  • "Données Personnelles" désigne toute information se rapportant à une personne physique identifiée ou identifiable.
  • -
- -

3. Inscription et compte utilisateur

- -

3.1 Conditions d'inscription

-

- Pour utiliser l'ensemble des Fonctionnalités de l'Application, l'Utilisateur doit créer un Compte en fournissant - les informations requises. L'Utilisateur s'engage à fournir des informations exactes, complètes et à jour. - Toute fausse déclaration peut entraîner la suspension ou la suppression du Compte. -

- -

3.2 Sécurité du compte

-

- L'Utilisateur est responsable de la confidentialité de ses identifiants de connexion (nom d'utilisateur et mot de passe) - et s'engage à ne pas les communiquer à des tiers. Toute connexion effectuée en utilisant les identifiants de l'Utilisateur - sera présumée avoir été effectuée par celui-ci. -

- -

3.3 Suspension ou suppression de compte

-

- Geosector se réserve le droit de suspendre ou de supprimer un Compte en cas de : -

-
    -
  • Non-respect des présentes CGU
  • -
  • Inactivité prolongée
  • -
  • Utilisation frauduleuse ou abusive de l'Application
  • -
  • Non-paiement des services payants
  • -
  • Demande de l'Utilisateur
  • -
- -

4. Utilisation de l'Application

- -

4.1 Licence d'utilisation

-

- Sous réserve du respect des présentes CGU, Geosector accorde à l'Utilisateur une licence limitée, non exclusive, - non transférable et révocable pour accéder et utiliser l'Application à des fins professionnelles ou personnelles. -

- -

4.2 Restrictions d'utilisation

-

L'Utilisateur s'engage à ne pas :

-
    -
  • Utiliser l'Application à des fins illégales ou interdites par les présentes CGU
  • -
  • Tenter de perturber le fonctionnement de l'Application ou d'accéder aux données d'autres Utilisateurs
  • -
  • Utiliser des robots, spiders, scrapers ou autres moyens automatisés pour accéder à l'Application
  • -
  • Contourner les mesures de sécurité de l'Application
  • -
  • Reproduire, copier, vendre, revendre ou exploiter toute partie de l'Application sans autorisation écrite préalable
  • -
  • Utiliser l'Application d'une manière qui pourrait endommager, désactiver, surcharger ou altérer les serveurs ou les réseaux
  • -
- -

4.3 Contenu de l'Utilisateur

-

- En publiant, téléchargeant, ou partageant du Contenu via l'Application, l'Utilisateur accorde à Geosector une licence mondiale, - non exclusive, transférable, libre de redevances pour utiliser, reproduire, modifier, adapter, publier, traduire et distribuer ce Contenu - dans le cadre de l'exploitation et de l'amélioration de l'Application. -

-

- L'Utilisateur garantit qu'il dispose des droits nécessaires sur le Contenu qu'il partage et que ce Contenu n'enfreint pas - les droits de tiers ni les lois applicables. -

- -

5. Services payants et abonnements

- -

5.1 Offres et tarifs

-

- Certaines Fonctionnalités de l'Application peuvent être soumises à paiement. Les offres et tarifs sont disponibles sur le site web - de Geosector ou directement dans l'Application. Geosector se réserve le droit de modifier ses offres et tarifs à tout moment, - moyennant un préavis raisonnable. -

- -

5.2 Paiement et facturation

-

- Les paiements sont effectués par carte bancaire ou tout autre moyen proposé dans l'Application. Pour les abonnements, - le paiement est automatiquement renouvelé à la fin de chaque période, sauf résiliation par l'Utilisateur avant la date de renouvellement. -

-

- Une facture électronique est mise à disposition de l'Utilisateur pour chaque paiement effectué. -

- -

5.3 Politique de remboursement

-

- Conformément à la législation applicable, l'Utilisateur bénéficie d'un droit de rétractation de 14 jours à compter de la souscription - à un service payant, sauf si l'exécution du service a commencé avec son accord avant la fin de ce délai. -

-

- Aucun remboursement ne sera accordé après l'expiration du délai de rétractation, sauf en cas de dysfonctionnement majeur de l'Application - imputable à Geosector. -

- -

6. Propriété intellectuelle

- -

6.1 Droits de Geosector

-

- L'Application, y compris son contenu, sa structure, ses fonctionnalités, son code source, ses interfaces, son design, - ses logos et ses marques, est la propriété exclusive de Geosector ou de ses concédants de licence. - Ces éléments sont protégés par les lois relatives à la propriété intellectuelle. -

- -

6.2 Droits des Utilisateurs

-

- L'Utilisateur conserve tous les droits de propriété intellectuelle sur le Contenu qu'il crée et partage via l'Application, - sous réserve de la licence accordée à Geosector conformément à l'article 4.3. -

- -

6.3 Signalement d'une violation

-

- Si vous pensez que votre contenu a été utilisé d'une manière qui constitue une violation de vos droits de propriété intellectuelle, - veuillez nous contacter à l'adresse suivante : [adresse email]. -

- -

7. Confidentialité et données personnelles

-

- La collecte et le traitement des Données Personnelles des Utilisateurs sont régis par notre Politique de Confidentialité, - disponible à l'adresse suivante : handleNavigation('politique-confidentialite')}>Politique de confidentialité. -

- -

8. Limitation de responsabilité

- -

8.1 Disponibilité de l'Application

-

- Geosector s'efforce de maintenir l'Application accessible 24 heures sur 24 et 7 jours sur 7. Cependant, l'accès peut être - temporairement suspendu, sans préavis, en raison de maintenance technique, de mise à jour ou pour toute autre raison. -

-

- Geosector ne peut être tenu responsable de tout dommage résultant de l'indisponibilité temporaire de l'Application. -

- -

8.2 Contenus et services tiers

-

- L'Application peut contenir des liens vers des sites web ou services tiers. Geosector n'exerce aucun contrôle sur ces sites et services - et n'assume aucune responsabilité quant à leur contenu ou leurs pratiques. -

- -

8.3 Limitation générale de responsabilité

-

- Dans toute la mesure permise par la loi applicable, Geosector ne pourra être tenu responsable de tout dommage indirect, - spécial, accessoire, consécutif ou punitif, y compris les pertes de profits, de revenus, de données ou d'opportunités commerciales, - résultant de l'utilisation ou de l'impossibilité d'utiliser l'Application. -

-

- La responsabilité totale de Geosector envers l'Utilisateur pour toute réclamation découlant des présentes CGU ne pourra excéder - le montant payé par l'Utilisateur à Geosector au cours des douze (12) mois précédant le fait générateur de la responsabilité. -

- -

9. Modifications des CGU

-

- Geosector se réserve le droit de modifier les présentes CGU à tout moment. Les Utilisateurs seront informés des modifications - par le biais d'une notification dans l'Application ou par e-mail. -

-

- Les modifications prendront effet à la date indiquée dans la notification. En continuant à utiliser l'Application après cette date, - l'Utilisateur accepte les CGU modifiées. -

-

- Si l'Utilisateur n'accepte pas les modifications, il doit cesser d'utiliser l'Application et, le cas échéant, supprimer son Compte. -

- -

10. Résiliation

- -

10.1 Résiliation par l'Utilisateur

-

- L'Utilisateur peut, à tout moment, cesser d'utiliser l'Application et supprimer son Compte en suivant la procédure prévue à cet effet - dans les paramètres de l'Application. -

- -

10.2 Résiliation par Geosector

-

- Geosector peut, à sa discrétion, suspendre ou résilier l'accès de l'Utilisateur à l'Application en cas de violation des présentes CGU, - sans préjudice de tout autre droit ou recours. -

- -

10.3 Conséquences de la résiliation

-

- En cas de résiliation, l'Utilisateur perd l'accès à son Compte et à toutes les Fonctionnalités de l'Application. - Les sections des présentes CGU relatives à la propriété intellectuelle, à la limitation de responsabilité et au règlement des litiges - survivront à la résiliation. -

- -

11. Dispositions spécifiques aux applications mobiles

- -

11.1 Application iOS (Apple App Store)

-

- Si vous téléchargez l'Application via l'App Store d'Apple, vous reconnaissez et acceptez que : -

-
    -
  • Ces CGU sont conclues entre vous et Geosector, et non avec Apple
  • -
  • Apple n'a aucune obligation de fournir des services de maintenance ou d'assistance concernant l'Application
  • -
  • En cas de non-conformité de l'Application avec une garantie applicable, vous pouvez en informer Apple, qui pourra vous rembourser le prix d'achat
  • -
  • Apple n'est pas responsable du traitement des réclamations ou de la responsabilité liée à l'Application
  • -
  • En cas de réclamation d'un tiers selon laquelle l'Application enfreint ses droits de propriété intellectuelle, Apple n'est pas responsable de l'enquête, de la défense, du règlement et de la décharge de cette réclamation
  • -
  • Vous devez vous conformer aux conditions d'utilisation de l'App Store d'Apple lors de l'utilisation de l'Application
  • -
- -

11.2 Application Android (Google Play)

-

- Si vous téléchargez l'Application via Google Play, vous reconnaissez et acceptez que : -

-
    -
  • Ces CGU sont conclues entre vous et Geosector, et non avec Google
  • -
  • L'utilisation de l'Application doit respecter les conditions d'utilisation de Google Play
  • -
  • Google n'a aucune obligation de fournir des services de maintenance ou d'assistance concernant l'Application
  • -
- -

12. Dispositions diverses

- -

12.1 Droit applicable et juridiction compétente

-

- Les présentes CGU sont régies par le droit français. Tout litige relatif à leur interprétation ou à leur exécution relève, - à défaut d'accord amiable, de la compétence exclusive des tribunaux français compétents. -

- -

12.2 Indépendance des clauses

-

- Si une ou plusieurs dispositions des présentes CGU sont tenues pour non valides ou déclarées comme telles en application d'une loi, - d'un règlement ou à la suite d'une décision définitive d'une juridiction compétente, les autres stipulations garderont toute leur force - et leur portée. -

- -

12.3 Non-renonciation

-

- Le fait pour Geosector de ne pas se prévaloir d'un manquement de l'Utilisateur à l'une quelconque des obligations visées dans les présentes CGU - ne saurait être interprété comme une renonciation à s'en prévaloir ultérieurement. -

- -

12.4 Communication

-

- Toute notification ou communication dans le cadre des présentes CGU doit être adressée à Geosector par e-mail à l'adresse suivante : - [adresse email] ou par courrier postal à l'adresse suivante : [adresse postale]. -

- -

13. Contact

-

- Pour toute question concernant les présentes CGU, veuillez nous contacter à : -

-

- Geosector
- E-mail : support@geosector.fr
- Adresse : [Adresse de l'entreprise]
- Téléphone : +33 (0)1 23 45 67 89 -

-
-
-
-
-
diff --git a/web/src/pages/Contact.svelte b/web/src/pages/Contact.svelte deleted file mode 100755 index 42d67f02..00000000 --- a/web/src/pages/Contact.svelte +++ /dev/null @@ -1,200 +0,0 @@ - - -
- -
-
-
-

Contactez-nous

-

Notre équipe est à votre disposition pour répondre à toutes vos questions et vous accompagner dans votre projet.

-
-
-
- - -
-
-
-
-
- -
-

Nos coordonnées

- -
-
-
- - - -
-
-

Téléphone

-

+33 (0)1 23 45 67 89

-
-
- -
-
- - - -
-
-

Email

-

contact@geosector.fr

-
-
- - -
-
- - - -
-
-

Horaires d'ouverture

-

Lundi - Vendredi: 9h00 - 18h00
Samedi - Dimanche: Fermé

-
-
-
- -
-

Suivez-nous

- -
-
- - -
-

Envoyez-nous un message

- - {#if formSubmitted} -
-

Message envoyé avec succès !

-

Nous vous répondrons dans les plus brefs délais.

-
- {:else} -
-
-
- - -
- -
- - -
-
- -
-
- - -
- -
- - -
-
- -
- - -
- -
- - -
- -
- -
-
- {/if} -
-
-
-
-
-
- - - - -
-
-
-

Questions fréquentes

- -
-
-

Comment puis-je obtenir une démonstration de Geosector ?

-

Vous pouvez demander une démonstration en remplissant le formulaire de contact ci-dessus ou en nous appelant directement. Un de nos conseillers vous contactera pour organiser une session personnalisée.

-
- -
-

Combien de temps dure la période d'essai ?

-

Nous proposons une période d'essai gratuite de 14 jours avec toutes les fonctionnalités disponibles. Aucune carte de crédit n'est requise pour commencer votre essai.

-
- -
-

Proposez-vous des formations pour utiliser votre logiciel ?

-

Oui, nous proposons des sessions de formation complètes pour vous aider à tirer le meilleur parti de Geosector. Ces formations peuvent être réalisées en ligne ou dans vos locaux selon vos préférences.

-
- -
-

Quels types de support technique proposez-vous ?

-

Nous offrons un support technique par email, téléphone et chat en direct pendant les heures de bureau. Nos clients avec des forfaits premium bénéficient d'un support 24/7.

-
-
-
-
-
-
diff --git a/web/src/pages/Fonctionnalites.svelte b/web/src/pages/Fonctionnalites.svelte deleted file mode 100755 index 1d775558..00000000 --- a/web/src/pages/Fonctionnalites.svelte +++ /dev/null @@ -1,244 +0,0 @@ - - -
- -
-
-
-

Fonctionnalités

-

Découvrez les outils puissants qui font de Geosector la solution idéale pour la gestion de vos distributions.

-
-
-
- - -
-
-
-

Fonctionnalités principales

- -
- -
-
- - - -
-
-

Cartographie avancée

-

Visualisez vos tournées sur des cartes interactives avec des données en temps réel sur le trafic et les conditions météorologiques.

-
    -
  • - - - - Cartes détaillées avec points d'intérêt -
  • -
  • - - - - Suivi GPS en temps réel -
  • -
  • - - - - Alertes de trafic et d'incidents -
  • -
-
-
- - -
-
- - - -
-
-

Optimisation des itinéraires

-

Nos algorithmes avancés calculent les itinéraires les plus efficaces en tenant compte de multiples facteurs.

-
    -
  • - - - - Réduction des coûts de carburant jusqu'à 30% -
  • -
  • - - - - Prise en compte des contraintes horaires -
  • -
  • - - - - Adaptation dynamique aux conditions réelles -
  • -
-
-
- - -
-
- - - -
-
-

Planification intelligente

-

Planifiez vos tournées à l'avance et adaptez-les facilement en fonction des imprévus.

-
    -
  • - - - - Calendrier interactif avec vue mensuelle/hebdomadaire/quotidienne -
  • -
  • - - - - Gestion des priorités et des urgences -
  • -
  • - - - - Notifications automatiques pour les changements -
  • -
-
-
- - -
-
- - - -
-
-

Rapports et analyses

-

Obtenez des insights précieux sur vos opérations grâce à nos outils d'analyse avancés.

-
    -
  • - - - - Tableaux de bord personnalisables -
  • -
  • - - - - Exportation des données en plusieurs formats -
  • -
  • - - - - Indicateurs de performance clés (KPIs) -
  • -
-
-
-
-
-
-
- - -
-
-
-

Application mobile

- -
-
-

Emportez Geosector partout avec vous

-

Notre application mobile offre toutes les fonctionnalités essentielles pour gérer vos distributions en déplacement.

- -
-
-
- - - -
-
-

Interface adaptée aux mobiles

-

Expérience utilisateur optimisée pour les écrans tactiles et la navigation mobile.

-
-
- -
-
- - - -
-
-

Mode hors ligne

-

Continuez à travailler même sans connexion internet, avec synchronisation automatique.

-
-
- -
-
- - - -
-
-

Notifications push

-

Restez informé des changements importants et des mises à jour en temps réel.

-
-
-
- - -
- -
-
- Capture d'écran de l'application mobile -
-
-
-
-
-
- - -
-
-
-

Prêt à optimiser vos distributions ?

-

Rejoignez les milliers d'entreprises qui font confiance à Geosector pour améliorer leur efficacité opérationnelle.

- Demander une démo -
-
-
-
diff --git a/web/src/pages/MentionsLegales.svelte b/web/src/pages/MentionsLegales.svelte deleted file mode 100755 index 164c0c06..00000000 --- a/web/src/pages/MentionsLegales.svelte +++ /dev/null @@ -1,118 +0,0 @@ - - -
- -
-
-
-

Mentions Légales

-

- Informations juridiques relatives à notre site web et application mobile -

-
-
-
- - -
-
-
-
-

1. Éditeur du site et de l'application

- -
-

Le site web et l'application mobile Geosector sont édités par :

-

Geosector

-

SIRET : [Votre numéro SIRET]

-

Adresse : [Votre adresse]

-

Email : contact@geosector.fr

-

Téléphone : [Votre numéro de téléphone]

-

Directeur de la publication : [Nom du directeur de publication]

-
-
- -
-

2. Hébergement

- -
-

Le site web et l'application mobile Geosector sont hébergés par :

-

[Nom de l'hébergeur]

-

Adresse : [Adresse de l'hébergeur]

-

Site web : [Site web de l'hébergeur]

-

Email : [Email de l'hébergeur]

-

Téléphone : [Téléphone de l'hébergeur]

-
-
- -
-

3. Propriété intellectuelle

- -
-

L'ensemble du contenu du site web et de l'application mobile Geosector, incluant sans limitation les textes, graphiques, images, logos, icônes, photographies, est la propriété exclusive de Geosector et est protégé par les lois françaises et internationales relatives à la propriété intellectuelle.

- -

Toute reproduction, représentation, modification, publication, transmission, adaptation, totale ou partielle des éléments du site ou de l'application, quel que soit le moyen ou le procédé utilisé, est interdite sans autorisation écrite préalable de Geosector.

- -

Toute utilisation non autorisée des contenus, œuvres ou marques constitue une contrefaçon sanctionnée par le Code de la propriété intellectuelle.

-
-
- -
-

4. Liens hypertextes

- -
-

Le site web et l'application Geosector peuvent contenir des liens hypertextes vers d'autres sites internet ou applications.

- -

Geosector n'a pas la possibilité de vérifier le contenu des sites ainsi visités, et n'assumera en conséquence aucune responsabilité de ce fait.

- -

La création de liens hypertextes vers le site web ou l'application Geosector est soumise à l'accord préalable de l'éditeur.

-
-
- -
-

5. Limitation de responsabilité

- -
-

Geosector s'efforce d'assurer au mieux de ses possibilités l'exactitude et la mise à jour des informations diffusées sur son site web et son application mobile, dont elle se réserve le droit de corriger, à tout moment et sans préavis, le contenu.

- -

Toutefois, Geosector ne peut garantir l'exactitude, la précision ou l'exhaustivité des informations mises à disposition sur son site web et son application.

- -

En conséquence, Geosector décline toute responsabilité :

-
    -
  • Pour toute imprécision, inexactitude ou omission portant sur des informations disponibles sur le site web ou l'application ;
  • -
  • Pour tous dommages résultant d'une intrusion frauduleuse d'un tiers ayant entraîné une modification des informations ou éléments mis à disposition sur le site web ou l'application ;
  • -
  • Et plus généralement, pour tous dommages, directs ou indirects, qu'elles qu'en soient les causes, origines, natures ou conséquences, provoqués en raison de l'accès de quiconque au site web ou à l'application ou de l'impossibilité d'y accéder, ainsi que l'utilisation du site web ou de l'application et/ou du crédit accordé à une quelconque information provenant directement ou indirectement de ces derniers.
  • -
-
-
- -
-

6. Loi applicable et juridiction

- -
-

Les présentes mentions légales sont régies par la loi française. En cas de litige, les tribunaux français seront seuls compétents.

- -

Pour toute question relative à l'application des présentes mentions légales, vous pouvez nous contacter à l'adresse email : contact@geosector.fr

-
-
- -
-

7. Modifications

- -
-

Geosector se réserve le droit de modifier les présentes mentions légales à tout moment. L'utilisateur est invité à les consulter régulièrement.

- -

Dernière mise à jour : {new Date().toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' })}

-
-
-
-
-
-
diff --git a/web/src/pages/PolitiqueConfidentialite.svelte b/web/src/pages/PolitiqueConfidentialite.svelte deleted file mode 100755 index 598fc536..00000000 --- a/web/src/pages/PolitiqueConfidentialite.svelte +++ /dev/null @@ -1,216 +0,0 @@ - - -
- -
-
-
-

- Politique de confidentialité -

-

- Protection de vos données personnelles et respect de votre vie privée -

-
-
-
- - -
-
-
-
-

Dernière mise à jour : {new Date().toLocaleDateString('fr-FR', { year: 'numeric', month: 'long', day: 'numeric' })}

- -

Introduction

-

- Cette politique de confidentialité s'applique à l'application Geosector, disponible sur le Web, iOS et Android, - ainsi qu'à tous les services associés (collectivement désignés par "Geosector", "nous", "notre" ou "nos"). -

-

- Chez Geosector, nous accordons une grande importance à la protection de vos données personnelles. - Cette politique décrit quelles informations nous collectons, comment nous les utilisons, - et quels choix vous avez concernant ces données. -

-

- Cette politique de confidentialité doit être lue conjointement avec nos handleNavigation('conditions-utilisation')}>Conditions d'utilisation, qui régissent votre utilisation de notre application. -

- -

Quelles informations collectons-nous ?

- -

1. Informations que vous nous fournissez

-
    -
  • Informations de compte : Lors de l'inscription, nous collectons votre nom, prénom, adresse e-mail, et mot de passe.
  • -
  • Informations de profil : Vous pouvez nous fournir des informations supplémentaires comme votre fonction, l'organisation à laquelle vous appartenez, et votre photo de profil.
  • -
  • Contenu utilisateur : Les informations que vous créez, téléchargez ou partagez via notre application, notamment les secteurs géographiques, les passages, et les commentaires.
  • -
  • Communications : Lorsque vous nous contactez, nous conservons un historique de ces communications.
  • -
- -

2. Informations collectées automatiquement

-
    -
  • Données d'utilisation : Informations sur vos interactions avec notre application, comme les fonctionnalités utilisées, les pages visitées et le temps passé.
  • -
  • Informations sur l'appareil : Type d'appareil, système d'exploitation, version de l'application, langue, fuseau horaire et autres caractéristiques techniques.
  • -
  • Données de localisation : Avec votre consentement, nous collectons des données de géolocalisation précises pour vous permettre d'utiliser les fonctionnalités cartographiques et de secteurs.
  • -
  • Cookies et technologies similaires : Sur notre version web, nous utilisons des cookies et des technologies similaires pour améliorer votre expérience. Pour plus d'informations, consultez notre politique relative aux cookies.
  • -
- -

Comment utilisons-nous vos informations ?

-

Nous utilisons vos informations pour les finalités suivantes :

-
    -
  • Fournir, maintenir et améliorer notre application et ses fonctionnalités
  • -
  • Créer et gérer votre compte
  • -
  • Traiter vos transactions et paiements
  • -
  • Vous envoyer des informations techniques, des mises à jour, des alertes de sécurité et des messages administratifs
  • -
  • Répondre à vos commentaires et questions et vous fournir un support client
  • -
  • Communiquer avec vous à propos de produits, services, offres et événements
  • -
  • Surveiller et analyser les tendances, l'utilisation et les activités liées à notre application
  • -
  • Détecter, prévenir et résoudre les problèmes techniques et de sécurité
  • -
  • Se conformer aux obligations légales
  • -
- -

Base légale du traitement (pour les utilisateurs de l'EEE et du Royaume-Uni)

-

Pour les utilisateurs de l'Espace économique européen (EEE) et du Royaume-Uni, nous traitons vos données personnelles sur les bases légales suivantes :

-
    -
  • Exécution d'un contrat : Lorsque le traitement est nécessaire pour l'exécution d'un contrat auquel vous êtes partie ou pour prendre des mesures à votre demande avant de conclure un contrat.
  • -
  • Intérêts légitimes : Lorsque le traitement est nécessaire pour nos intérêts légitimes ou ceux d'un tiers, et que ces intérêts ne sont pas supplantés par vos intérêts ou droits fondamentaux.
  • -
  • Consentement : Lorsque vous avez donné votre consentement au traitement de vos données personnelles pour une ou plusieurs finalités spécifiques.
  • -
  • Obligation légale : Lorsque le traitement est nécessaire pour respecter une obligation légale à laquelle nous sommes soumis.
  • -
- -

Comment partageons-nous vos informations ?

-

Nous pouvons partager vos informations personnelles avec les tiers suivants :

-
    -
  • Prestataires de services : Nous travaillons avec des prestataires de services tiers qui fournissent des services tels que l'hébergement, l'analyse, le traitement des paiements et le support client.
  • -
  • Partenaires professionnels : Nous pouvons partager des informations avec nos partenaires commerciaux pour offrir certains produits, services ou promotions.
  • -
  • Conformité légale : Nous pouvons divulguer vos informations si nous estimons de bonne foi que cette divulgation est nécessaire pour se conformer à la loi, protéger nos droits ou assurer votre sécurité.
  • -
  • Transactions d'entreprise : En cas de fusion, acquisition, restructuration ou vente d'actifs, vos informations peuvent être transférées dans le cadre de cette transaction.
  • -
-

Nous ne vendons pas vos données personnelles à des tiers.

- -

Transferts internationaux de données

-

- Vos informations peuvent être transférées et traitées dans des pays autres que celui où vous résidez. - Ces pays peuvent avoir des lois sur la protection des données différentes de celles de votre pays. -

-

- Si nous transférons des données personnelles provenant de l'EEE, du Royaume-Uni ou de la Suisse vers des pays - n'offrant pas un niveau de protection adéquat selon les autorités compétentes, nous utilisons des - mécanismes de transfert légalement reconnus, tels que les clauses contractuelles types approuvées par la Commission européenne. -

- -

Vos droits et choix

-

Selon votre lieu de résidence, vous pouvez disposer de certains droits concernant vos données personnelles :

-
    -
  • Accès et portabilité : Vous pouvez accéder à vos informations personnelles et en obtenir une copie dans un format structuré, couramment utilisé et lisible par machine.
  • -
  • Correction : Vous pouvez mettre à jour ou corriger vos informations personnelles si elles sont inexactes ou incomplètes.
  • -
  • Suppression : Vous pouvez demander la suppression de vos données personnelles dans certaines circonstances.
  • -
  • Restriction et opposition : Vous pouvez demander la restriction du traitement de vos données personnelles ou vous opposer à leur traitement dans certaines circonstances.
  • -
  • Consentement : Lorsque le traitement est basé sur votre consentement, vous pouvez retirer ce consentement à tout moment.
  • -
  • Réclamation : Vous avez le droit d'introduire une réclamation auprès d'une autorité de protection des données.
  • -
-

- Pour exercer ces droits, contactez-nous à l'adresse indiquée dans la section "Nous contacter" ci-dessous. - Notez que ces droits peuvent être soumis à des limitations et exceptions prévues par la loi applicable. -

- -

Conservation des données

-

- Nous conservons vos données personnelles aussi longtemps que nécessaire pour atteindre les finalités décrites dans cette politique, - sauf si une période de conservation plus longue est requise ou permise par la loi. - Les critères utilisés pour déterminer nos périodes de conservation comprennent : -

-
    -
  • La durée pendant laquelle nous entretenons une relation continue avec vous et vous fournissons l'application
  • -
  • Si nous avons une obligation légale à laquelle nous sommes soumis
  • -
  • Si la conservation est souhaitable compte tenu de notre position juridique (par exemple, concernant les délais de prescription applicables, les litiges ou les enquêtes réglementaires)
  • -
- -

Sécurité des données

-

- Nous mettons en œuvre des mesures de sécurité techniques et organisationnelles appropriées pour protéger vos données personnelles - contre la perte accidentelle, l'utilisation non autorisée, l'altération et la divulgation. - Ces mesures comprennent le chiffrement des données, les contrôles d'accès, les pare-feu et les audits de sécurité réguliers. -

-

- Cependant, aucun système de sécurité n'est impénétrable et nous ne pouvons garantir la sécurité absolue de vos informations. - Il est important que vous preniez des précautions pour protéger votre mot de passe et votre appareil. -

- -

Protection de la vie privée des enfants

-

- Notre application n'est pas destinée aux personnes âgées de moins de 16 ans et nous ne collectons pas sciemment - des données personnelles auprès d'enfants de moins de 16 ans. Si vous êtes parent ou tuteur et que vous pensez - que votre enfant nous a fourni des informations personnelles, veuillez nous contacter. -

- -

Modifications de cette politique

-

- Nous pouvons modifier cette politique de confidentialité de temps à autre. Si nous apportons des modifications importantes, - nous vous en informerons par e-mail ou par une notification dans notre application avant que les modifications - ne prennent effet. Nous vous encourageons à consulter régulièrement cette politique pour rester informé de - nos pratiques en matière de protection des données. -

- -

Nous contacter

-

- Si vous avez des questions concernant cette politique de confidentialité ou nos pratiques en matière de protection des données, - veuillez nous contacter à l'adresse suivante : -

-

- Geosector
- E-mail : privacy@geosector.fr
- Adresse : [Adresse de l'entreprise]
- Téléphone : +33 (0)1 23 45 67 89 -

- -

Informations spécifiques aux plateformes

- -

Application iOS (Apple App Store)

-

- En utilisant notre application via l'App Store d'Apple, vous reconnaissez qu'Apple n'est pas responsable de nos pratiques - en matière de protection des données. Veuillez consulter la politique de confidentialité d'Apple pour plus d'informations - sur la façon dont Apple peut collecter et traiter vos données. -

- -

Application Android (Google Play)

-

- En utilisant notre application via Google Play, vous reconnaissez que Google n'est pas responsable de nos pratiques - en matière de protection des données. Veuillez consulter la politique de confidentialité de Google pour plus d'informations - sur la façon dont Google peut collecter et traiter vos données. -

- -

Permissions des applications mobiles

-

Notre application peut demander certaines permissions sur votre appareil mobile, notamment :

-
    -
  • Localisation : Pour les fonctionnalités basées sur la localisation, comme l'affichage des secteurs et la navigation
  • -
  • Stockage : Pour stocker des données localement sur votre appareil
  • -
  • Appareil photo : Pour scanner des codes QR ou prendre des photos
  • -
  • Notifications : Pour vous envoyer des alertes et des mises à jour importantes
  • -
-

- Vous pouvez gérer ces permissions à tout moment dans les paramètres de votre appareil, mais notez que - la désactivation de certaines permissions peut limiter les fonctionnalités de l'application. -

-
-
-
-
-
diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts deleted file mode 100755 index 4078e747..00000000 --- a/web/src/vite-env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/web/svelte.config.js b/web/svelte.config.js deleted file mode 100755 index b0683fd2..00000000 --- a/web/svelte.config.js +++ /dev/null @@ -1,7 +0,0 @@ -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' - -export default { - // Consult https://svelte.dev/docs#compile-time-svelte-preprocess - // for more information about preprocessors - preprocess: vitePreprocess(), -} diff --git a/web/tailwind.config.js b/web/tailwind.config.js deleted file mode 100755 index d923d9a4..00000000 --- a/web/tailwind.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: ["./src/**/*.{html,js,svelte,ts}"], - - theme: { - fontFamily: { - sans: ['Figtree', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'], - }, - extend: {} - }, - - plugins: [] -}; diff --git a/web/vite.config.js b/web/vite.config.js deleted file mode 100755 index ca5dd9c2..00000000 --- a/web/vite.config.js +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from 'vite' -import { svelte } from '@sveltejs/vite-plugin-svelte' - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [svelte()], - server: { - // Configuration pour le mode histoire - historyApiFallback: true - }, - // Pour le build de production, configurer la gestion des routes - build: { - rollupOptions: { - output: { - manualChunks: undefined, - }, - }, - }, -})