feat: Livraison version 3.0.6

- Amélioration de la gestion des entités et des utilisateurs
- Mise à jour des modèles Amicale et Client avec champs supplémentaires
- Ajout du service de logging et amélioration du chargement UI
- Refactoring des formulaires utilisateur et amicale
- Intégration de file_picker et image_picker pour la gestion des fichiers
- Amélioration de la gestion des membres et de leur suppression
- Optimisation des performances de l'API
- Mise à jour de la documentation technique

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-08 20:33:54 +02:00
parent 599b9fcda0
commit ca121af747
69 changed files with 203569 additions and 174972 deletions

View File

@@ -9,6 +9,7 @@
5. [Base de données](#base-de-données)
6. [Sécurité](#sécurité)
7. [Endpoints API](#endpoints-api)
8. [Changements récents](#changements-récents)
## Structure du projet
@@ -126,6 +127,54 @@ Exemple détaillé du parcours d'une requête POST /api/users :
- Gère le pool de connexions
- Assure la sécurité des requêtes
## Base de données
### Structure des tables principales
#### Table `users`
- `encrypted_user_name` : Identifiant de connexion chiffré (unique)
- `encrypted_email` : Email chiffré (unique)
- `user_pass_hash` : Hash du mot de passe
- `encrypted_name`, `encrypted_phone`, `encrypted_mobile` : Données personnelles chiffrées
- Autres champs : `first_name`, `sect_name`, `fk_role`, `fk_entite`, etc.
#### Table `entites` (Amicales)
- `chk_mdp_manuel` (DEFAULT 0) : Gestion manuelle des mots de passe
- `chk_username_manuel` (DEFAULT 0) : Gestion manuelle des identifiants
- `chk_stripe` : Activation des paiements Stripe
- Données chiffrées : `encrypted_name`, `encrypted_email`, `encrypted_phone`, etc.
#### Table `medias`
- `support` : Type de support (entite, user, operation, passage)
- `support_id` : ID de l'élément associé
- `file_category` : Catégorie (logo, export, carte, etc.)
- `file_path` : Chemin complet du fichier
- `processed_width/height` : Dimensions après traitement
- Utilisée pour stocker les logos des entités
### Chiffrement des données
Toutes les données sensibles sont chiffrées avec AES-256-CBC :
- Emails, noms, téléphones
- Identifiants de connexion
- Informations bancaires (IBAN, BIC)
### Migration de base de données
Script SQL pour ajouter les nouveaux champs :
```sql
-- Ajout de la gestion manuelle des usernames
ALTER TABLE `entites`
ADD COLUMN `chk_username_manuel` tinyint(1) unsigned NOT NULL DEFAULT 0
COMMENT 'Gestion des usernames manuelle (1) ou automatique (0)'
AFTER `chk_mdp_manuel`;
-- Index pour optimiser la vérification d'unicité
ALTER TABLE `users`
ADD INDEX `idx_encrypted_user_name` (`encrypted_user_name`);
```
## Sécurité
### Mesures implémentées
@@ -137,6 +186,8 @@ Exemple détaillé du parcours d'une requête POST /api/users :
- Gestion des CORS
- Session sécurisée
- Authentification requise
- Chiffrement AES-256 des données sensibles
- Envoi séparé des identifiants par email
## Endpoints API
@@ -223,39 +274,260 @@ La configuration des sessions inclut :
#### Création d'utilisateur
La création d'utilisateur s'adapte aux paramètres de l'entité (amicale) :
```http
POST /api/users
Content-Type: application/json
Authorization: Bearer {session_id}
{
"name": "John Doe",
"email": "john@example.com",
"password": "SecurePassword123"
"name": "John Doe",
"first_name": "John",
"role": 1,
"fk_entite": 5,
"username": "j.doe38", // Requis si chk_username_manuel=1 pour l'entité
"password": "SecurePass123", // Requis si chk_mdp_manuel=1 pour l'entité
"phone": "0476123456",
"mobile": "0612345678",
"sect_name": "Secteur A",
"date_naissance": "1990-01-15",
"date_embauche": "2020-03-01"
}
```
**Comportement selon les paramètres de l'entité :**
| chk_username_manuel | chk_mdp_manuel | Comportement |
|---------------------|----------------|--------------|
| 0 | 0 | Username et password générés automatiquement |
| 0 | 1 | Username généré, password requis dans le payload |
| 1 | 0 | Username requis dans le payload, password généré |
| 1 | 1 | Username et password requis dans le payload |
**Validation du username (si manuel) :**
- Format : 10-30 caractères
- Commence par une lettre
- Caractères autorisés : a-z, 0-9, ., -, _
- Doit être unique dans toute la base
**Réponse réussie :**
```json
{
"message": "Utilisateur créé",
"id": "123"
"status": "success",
"message": "Utilisateur créé avec succès",
"id": 123,
"username": "j.doe38", // Toujours retourné
"password": "xY7#mK9@pL2" // Retourné seulement si généré automatiquement
}
```
**Envoi d'emails :**
- **Email 1** : Identifiant de connexion (toujours envoyé)
- **Email 2** : Mot de passe (toujours envoyé, 1 seconde après le premier)
**Codes de statut :**
- 201: Création réussie
- 400: Données invalides
- 400: Données invalides ou username/password manquant si requis
- 401: Non authentifié
- 403: Accès non autorisé (rôle insuffisant)
- 409: Email ou username déjà utilisé
- 500: Erreur serveur
#### Vérification de disponibilité du username
```http
POST /api/users/check-username
Content-Type: application/json
Authorization: Bearer {session_id}
{
"username": "j.doe38"
}
```
**Réponse si disponible :**
```json
{
"status": "success",
"available": true,
"message": "Nom d'utilisateur disponible",
"username": "j.doe38"
}
```
**Réponse si déjà pris :**
```json
{
"status": "success",
"available": false,
"message": "Ce nom d'utilisateur est déjà utilisé",
"suggestions": ["j.doe38_42", "j.doe381234", "j.doe3825"]
}
```
#### Autres endpoints
- GET /api/users
- GET /api/users/{id}
- PUT /api/users/{id}
- DELETE /api/users/{id}
- POST /api/users/{id}/reset-password
### Entités (Amicales)
#### Upload du logo d'une entité
```http
POST /api/entites/{id}/logo
Content-Type: multipart/form-data
Authorization: Bearer {session_id}
Body:
logo: File (image/png, image/jpeg, image/jpg)
```
**Restrictions :**
- Réservé aux administrateurs d'amicale (fk_role == 2)
- L'admin ne peut uploader que le logo de sa propre amicale
- Un seul logo actif par entité (le nouveau remplace l'ancien)
**Traitement de l'image :**
- Formats acceptés : PNG, JPG, JPEG
- Redimensionnement automatique : 250x250px maximum (ratio conservé)
- Résolution : 72 DPI (standard web)
- Préservation de la transparence pour les PNG
**Stockage :**
- Chemin : `/uploads/entites/{id}/logo/logo_{id}_{timestamp}.{ext}`
- Enregistrement dans la table `medias`
- Suppression automatique de l'ancien logo
**Réponse réussie :**
```json
{
"status": "success",
"message": "Logo uploadé avec succès",
"media_id": 42,
"file_name": "logo_5_1234567890.jpg",
"file_path": "/entites/5/logo/logo_5_1234567890.jpg",
"dimensions": {
"width": 250,
"height": 180
}
}
```
#### Récupération du logo d'une entité
```http
GET /api/entites/{id}/logo
Authorization: Bearer {session_id}
```
**Réponse :**
```json
{
"status": "success",
"logo": {
"id": 42,
"data_url": "data:image/png;base64,iVBORw0KGgoAAAANS...",
"file_name": "logo_5_1234567890.png",
"mime_type": "image/png",
"width": 250,
"height": 180,
"size": 15234
}
}
```
**Note :** Le logo est également inclus automatiquement dans la réponse du login si disponible.
#### Mise à jour d'une entité
```http
PUT /api/entites/{id}
Content-Type: application/json
Authorization: Bearer {session_id}
{
"name": "Amicale de Grenoble",
"adresse1": "123 rue de la Caserne",
"adresse2": "",
"code_postal": "38000",
"ville": "Grenoble",
"phone": "0476123456",
"mobile": "0612345678",
"email": "contact@amicale38.fr",
"chk_stripe": true, // Activation paiement Stripe
"chk_mdp_manuel": false, // Génération auto des mots de passe
"chk_username_manuel": false, // Génération auto des usernames
"chk_copie_mail_recu": true,
"chk_accept_sms": false
}
```
**Paramètres de gestion des membres :**
| Paramètre | Type | Description |
|-----------|------|-------------|
| chk_mdp_manuel | boolean | `true`: L'admin saisit les mots de passe<br>`false`: Génération automatique |
| chk_username_manuel | boolean | `true`: L'admin saisit les identifiants<br>`false`: Génération automatique |
| chk_stripe | boolean | Active/désactive les paiements Stripe |
**Note :** Ces paramètres sont modifiables uniquement par les administrateurs (fk_role > 1).
#### Réponse du login avec paramètres entité
Lors du login, les paramètres de l'entité sont retournés dans le groupe `amicale` :
```json
{
"status": "success",
"session_id": "abc123...",
"session_expiry": "2025-01-09T15:30:00+00:00",
"user": {
"id": 9999980,
"fk_entite": 5,
"fk_role": 2,
"fk_titre": null,
"first_name": "Pierre",
"sect_name": "",
"date_naissance": "1990-01-15", // Maintenant correctement récupéré
"date_embauche": "2020-03-01", // Maintenant correctement récupéré
"username": "pv_admin",
"name": "VALERY ADM",
"phone": "0476123456", // Maintenant correctement récupéré
"mobile": "0612345678", // Maintenant correctement récupéré
"email": "contact@resalice.com"
},
"amicale": {
"id": 5,
"name": "Amicale de Grenoble",
"chk_mdp_manuel": 0,
"chk_username_manuel": 0,
"chk_stripe": 1,
"logo": { // Logo de l'entité (si disponible)
"id": 42,
"data_url": "data:image/png;base64,iVBORw0KGgoAAAANS...",
"file_name": "logo_5_1234567890.png",
"mime_type": "image/png",
"width": 250,
"height": 180
}
// ... autres champs
}
}
```
Ces paramètres permettent à l'application Flutter d'adapter dynamiquement le formulaire de création de membre.
## Intégration Frontend
@@ -298,3 +570,45 @@ fetch('/api/endpoint', {
- Surveillance de la base de données
- Monitoring des performances
- Alertes sur erreurs critiques
## Changements récents
### Version 3.0.6 (Janvier 2025)
#### 1. Correction des rôles administrateurs
- **Avant :** Les administrateurs d'amicale devaient avoir `fk_role > 2`
- **Après :** Les administrateurs d'amicale ont `fk_role > 1` (donc rôle 2 et plus)
- **Impact :** Les champs `chk_stripe`, `chk_mdp_manuel`, `chk_username_manuel` sont maintenant modifiables par les admins d'amicale (rôle 2)
#### 2. Envoi systématique des deux emails lors de la création d'utilisateur
- **Avant :** Le 2ème email (mot de passe) n'était envoyé que si le mot de passe était généré automatiquement
- **Après :** Les deux emails sont toujours envoyés lors de la création d'un membre
- Email 1 : Identifiant (username)
- Email 2 : Mot de passe (1 seconde après)
- **Raison :** Le nouveau membre a toujours besoin des deux informations pour se connecter
#### 3. Ajout des champs manquants dans la réponse du login
- **Champs ajoutés dans la requête SQL :**
- `fk_titre`
- `date_naissance`
- `date_embauche`
- `encrypted_phone`
- `encrypted_mobile`
- **Impact :** Ces données sont maintenant correctement retournées dans l'objet `user` lors du login
#### 4. Système de gestion des logos d'entité
- **Nouvelle fonctionnalité :** Upload et gestion des logos pour les amicales
- **Routes ajoutées :**
- `POST /api/entites/{id}/logo` : Upload d'un nouveau logo
- `GET /api/entites/{id}/logo` : Récupération du logo
- **Caractéristiques :**
- Réservé aux administrateurs d'amicale (fk_role == 2)
- Un seul logo actif par entité
- Redimensionnement automatique (250x250px max)
- Format base64 dans les réponses JSON (compatible Flutter)
- Logo inclus automatiquement dans la réponse du login
#### 5. Amélioration de l'intégration Flutter
- **Format d'envoi des images :** Base64 data URL pour compatibilité multiplateforme
- **Structure de réponse enrichie :** Le logo est inclus dans l'objet `amicale` lors du login
- **Optimisation :** Pas de requête HTTP supplémentaire nécessaire pour afficher le logo

View File

@@ -220,7 +220,8 @@ CREATE TABLE `entites` (
`encrypted_iban` varchar(255) DEFAULT '',
`encrypted_bic` varchar(128) DEFAULT '',
`chk_demo` tinyint(1) unsigned DEFAULT 1,
`chk_mdp_manuel` tinyint(1) unsigned NOT NULL DEFAULT 1 COMMENT 'Gestion des mots de passe manuelle O/N',
`chk_mdp_manuel` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'Gestion des mots de passe manuelle (1) ou automatique (0)',
`chk_username_manuel` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'Gestion des usernames manuelle (1) ou automatique (0)',
`chk_copie_mail_recu` tinyint(1) unsigned NOT NULL DEFAULT 0,
`chk_accept_sms` tinyint(1) unsigned NOT NULL DEFAULT 0,
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',

View File

@@ -0,0 +1,25 @@
-- Script de migration pour ajouter le champ chk_username_manuel et modifier chk_mdp_manuel
-- Date: 2025-08-07
-- Description: Permet aux administrateurs d'entité de choisir entre saisie manuelle ou génération automatique des usernames et mots de passe
-- Modification du champ chk_mdp_manuel pour mettre la valeur par défaut à 0 (génération automatique)
ALTER TABLE `entites`
MODIFY COLUMN `chk_mdp_manuel` tinyint(1) unsigned NOT NULL DEFAULT 0
COMMENT 'Gestion des mots de passe manuelle (1) ou automatique (0)';
-- Ajout du champ chk_username_manuel dans la table entites
ALTER TABLE `entites`
ADD COLUMN `chk_username_manuel` tinyint(1) unsigned NOT NULL DEFAULT 0
COMMENT 'Gestion des usernames manuelle (1) ou automatique (0)'
AFTER `chk_mdp_manuel`;
-- Par défaut, on met à 0 (génération automatique) pour toutes les entités existantes
-- Cela permet de garder le comportement actuel par défaut
UPDATE `entites` SET `chk_username_manuel` = 0 WHERE `chk_username_manuel` IS NULL;
-- Ajout d'un index sur encrypted_user_name pour améliorer les performances de vérification d'unicité
ALTER TABLE `users`
ADD INDEX `idx_encrypted_user_name` (`encrypted_user_name`);
-- Message de confirmation
SELECT CONCAT('Migration effectuée : chk_username_manuel ajouté à la table entites') AS 'Status';

View File

@@ -415,7 +415,7 @@ class EntiteController {
}
$userRole = (int)$user['fk_role'];
$isAdmin = $userRole > 2;
$isAdmin = $userRole > 1;
// Récupérer les données de la requête
$data = Request::getJson();
@@ -577,6 +577,16 @@ class EntiteController {
$updateFields[] = 'chk_stripe = ?';
$params[] = $data['chk_stripe'] ? 1 : 0;
}
if (isset($data['chk_mdp_manuel'])) {
$updateFields[] = 'chk_mdp_manuel = ?';
$params[] = $data['chk_mdp_manuel'] ? 1 : 0;
}
if (isset($data['chk_username_manuel'])) {
$updateFields[] = 'chk_username_manuel = ?';
$params[] = $data['chk_username_manuel'] ? 1 : 0;
}
}
// Si aucun champ à mettre à jour, retourner une erreur
@@ -631,4 +641,341 @@ class EntiteController {
], 500);
}
}
/**
* Upload et traite le logo d'une entité
* Réservé aux administrateurs d'amicale (fk_role == 2)
*
* @param string $id ID de l'entité
* @return void
*/
public function uploadLogo(string $id): void {
try {
// Vérifier l'authentification
$userId = Session::getUserId();
if (!$userId) {
Response::json([
'status' => 'error',
'message' => 'Vous devez être connecté pour effectuer cette action'
], 401);
return;
}
// Récupérer les infos de l'utilisateur
$stmt = $this->db->prepare('SELECT fk_role, fk_entite FROM users WHERE id = ?');
$stmt->execute([$userId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
Response::json([
'status' => 'error',
'message' => 'Utilisateur non trouvé'
], 404);
return;
}
// Vérifier que l'utilisateur est admin d'amicale (fk_role == 2)
if ((int)$user['fk_role'] !== 2) {
Response::json([
'status' => 'error',
'message' => 'Seuls les administrateurs d\'amicale peuvent uploader un logo'
], 403);
return;
}
// Vérifier que l'entité correspond à celle de l'utilisateur
$entiteId = (int)$id;
if ($entiteId !== (int)$user['fk_entite']) {
Response::json([
'status' => 'error',
'message' => 'Vous ne pouvez modifier que le logo de votre propre amicale'
], 403);
return;
}
// Vérifier qu'un fichier a été envoyé
if (!isset($_FILES['logo']) || $_FILES['logo']['error'] !== UPLOAD_ERR_OK) {
Response::json([
'status' => 'error',
'message' => 'Aucun fichier reçu ou erreur lors de l\'upload'
], 400);
return;
}
$uploadedFile = $_FILES['logo'];
// Vérifier le type MIME
$allowedMimes = ['image/jpeg', 'image/jpg', 'image/png'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $uploadedFile['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedMimes)) {
Response::json([
'status' => 'error',
'message' => 'Format de fichier non autorisé. Seuls PNG, JPG et JPEG sont acceptés'
], 400);
return;
}
// Déterminer l'extension
$extension = match($mimeType) {
'image/jpeg', 'image/jpg' => 'jpg',
'image/png' => 'png',
default => 'jpg'
};
// Créer le dossier de destination
require_once __DIR__ . '/../Services/FileService.php';
$fileService = new \FileService();
$uploadPath = "/entites/{$entiteId}/logo";
$fullPath = $fileService->createDirectory($entiteId, $uploadPath);
// Nom du fichier final
$fileName = "logo_{$entiteId}_" . time() . ".{$extension}";
$filePath = $fullPath . '/' . $fileName;
// Charger l'image avec GD
$sourceImage = match($mimeType) {
'image/jpeg', 'image/jpg' => imagecreatefromjpeg($uploadedFile['tmp_name']),
'image/png' => imagecreatefrompng($uploadedFile['tmp_name']),
default => false
};
if (!$sourceImage) {
Response::json([
'status' => 'error',
'message' => 'Impossible de traiter l\'image'
], 500);
return;
}
// Obtenir les dimensions originales
$originalWidth = imagesx($sourceImage);
$originalHeight = imagesy($sourceImage);
// Calculer les nouvelles dimensions (max 250px) en gardant le ratio
$maxSize = 250;
$ratio = min($maxSize / $originalWidth, $maxSize / $originalHeight, 1);
$newWidth = (int)($originalWidth * $ratio);
$newHeight = (int)($originalHeight * $ratio);
// Créer l'image redimensionnée
$resizedImage = imagecreatetruecolor($newWidth, $newHeight);
// Préserver la transparence pour les PNG
if ($mimeType === 'image/png') {
imagealphablending($resizedImage, false);
imagesavealpha($resizedImage, true);
$transparent = imagecolorallocatealpha($resizedImage, 255, 255, 255, 127);
imagefilledrectangle($resizedImage, 0, 0, $newWidth, $newHeight, $transparent);
}
// Redimensionner
imagecopyresampled(
$resizedImage, $sourceImage,
0, 0, 0, 0,
$newWidth, $newHeight,
$originalWidth, $originalHeight
);
// Sauvegarder l'image (72 DPI est la résolution standard web)
$saved = match($mimeType) {
'image/jpeg', 'image/jpg' => imagejpeg($resizedImage, $filePath, 85),
'image/png' => imagepng($resizedImage, $filePath, 8),
default => false
};
// Libérer la mémoire
imagedestroy($sourceImage);
imagedestroy($resizedImage);
if (!$saved) {
Response::json([
'status' => 'error',
'message' => 'Impossible de sauvegarder l\'image'
], 500);
return;
}
// Appliquer les permissions
$fileService->setFilePermissions($filePath);
// Supprimer l'ancien logo s'il existe
$stmt = $this->db->prepare('
SELECT id, file_path FROM medias
WHERE support = ? AND support_id = ? AND file_category = ?
');
$stmt->execute(['entite', $entiteId, 'logo']);
$oldLogo = $stmt->fetch(PDO::FETCH_ASSOC);
if ($oldLogo && !empty($oldLogo['file_path']) && file_exists($oldLogo['file_path'])) {
unlink($oldLogo['file_path']);
// Supprimer l'entrée de la base
$stmt = $this->db->prepare('DELETE FROM medias WHERE id = ?');
$stmt->execute([$oldLogo['id']]);
}
// Enregistrer dans la table medias
$stmt = $this->db->prepare('
INSERT INTO medias (
support, support_id, fichier, file_type, file_category,
file_size, mime_type, original_name, fk_entite,
file_path, original_width, original_height,
processed_width, processed_height, is_processed,
description, created_at, fk_user_creat
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), ?)
');
$stmt->execute([
'entite', // support
$entiteId, // support_id
$fileName, // fichier
$extension, // file_type
'logo', // file_category
filesize($filePath), // file_size
$mimeType, // mime_type
$uploadedFile['name'], // original_name
$entiteId, // fk_entite
$filePath, // file_path
$originalWidth, // original_width
$originalHeight, // original_height
$newWidth, // processed_width
$newHeight, // processed_height
1, // is_processed
'Logo de l\'entité', // description
$userId // fk_user_creat
]);
$mediaId = $this->db->lastInsertId();
LogService::log('Upload de logo réussi', [
'level' => 'info',
'userId' => $userId,
'entiteId' => $entiteId,
'mediaId' => $mediaId,
'fileName' => $fileName,
'originalSize' => "{$originalWidth}x{$originalHeight}",
'newSize' => "{$newWidth}x{$newHeight}"
]);
Response::json([
'status' => 'success',
'message' => 'Logo uploadé avec succès',
'media_id' => $mediaId,
'file_name' => $fileName,
'file_path' => $uploadPath . '/' . $fileName,
'dimensions' => [
'width' => $newWidth,
'height' => $newHeight
]
], 200);
} catch (Exception $e) {
LogService::log('Erreur lors de l\'upload du logo', [
'level' => 'error',
'error' => $e->getMessage(),
'entiteId' => $id
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors de l\'upload du logo'
], 500);
}
}
/**
* Récupère le logo d'une entité
*
* @param string $id ID de l'entité
* @return void
*/
public function getLogo(string $id): void {
try {
// Vérifier l'authentification
$userId = Session::getUserId();
if (!$userId) {
Response::json([
'status' => 'error',
'message' => 'Vous devez être connecté pour effectuer cette action'
], 401);
return;
}
$entiteId = (int)$id;
// Récupérer le logo depuis la base
$stmt = $this->db->prepare('
SELECT id, fichier, file_path, file_type, mime_type,
processed_width, processed_height, file_size
FROM medias
WHERE support = ? AND support_id = ? AND file_category = ?
ORDER BY created_at DESC
LIMIT 1
');
$stmt->execute(['entite', $entiteId, 'logo']);
$logo = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$logo) {
Response::json([
'status' => 'error',
'message' => 'Aucun logo trouvé pour cette entité'
], 404);
return;
}
if (!file_exists($logo['file_path'])) {
Response::json([
'status' => 'error',
'message' => 'Fichier logo introuvable'
], 404);
return;
}
// Option 1 : Retourner l'image directement (pour une URL séparée)
// header('Content-Type: ' . $logo['mime_type']);
// header('Content-Length: ' . $logo['file_size']);
// header('Cache-Control: public, max-age=86400'); // Cache 24h
// readfile($logo['file_path']);
// Option 2 : Retourner en base64 dans JSON (recommandé pour Flutter)
$imageData = file_get_contents($logo['file_path']);
if ($imageData === false) {
Response::json([
'status' => 'error',
'message' => 'Impossible de lire le fichier logo'
], 500);
return;
}
$base64 = base64_encode($imageData);
$dataUrl = 'data:' . $logo['mime_type'] . ';base64,' . $base64;
Response::json([
'status' => 'success',
'logo' => [
'id' => $logo['id'],
'data_url' => $dataUrl,
'file_name' => $logo['fichier'],
'mime_type' => $logo['mime_type'],
'width' => $logo['processed_width'],
'height' => $logo['processed_height'],
'size' => $logo['file_size']
]
], 200);
} catch (Exception $e) {
LogService::log('Erreur lors de la récupération du logo', [
'level' => 'error',
'error' => $e->getMessage(),
'entiteId' => $id
]);
Response::json([
'status' => 'error',
'message' => 'Erreur lors de la récupération du logo'
], 500);
}
}
}

View File

@@ -64,7 +64,8 @@ class LoginController {
$stmt = $this->db->prepare(
'SELECT
u.id, u.encrypted_email, u.encrypted_user_name, u.encrypted_name, u.user_pass_hash,
u.first_name, u.fk_role, u.fk_entite, u.chk_active, u.sect_name,
u.first_name, u.fk_role, u.fk_entite, u.fk_titre, u.chk_active, u.sect_name,
u.date_naissance, u.date_embauche, u.encrypted_phone, u.encrypted_mobile,
e.id AS entite_id, e.encrypted_name AS entite_encrypted_name,
e.adresse1, e.code_postal, e.ville, e.gps_lat, e.gps_lng, e.chk_active AS entite_chk_active
FROM users u
@@ -432,7 +433,7 @@ class LoginController {
}
}
// Section "clients" supprimée car redondante avec le nouveau groupe "amicales"
// 5. Section clients gérée plus bas pour les super-administrateurs
// 6. Récupérer les membres (users de l'entité du user) si nécessaire
if ($interface === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) {
@@ -518,7 +519,8 @@ class LoginController {
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
e.encrypted_email as email, e.gps_lat, e.gps_lng,
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe
FROM entites e
LEFT JOIN x_regions r ON e.fk_region = r.id
WHERE e.id = ? AND e.chk_active = 1'
@@ -531,7 +533,8 @@ class LoginController {
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
e.encrypted_email as email, e.gps_lat, e.gps_lng,
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe
FROM entites e
LEFT JOIN x_regions r ON e.fk_region = r.id
WHERE e.id != 1 AND e.chk_active = 1'
@@ -583,7 +586,8 @@ class LoginController {
'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville,
e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile,
e.encrypted_email as email, e.gps_lat, e.gps_lng,
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe
e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_mdp_manuel, e.chk_username_manuel,
e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe
FROM entites e
LEFT JOIN x_regions r ON e.fk_region = r.id
WHERE e.fk_type = 1 AND e.chk_active = 1'
@@ -636,12 +640,58 @@ class LoginController {
// Ajout des amicales à la racine de la réponse si disponibles
if (!empty($amicalesData)) {
// Récupérer le logo de l'entité de l'utilisateur si elle existe
$logoData = null;
if (!empty($user['fk_entite'])) {
$logoStmt = $this->db->prepare('
SELECT id, fichier, file_path, file_type, mime_type, processed_width, processed_height
FROM medias
WHERE support = ? AND support_id = ? AND file_category = ?
ORDER BY created_at DESC
LIMIT 1
');
$logoStmt->execute(['entite', $user['fk_entite'], 'logo']);
$logo = $logoStmt->fetch(PDO::FETCH_ASSOC);
if ($logo && file_exists($logo['file_path'])) {
// Lire le fichier et l'encoder en base64
$imageData = file_get_contents($logo['file_path']);
if ($imageData !== false) {
$base64 = base64_encode($imageData);
// Format data URL pour usage direct dans Flutter
$dataUrl = 'data:' . $logo['mime_type'] . ';base64,' . $base64;
$logoData = [
'id' => $logo['id'],
'data_url' => $dataUrl, // Image encodée en base64
'file_name' => $logo['fichier'],
'mime_type' => $logo['mime_type'],
'width' => $logo['processed_width'],
'height' => $logo['processed_height']
];
}
}
}
// Si c'est un tableau avec un seul élément, on envoie directement l'objet
// pour que le client reçoive un objet et non un tableau avec un seul objet
if (count($amicalesData) === 1) {
$response['amicale'] = $amicalesData[0];
// Ajouter le logo à l'amicale si disponible
if ($logoData !== null) {
$response['amicale']['logo'] = $logoData;
}
} else {
$response['amicale'] = $amicalesData;
// Pour plusieurs amicales, ajouter le logo à celle de l'utilisateur
if ($logoData !== null && !empty($user['fk_entite'])) {
foreach ($response['amicale'] as &$amicale) {
if ($amicale['id'] == $user['fk_entite']) {
$amicale['logo'] = $logoData;
break;
}
}
}
}
}
@@ -673,7 +723,7 @@ class LoginController {
$response['users_sectors'] = $usersSectorsData;
}
// Section "clients" supprimée car redondante avec le nouveau groupe "amicales"
// 5. Section clients gérée plus bas pour les super-administrateurs
// 9. Récupérer les régions selon le rôle de l'utilisateur
$regionsData = [];

View File

@@ -227,6 +227,26 @@ class UserController {
$role = isset($data['role']) ? (int)$data['role'] : 1;
$entiteId = isset($data['fk_entite']) ? (int)$data['fk_entite'] : 1;
// Récupérer les paramètres de gestion de l'entité
$entiteStmt = $this->db->prepare('
SELECT chk_mdp_manuel, chk_username_manuel, code_postal, ville
FROM entites
WHERE id = ?
');
$entiteStmt->execute([$entiteId]);
$entiteConfig = $entiteStmt->fetch(PDO::FETCH_ASSOC);
if (!$entiteConfig) {
Response::json([
'status' => 'error',
'message' => 'Entité non trouvée'
], 404);
return;
}
$chkMdpManuel = (int)$entiteConfig['chk_mdp_manuel'];
$chkUsernameManuel = (int)$entiteConfig['chk_username_manuel'];
// Vérification des longueurs d'entrée
if (strlen($email) > 75 || strlen($name) > 50) {
Response::json([
@@ -260,9 +280,83 @@ class UserController {
return;
}
// Génération du mot de passe
$password = ApiService::generateSecurePassword();
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
// Gestion du USERNAME selon chk_username_manuel
$encryptedUsername = '';
if ($chkUsernameManuel === 1) {
// Username manuel obligatoire
if (!isset($data['username']) || empty(trim($data['username']))) {
Response::json([
'status' => 'error',
'message' => 'Le nom d\'utilisateur est requis pour cette entité'
], 400);
return;
}
$username = trim(strtolower($data['username']));
// Validation du format du username
if (!preg_match('/^[a-z][a-z0-9._-]{9,29}$/', $username)) {
Response::json([
'status' => 'error',
'message' => 'Format du nom d\'utilisateur invalide (10-30 caractères, commence par une lettre, caractères autorisés: a-z, 0-9, ., -, _)'
], 400);
return;
}
$encryptedUsername = ApiService::encryptSearchableData($username);
// Vérification de l'unicité du username
$checkUsernameStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_user_name = ?');
$checkUsernameStmt->execute([$encryptedUsername]);
if ($checkUsernameStmt->fetch()) {
Response::json([
'status' => 'error',
'message' => 'Ce nom d\'utilisateur est déjà utilisé dans GeoSector'
], 409);
return;
}
} else {
// Génération automatique du username
$username = ApiService::generateUserName(
$this->db,
$name,
$entiteConfig['code_postal'] ?? '00000',
$entiteConfig['ville'] ?? 'ville',
10
);
$encryptedUsername = ApiService::encryptSearchableData($username);
}
// Gestion du MOT DE PASSE selon chk_mdp_manuel
$password = '';
$passwordHash = '';
if ($chkMdpManuel === 1) {
// Mot de passe manuel obligatoire
if (!isset($data['password']) || empty($data['password'])) {
Response::json([
'status' => 'error',
'message' => 'Le mot de passe est requis pour cette entité'
], 400);
return;
}
$password = $data['password'];
// Validation du mot de passe (minimum 8 caractères)
if (strlen($password) < 8) {
Response::json([
'status' => 'error',
'message' => 'Le mot de passe doit contenir au moins 8 caractères'
], 400);
return;
}
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
} else {
// Génération automatique du mot de passe
$password = ApiService::generateSecurePassword();
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
}
// Préparation des champs optionnels
$phone = isset($data['phone']) ? ApiService::encryptData(trim($data['phone'])) : null;
@@ -276,13 +370,13 @@ class UserController {
// Insertion en base de données
$stmt = $this->db->prepare('
INSERT INTO users (
encrypted_email, user_pass_hash, encrypted_name, first_name,
encrypted_email, encrypted_user_name, user_pass_hash, encrypted_name, first_name,
sect_name, encrypted_phone, encrypted_mobile, fk_role,
fk_entite, chk_alert_email, chk_suivi,
date_naissance, date_embauche,
created_at, fk_user_creat, chk_active
) VALUES (
?, ?, ?, ?,
?, ?, ?, ?, ?,
?, ?, ?, ?,
?, ?, ?,
?, ?,
@@ -291,6 +385,7 @@ class UserController {
');
$stmt->execute([
$encryptedEmail,
$encryptedUsername,
$passwordHash,
$encryptedName,
$firstName,
@@ -307,21 +402,54 @@ class UserController {
]);
$userId = $this->db->lastInsertId();
// Envoi de l'email avec les identifiants
ApiService::sendEmail($email, $name, 'welcome', ['password' => $password]);
// Envoi des emails séparés pour plus de sécurité
// 1er email : TOUJOURS envoyer l'identifiant (username)
$usernameEmailData = [
'email' => $email,
'username' => $username,
'name' => $name
];
ApiService::sendEmail($email, $name, 'welcome_username', $usernameEmailData);
// 2ème email : Envoyer le mot de passe (toujours, qu'il soit manuel ou généré)
// Attendre un peu entre les deux emails pour éviter qu'ils arrivent dans le mauvais ordre
sleep(1);
$passwordEmailData = [
'email' => $email,
'password' => $password,
'name' => $name
];
ApiService::sendEmail($email, $name, 'welcome_password', $passwordEmailData);
LogService::log('Utilisateur GeoSector créé', [
'level' => 'info',
'createdBy' => $currentUserId,
'newUserId' => $userId,
'email' => $email
'email' => $email,
'username' => $username,
'usernameManual' => $chkUsernameManuel === 1 ? 'oui' : 'non',
'passwordManual' => $chkMdpManuel === 1 ? 'oui' : 'non',
'emailsSent' => '2 emails (username + password)'
]);
Response::json([
// Préparer la réponse avec les informations de connexion si générées automatiquement
$responseData = [
'status' => 'success',
'message' => 'Utilisateur créé avec succès',
'id' => $userId
], 201);
];
// Ajouter le username dans la réponse (toujours, car nécessaire pour la connexion)
$responseData['username'] = $username;
// Ajouter le mot de passe seulement si généré automatiquement
if ($chkMdpManuel === 0) {
$responseData['password'] = $password;
}
Response::json($responseData, 201);
} catch (PDOException $e) {
LogService::log('Erreur lors de la création d\'un utilisateur GeoSector', [
'level' => 'error',
@@ -756,6 +884,106 @@ class UserController {
}
}
public function checkUsername(): void {
Session::requireAuth();
try {
$data = Request::getJson();
// Validation de la présence du username
if (!isset($data['username']) || empty(trim($data['username']))) {
Response::json([
'status' => 'error',
'message' => 'Username requis pour la vérification'
], 400);
return;
}
$username = trim(strtolower($data['username']));
// Validation du format du username
if (!preg_match('/^[a-z][a-z0-9._-]{9,29}$/', $username)) {
Response::json([
'status' => 'error',
'message' => 'Format invalide : 10-30 caractères, commence par une lettre, caractères autorisés: a-z, 0-9, ., -, _',
'available' => false
], 400);
return;
}
// Chiffrement du username pour la recherche
$encryptedUsername = ApiService::encryptSearchableData($username);
// Vérification de l'existence dans la base
$stmt = $this->db->prepare('
SELECT id, encrypted_name, fk_entite
FROM users
WHERE encrypted_user_name = ?
LIMIT 1
');
$stmt->execute([$encryptedUsername]);
$existingUser = $stmt->fetch(PDO::FETCH_ASSOC);
if ($existingUser) {
// Username déjà pris - générer des suggestions
$baseName = substr($username, 0, -2); // Enlever les 2 derniers caractères
$suggestions = [];
// Génération de 3 suggestions
$suggestions[] = $username . '_' . rand(10, 99);
$suggestions[] = $baseName . rand(100, 999);
// Suggestion avec l'année courante
$year = date('y');
$suggestions[] = $username . $year;
// Vérifier que les suggestions sont aussi disponibles
$availableSuggestions = [];
foreach ($suggestions as $suggestion) {
$encryptedSuggestion = ApiService::encryptSearchableData($suggestion);
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_user_name = ?');
$checkStmt->execute([$encryptedSuggestion]);
if (!$checkStmt->fetch()) {
$availableSuggestions[] = $suggestion;
}
}
// Si aucune suggestion n'est disponible, en générer d'autres
if (empty($availableSuggestions)) {
for ($i = 0; $i < 3; $i++) {
$randomSuffix = rand(1000, 9999);
$availableSuggestions[] = $baseName . $randomSuffix;
}
}
Response::json([
'status' => 'success',
'available' => false,
'message' => 'Ce nom d\'utilisateur est déjà utilisé',
'suggestions' => array_slice($availableSuggestions, 0, 3)
]);
} else {
// Username disponible
Response::json([
'status' => 'success',
'available' => true,
'message' => 'Nom d\'utilisateur disponible',
'username' => $username
]);
}
} catch (PDOException $e) {
LogService::log('Erreur lors de la vérification du username', [
'level' => 'error',
'error' => $e->getMessage()
]);
Response::json([
'status' => 'error',
'message' => 'Erreur serveur lors de la vérification'
], 500);
}
}
// Méthodes auxiliaires
private function validateUpdateData(array $data): ?string {
// Validation de l'email

View File

@@ -38,6 +38,7 @@ class Router {
$this->put('users/:id', ['UserController', 'updateUser']);
$this->delete('users/:id', ['UserController', 'deleteUser']);
$this->post('users/:id/reset-password', ['UserController', 'resetPassword']);
$this->post('users/check-username', ['UserController', 'checkUsername']);
$this->post('logout', ['LoginController', 'logout']);
// Routes entités
@@ -45,6 +46,8 @@ class Router {
$this->get('entites/:id', ['EntiteController', 'getEntiteById']);
$this->get('entites/postal/:code', ['EntiteController', 'getEntiteByPostalCode']);
$this->put('entites/:id', ['EntiteController', 'updateEntite']);
$this->post('entites/:id/logo', ['EntiteController', 'uploadLogo']);
$this->get('entites/:id/logo', ['EntiteController', 'getLogo']);
// Routes opérations
$this->get('operations', ['OperationController', 'getOperations']);

View File

@@ -52,6 +52,16 @@ class ApiService {
$mail->Subject = 'Bienvenue sur GEOSECTOR';
$mail->Body = EmailTemplates::getWelcomeTemplate($name, $data['username'] ?? '', $data['password']);
break;
case 'welcome_username':
$mail->Subject = 'GEOSECTOR - Votre identifiant de connexion';
$mail->Body = EmailTemplates::getWelcomeUsernameTemplate($name, $data['username'] ?? '');
break;
case 'welcome_password':
$mail->Subject = 'GEOSECTOR - Votre mot de passe';
$mail->Body = EmailTemplates::getWelcomePasswordTemplate($name, $data['password'] ?? '');
break;
case 'lostpwd':
$mail->Subject = 'Réinitialisation de votre mot de passe GEOSECTOR';

View File

@@ -17,6 +17,59 @@ class EmailTemplates {
L'équipe GeoSector";
}
/**
* Template d'email de bienvenue - Identifiant uniquement
*/
public static function getWelcomeUsernameTemplate(string $name, string $username): string {
return "
Bonjour $name,<br><br>
Votre compte a été créé avec succès sur <b>GeoSector</b>.<br><br>
Voici votre identifiant de connexion :<br>
<div style='background:#f5f5f5; padding:15px; margin:20px 0; border-left:4px solid #007bff;'>
<b style='font-size:16px;'>Identifiant :</b> <span style='font-size:18px; color:#333;'>$username</span>
</div>
<p style='color:#666; font-size:14px;'>
<b>Important :</b> Conservez précieusement cet identifiant, vous en aurez besoin pour vous connecter.
</p>
<p>
Votre mot de passe vous sera communiqué dans un email séparé pour des raisons de sécurité.
</p>
<p>
Une fois que vous aurez reçu votre mot de passe, vous pourrez vous connecter sur
<a href=\"https://app.geosector.fr\" style='color:#007bff;'>app.geosector.fr</a>
</p>
<br>
À très bientôt,<br>
L'équipe GeoSector";
}
/**
* Template d'email de bienvenue - Mot de passe uniquement
*/
public static function getWelcomePasswordTemplate(string $name, string $password): string {
return "
Bonjour $name,<br><br>
Suite à la création de votre compte <b>GeoSector</b>, voici votre mot de passe :<br><br>
<div style='background:#f5f5f5; padding:15px; margin:20px 0; border-left:4px solid #28a745;'>
<b style='font-size:16px;'>Mot de passe :</b> <span style='font-family:monospace; font-size:18px; color:#333;'>$password</span>
</div>
<p style='color:#d73502; font-size:14px;'>
<b>⚠ Sécurité :</b> Pour garantir la sécurité de votre compte, nous vous recommandons
de conserver ce mot de passe en lieu sûr et de ne jamais le partager.
</p>
<p>
Vous pouvez maintenant vous connecter avec votre identifiant (reçu dans un email précédent)
et ce mot de passe sur <a href=\"https://app.geosector.fr\" style='color:#007bff;'>app.geosector.fr</a>
</p>
<p style='background:#fff3cd; padding:10px; border-radius:5px; margin-top:20px;'>
<b>Rappel :</b> Ne communiquez jamais votre mot de passe à un tiers. L'équipe GeoSector
ne vous demandera jamais votre mot de passe par email ou téléphone.
</p>
<br>
À très bientôt,<br>
L'équipe GeoSector";
}
/**
* Template d'email pour mot de passe perdu
*/

File diff suppressed because one or more lines are too long

View File

@@ -35,13 +35,16 @@ class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
chkStripe: fields[19] as bool,
createdAt: fields[20] as DateTime?,
updatedAt: fields[21] as DateTime?,
chkMdpManuel: fields[22] as bool,
chkUsernameManuel: fields[23] as bool,
logoBase64: fields[24] as String?,
);
}
@override
void write(BinaryWriter writer, AmicaleModel obj) {
writer
..writeByte(22)
..writeByte(25)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@@ -85,7 +88,13 @@ class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
..writeByte(20)
..write(obj.createdAt)
..writeByte(21)
..write(obj.updatedAt);
..write(obj.updatedAt)
..writeByte(22)
..write(obj.chkMdpManuel)
..writeByte(23)
..write(obj.chkUsernameManuel)
..writeByte(24)
..write(obj.logoBase64);
}
@override

View File

@@ -35,13 +35,15 @@ class ClientModelAdapter extends TypeAdapter<ClientModel> {
chkStripe: fields[19] as bool?,
createdAt: fields[20] as DateTime?,
updatedAt: fields[21] as DateTime?,
chkMdpManuel: fields[22] as bool?,
chkUsernameManuel: fields[23] as bool?,
);
}
@override
void write(BinaryWriter writer, ClientModel obj) {
writer
..writeByte(22)
..writeByte(24)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@@ -85,7 +87,11 @@ class ClientModelAdapter extends TypeAdapter<ClientModel> {
..writeByte(20)
..write(obj.createdAt)
..writeByte(21)
..write(obj.updatedAt);
..write(obj.updatedAt)
..writeByte(22)
..write(obj.chkMdpManuel)
..writeByte(23)
..write(obj.chkUsernameManuel);
}
@override

View File

@@ -8,6 +8,7 @@
import 'package:connectivity_plus/src/connectivity_plus_web.dart';
import 'package:geolocator_web/geolocator_web.dart';
import 'package:image_picker_for_web/image_picker_for_web.dart';
import 'package:package_info_plus/src/package_info_plus_web.dart';
import 'package:shared_preferences_web/shared_preferences_web.dart';
import 'package:url_launcher_web/url_launcher_web.dart';
@@ -17,6 +18,7 @@ void registerPlugins([final Registrar? pluginRegistrar]) {
final Registrar registrar = pluginRegistrar ?? webPluginRegistrar;
ConnectivityPlusWebPlugin.registerWith(registrar);
GeolocatorPlugin.registerWith(registrar);
ImagePickerPlugin.registerWith(registrar);
PackageInfoPlusWebPlugin.registerWith(registrar);
SharedPreferencesPlugin.registerWith(registrar);
UrlLauncherPlugin.registerWith(registrar);

File diff suppressed because one or more lines are too long

View File

@@ -86,6 +86,11 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interfa
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/method_channel_connectivity.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/src/enums.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2/lib/cross_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2/lib/src/types/base.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2/lib/src/types/html.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2/lib/src/web_helpers/web_helpers.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2/lib/src/x_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/crypto-3.0.6/lib/crypto.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/crypto-3.0.6/lib/src/digest.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/crypto-3.0.6/lib/src/digest_sink.dart
@@ -103,36 +108,36 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/dart_polylabel2-1.0.0/lib/dart_pol
file:///home/pierre/.pub-cache/hosted/pub.dev/dart_polylabel2-1.0.0/lib/src/impl.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dart_polylabel2-1.0.0/lib/src/point.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dart_polylabel2-1.0.0/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/dio.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/adapter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/adapters/browser_adapter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/cancel_token.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/compute/compute.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/compute/compute_web.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/dio.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/dio/dio_for_browser.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/dio_exception.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/dio_mixin.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/form_data.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/headers.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/interceptor.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/interceptors/imply_content_type.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/interceptors/log.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/multipart_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/multipart_file/browser_multipart_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/options.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/parameter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/progress_stream/browser_progress_stream.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/redirect_record.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/response/response_stream_handler.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/transformers/background_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/transformers/fused_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/transformers/sync_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/transformers/util/consolidate_bytes.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/transformers/util/transform_empty_to_null.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/dio.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/adapter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/adapters/browser_adapter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/cancel_token.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/compute/compute.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/compute/compute_web.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/dio.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/dio/dio_for_browser.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/dio_exception.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/dio_mixin.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/form_data.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/headers.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/interceptor.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/interceptors/imply_content_type.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/interceptors/log.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/multipart_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/multipart_file/browser_multipart_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/options.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/parameter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/progress_stream/browser_progress_stream.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/redirect_record.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/response/response_stream_handler.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/transformers/background_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/transformers/fused_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/transformers/sync_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/transformers/util/consolidate_bytes.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/transformers/util/transform_empty_to_null.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio_cache_interceptor-4.0.3/lib/dio_cache_interceptor.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio_cache_interceptor-4.0.3/lib/src/dio_cache_interceptor.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio_cache_interceptor-4.0.3/lib/src/dio_cache_interceptor_cache_utils.dart
@@ -442,25 +447,25 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/hive_flutter-1.1.0/lib/src/hive_ex
file:///home/pierre/.pub-cache/hosted/pub.dev/hive_flutter-1.1.0/lib/src/stub/path.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/hive_flutter-1.1.0/lib/src/stub/path_provider.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/hive_flutter-1.1.0/lib/src/watch_box_builder.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/http.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/retry.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/abortable.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/base_client.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/base_request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/base_response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/boundary_characters.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/browser_client.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/byte_stream.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/client.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/exception.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/multipart_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/multipart_file_stub.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/multipart_request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/streamed_request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/streamed_response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/http.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/retry.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/abortable.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/base_client.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/base_request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/base_response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/boundary_characters.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/browser_client.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/byte_stream.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/client.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/exception.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/multipart_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/multipart_file_stub.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/multipart_request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/streamed_request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/streamed_response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http_cache_core-1.1.1/lib/http_cache_core.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http_cache_core-1.1.1/lib/src/model/cache/cache.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http_cache_core-1.1.1/lib/src/model/cache/cache_cipher.dart
@@ -497,6 +502,29 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/http_dat
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/media_type.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/scan.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2/lib/image_picker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/image_picker_for_web.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/image_resizer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/image_resizer_utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/pkg_web_tweaks.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/image_picker_platform_interface.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/method_channel/method_channel_image_picker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/platform_interface/image_picker_platform.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/camera_delegate.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/camera_device.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/image_options.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/image_source.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/lost_data_response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/media_options.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/media_selection_type.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/multi_image_picker_options.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/base.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/html.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/lost_data.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/picked_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/retrieve_type.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/types.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/date_symbol_data_custom.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/date_symbols.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/intl.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/number_symbols.dart
@@ -615,6 +643,15 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/bb
file:///home/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/lonlat.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/utm.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/mgrs.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/mime.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/bound_multipart_stream.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/char_code.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/default_extension_map.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/extension.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/magic_number.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_multipart_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_shared.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_type.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/package_info_plus.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_linux.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_web.dart
@@ -736,129 +773,128 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/span_
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/string_scanner.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/string_scanner.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/charts.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/datetime_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/datetime_category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/logarithmic_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/multi_level_labels.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/numeric_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/plot_band.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/base.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/behaviors/crosshair.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/behaviors/trackball.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/behaviors/zooming.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/cartesian_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/circular_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/annotation.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/callbacks.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/chart_point.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/circular_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/circular_data_label_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/connector_line.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/core_legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/core_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/element_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/empty_points.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/funnel_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/interactive_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/layout_handler.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/pyramid_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/title.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/funnel_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/accumulation_distribution_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/atr_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/bollinger_bands_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/ema_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/macd_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/momentum_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/roc_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/rsi_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/sma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/stochastic_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/technical_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/tma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/wma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/interactions/behavior.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/interactions/selection.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/interactions/tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/pyramid_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/box_and_whisker_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/bubble_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/candle_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/chart_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/doughnut_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/error_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/fast_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/funnel_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/hilo_open_close_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/hilo_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/histogram_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/pie_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/pyramid_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/radial_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/range_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/range_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/scatter_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/spline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_area100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_bar100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_column100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_line100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/step_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stepline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/waterfall_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/trendline/trendline.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/utils/constants.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/utils/renderer_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/utils/typedef.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/utils/zooming_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/sparkline/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/sparkline/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/sparkline/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/core.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/calendar/calendar_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/calendar/hijri_date_time.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/license.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/localizations/global_localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/slider_controller.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/assistview_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/barcodes_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/calendar_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/chat_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/color_scheme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/datagrid_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/datapager_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/daterangepicker_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/gauges_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/maps_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/pdfviewer_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/range_selector_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/range_slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/spark_charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/theme_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/treemap_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/utils/shape_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/charts.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/datetime_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/datetime_category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/logarithmic_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/multi_level_labels.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/numeric_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/plot_band.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/base.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/behaviors/crosshair.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/behaviors/trackball.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/behaviors/zooming.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/cartesian_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/circular_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/annotation.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/callbacks.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/chart_point.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/circular_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/circular_data_label_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/connector_line.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/core_legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/core_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/element_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/empty_points.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/funnel_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/interactive_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/layout_handler.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/pyramid_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/title.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/funnel_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/accumulation_distribution_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/atr_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/bollinger_bands_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/ema_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/macd_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/momentum_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/roc_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/rsi_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/sma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/stochastic_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/technical_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/tma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/wma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/interactions/behavior.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/interactions/selection.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/interactions/tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/pyramid_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/box_and_whisker_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/bubble_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/candle_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/chart_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/doughnut_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/error_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/fast_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/funnel_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/hilo_open_close_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/hilo_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/histogram_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/pie_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/pyramid_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/radial_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/range_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/range_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/scatter_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/spline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_area100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_bar100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_column100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_line100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/step_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stepline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/waterfall_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/trendline/trendline.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/constants.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/renderer_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/typedef.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/zooming_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/sparkline/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/sparkline/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/sparkline/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/core.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/calendar/calendar_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/calendar/hijri_date_time.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/localizations/global_localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/slider_controller.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/assistview_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/barcodes_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/calendar_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/chat_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/color_scheme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/datagrid_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/datapager_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/daterangepicker_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/gauges_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/maps_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/pdfviewer_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/range_selector_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/range_slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/spark_charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/theme_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/treemap_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/utils/shape_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/ascii_glyph_set.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/glyph_set.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart
@@ -951,6 +987,7 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_m
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/vector4.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/vector_math.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/vector_math_64.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/helpers.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/accelerometer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/angle_instanced_arrays.dart
@@ -1795,6 +1832,15 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_span.dar
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_state.dart
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/will_pop_scope.dart
file:///home/pierre/dev/flutter/packages/flutter/lib/widgets.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/flutter_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/cupertino_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/l10n/generated_cupertino_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/l10n/generated_date_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/l10n/generated_material_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/l10n/generated_widgets_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/material_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/utils/date_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/widgets_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/flutter_web_plugins.dart
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/navigation/url_strategy.dart
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/navigation/utils.dart
@@ -1858,6 +1904,7 @@ file:///home/pierre/dev/geosector/app/lib/core/services/hive_reset_state_service
file:///home/pierre/dev/geosector/app/lib/core/services/hive_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/hive_web_fix.dart
file:///home/pierre/dev/geosector/app/lib/core/services/location_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/logger_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/sync_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/theme_service.dart
file:///home/pierre/dev/geosector/app/lib/core/theme/app_theme.dart
@@ -1904,7 +1951,7 @@ file:///home/pierre/dev/geosector/app/lib/presentation/widgets/dashboard_app_bar
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/dashboard_layout.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/form_section.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/help_dialog.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/loading_progress_overlay.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/loading_spin_overlay.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/mapbox_map.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/membre_row_widget.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/membre_table_widget.dart

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -86,6 +86,11 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interfa
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/method_channel_connectivity.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/src/enums.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/connectivity_plus_platform_interface-2.0.1/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2/lib/cross_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2/lib/src/types/base.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2/lib/src/types/html.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2/lib/src/web_helpers/web_helpers.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2/lib/src/x_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/crypto-3.0.6/lib/crypto.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/crypto-3.0.6/lib/src/digest.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/crypto-3.0.6/lib/src/digest_sink.dart
@@ -103,36 +108,36 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/dart_polylabel2-1.0.0/lib/dart_pol
file:///home/pierre/.pub-cache/hosted/pub.dev/dart_polylabel2-1.0.0/lib/src/impl.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dart_polylabel2-1.0.0/lib/src/point.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dart_polylabel2-1.0.0/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/dio.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/adapter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/adapters/browser_adapter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/cancel_token.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/compute/compute.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/compute/compute_web.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/dio.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/dio/dio_for_browser.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/dio_exception.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/dio_mixin.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/form_data.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/headers.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/interceptor.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/interceptors/imply_content_type.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/interceptors/log.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/multipart_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/multipart_file/browser_multipart_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/options.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/parameter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/progress_stream/browser_progress_stream.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/redirect_record.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/response/response_stream_handler.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/transformers/background_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/transformers/fused_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/transformers/sync_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/transformers/util/consolidate_bytes.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/transformers/util/transform_empty_to_null.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/dio.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/adapter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/adapters/browser_adapter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/cancel_token.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/compute/compute.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/compute/compute_web.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/dio.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/dio/dio_for_browser.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/dio_exception.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/dio_mixin.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/form_data.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/headers.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/interceptor.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/interceptors/imply_content_type.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/interceptors/log.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/multipart_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/multipart_file/browser_multipart_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/options.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/parameter.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/progress_stream/browser_progress_stream.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/redirect_record.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/response/response_stream_handler.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/transformers/background_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/transformers/fused_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/transformers/sync_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/transformers/util/consolidate_bytes.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/transformers/util/transform_empty_to_null.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio_cache_interceptor-4.0.3/lib/dio_cache_interceptor.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio_cache_interceptor-4.0.3/lib/src/dio_cache_interceptor.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/dio_cache_interceptor-4.0.3/lib/src/dio_cache_interceptor_cache_utils.dart
@@ -442,25 +447,25 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/hive_flutter-1.1.0/lib/src/hive_ex
file:///home/pierre/.pub-cache/hosted/pub.dev/hive_flutter-1.1.0/lib/src/stub/path.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/hive_flutter-1.1.0/lib/src/stub/path_provider.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/hive_flutter-1.1.0/lib/src/watch_box_builder.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/http.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/retry.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/abortable.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/base_client.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/base_request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/base_response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/boundary_characters.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/browser_client.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/byte_stream.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/client.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/exception.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/multipart_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/multipart_file_stub.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/multipart_request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/streamed_request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/streamed_response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/http.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/retry.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/abortable.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/base_client.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/base_request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/base_response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/boundary_characters.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/browser_client.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/byte_stream.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/client.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/exception.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/multipart_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/multipart_file_stub.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/multipart_request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/streamed_request.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/streamed_response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http_cache_core-1.1.1/lib/http_cache_core.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http_cache_core-1.1.1/lib/src/model/cache/cache.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http_cache_core-1.1.1/lib/src/model/cache/cache_cipher.dart
@@ -497,6 +502,29 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/http_dat
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/media_type.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/scan.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/http_parser-4.1.2/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2/lib/image_picker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/image_picker_for_web.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/image_resizer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/image_resizer_utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/src/pkg_web_tweaks.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/image_picker_platform_interface.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/method_channel/method_channel_image_picker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/platform_interface/image_picker_platform.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/camera_delegate.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/camera_device.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/image_options.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/image_source.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/lost_data_response.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/media_options.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/media_selection_type.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/multi_image_picker_options.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/base.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/html.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/lost_data.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/picked_file/picked_file.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/retrieve_type.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/src/types/types.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/date_symbol_data_custom.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/date_symbols.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/intl.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/lib/number_symbols.dart
@@ -615,6 +643,15 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/bb
file:///home/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/lonlat.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/classes/utm.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mgrs_dart-2.0.0/lib/src/mgrs.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/mime.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/bound_multipart_stream.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/char_code.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/default_extension_map.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/extension.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/magic_number.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_multipart_transformer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_shared.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/mime-2.0.0/lib/src/mime_type.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/package_info_plus.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_linux.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/package_info_plus-8.3.0/lib/src/package_info_plus_web.dart
@@ -736,129 +773,128 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/span_
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/string_scanner.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/src/utils.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/string_scanner.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/charts.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/datetime_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/datetime_category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/logarithmic_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/multi_level_labels.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/numeric_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/axis/plot_band.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/base.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/behaviors/crosshair.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/behaviors/trackball.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/behaviors/zooming.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/cartesian_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/circular_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/annotation.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/callbacks.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/chart_point.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/circular_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/circular_data_label_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/connector_line.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/core_legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/core_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/element_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/empty_points.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/funnel_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/interactive_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/layout_handler.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/pyramid_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/common/title.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/funnel_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/accumulation_distribution_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/atr_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/bollinger_bands_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/ema_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/macd_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/momentum_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/roc_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/rsi_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/sma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/stochastic_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/technical_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/tma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/indicators/wma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/interactions/behavior.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/interactions/selection.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/interactions/tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/pyramid_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/box_and_whisker_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/bubble_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/candle_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/chart_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/doughnut_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/error_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/fast_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/funnel_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/hilo_open_close_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/hilo_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/histogram_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/pie_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/pyramid_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/radial_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/range_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/range_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/scatter_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/spline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_area100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_bar100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_column100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_line100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stacked_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/step_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/stepline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/series/waterfall_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/trendline/trendline.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/utils/constants.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/utils/renderer_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/utils/typedef.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/charts/utils/zooming_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/sparkline/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/sparkline/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/src/sparkline/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/core.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/calendar/calendar_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/calendar/hijri_date_time.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/license.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/localizations/global_localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/slider_controller.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/assistview_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/barcodes_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/calendar_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/chat_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/color_scheme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/datagrid_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/datapager_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/daterangepicker_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/gauges_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/maps_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/pdfviewer_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/range_selector_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/range_slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/spark_charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/theme_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/theme/treemap_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/src/utils/shape_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/charts.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/datetime_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/datetime_category_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/logarithmic_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/multi_level_labels.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/numeric_axis.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/axis/plot_band.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/base.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/behaviors/crosshair.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/behaviors/trackball.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/behaviors/zooming.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/cartesian_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/circular_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/annotation.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/callbacks.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/chart_point.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/circular_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/circular_data_label_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/connector_line.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/core_legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/core_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/element_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/empty_points.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/funnel_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/interactive_tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/layout_handler.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/legend.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/pyramid_data_label.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/common/title.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/funnel_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/accumulation_distribution_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/atr_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/bollinger_bands_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/ema_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/macd_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/momentum_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/roc_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/rsi_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/sma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/stochastic_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/technical_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/tma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/indicators/wma_indicator.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/interactions/behavior.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/interactions/selection.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/interactions/tooltip.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/pyramid_chart.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/box_and_whisker_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/bubble_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/candle_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/chart_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/doughnut_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/error_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/fast_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/funnel_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/hilo_open_close_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/hilo_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/histogram_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/pie_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/pyramid_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/radial_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/range_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/range_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/scatter_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/spline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_area100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_bar100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_bar_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_column100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_column_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_line100_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stacked_line_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/step_area_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/stepline_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/series/waterfall_series.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/trendline/trendline.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/constants.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/renderer_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/typedef.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/charts/utils/zooming_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/sparkline/marker.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/sparkline/utils/enum.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/src/sparkline/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/core.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/calendar/calendar_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/calendar/hijri_date_time.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/localizations/global_localizations.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/slider_controller.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/assistview_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/barcodes_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/calendar_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/chat_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/color_scheme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/datagrid_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/datapager_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/daterangepicker_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/gauges_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/maps_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/pdfviewer_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/range_selector_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/range_slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/slider_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/spark_charts_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/theme_widget.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/theme/treemap_theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/utils/helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/src/utils/shape_helper.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/theme.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/ascii_glyph_set.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/glyph_set.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/term_glyph-1.2.2/lib/src/generated/top_level.dart
@@ -951,6 +987,7 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_m
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/src/vector_math_64/vector4.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/vector_math.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/vector_math_64.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/helpers.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/accelerometer.dart
file:///home/pierre/.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/angle_instanced_arrays.dart
@@ -1794,6 +1831,15 @@ file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_span.dar
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/widget_state.dart
file:///home/pierre/dev/flutter/packages/flutter/lib/src/widgets/will_pop_scope.dart
file:///home/pierre/dev/flutter/packages/flutter/lib/widgets.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/flutter_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/cupertino_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/l10n/generated_cupertino_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/l10n/generated_date_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/l10n/generated_material_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/l10n/generated_widgets_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/material_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/utils/date_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/src/widgets_localizations.dart
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/flutter_web_plugins.dart
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/navigation/url_strategy.dart
file:///home/pierre/dev/flutter/packages/flutter_web_plugins/lib/src/navigation/utils.dart
@@ -1857,6 +1903,7 @@ file:///home/pierre/dev/geosector/app/lib/core/services/hive_reset_state_service
file:///home/pierre/dev/geosector/app/lib/core/services/hive_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/hive_web_fix.dart
file:///home/pierre/dev/geosector/app/lib/core/services/location_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/logger_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/sync_service.dart
file:///home/pierre/dev/geosector/app/lib/core/services/theme_service.dart
file:///home/pierre/dev/geosector/app/lib/core/theme/app_theme.dart
@@ -1903,7 +1950,7 @@ file:///home/pierre/dev/geosector/app/lib/presentation/widgets/dashboard_app_bar
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/dashboard_layout.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/form_section.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/help_dialog.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/loading_progress_overlay.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/loading_spin_overlay.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/mapbox_map.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/membre_row_widget.dart
file:///home/pierre/dev/geosector/app/lib/presentation/widgets/membre_table_widget.dart

View File

@@ -8,6 +8,7 @@
import 'package:connectivity_plus/src/connectivity_plus_web.dart';
import 'package:geolocator_web/geolocator_web.dart';
import 'package:image_picker_for_web/image_picker_for_web.dart';
import 'package:package_info_plus/src/package_info_plus_web.dart';
import 'package:shared_preferences_web/shared_preferences_web.dart';
import 'package:url_launcher_web/url_launcher_web.dart';
@@ -17,6 +18,7 @@ void registerPlugins([final Registrar? pluginRegistrar]) {
final Registrar registrar = pluginRegistrar ?? webPluginRegistrar;
ConnectivityPlusWebPlugin.registerWith(registrar);
GeolocatorPlugin.registerWith(registrar);
ImagePickerPlugin.registerWith(registrar);
PackageInfoPlusWebPlugin.registerWith(registrar);
SharedPreferencesPlugin.registerWith(registrar);
UrlLauncherPlugin.registerWith(registrar);

File diff suppressed because one or more lines are too long

View File

@@ -87,7 +87,7 @@
},
{
"name": "built_value",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/built_value-8.11.0",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/built_value-8.11.1",
"packageUri": "lib/",
"languageVersion": "3.0"
},
@@ -151,6 +151,12 @@
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "cross_file",
"rootUri": "file:///home/pierre/.pub-cache/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.6",
@@ -195,7 +201,7 @@
},
{
"name": "dio",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0",
"packageUri": "lib/",
"languageVersion": "2.18"
},
@@ -241,6 +247,30 @@
"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",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "file_selector_macos",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+3",
"packageUri": "lib/",
"languageVersion": "3.6"
},
{
"name": "file_selector_platform_interface",
"rootUri": "file:///home/pierre/.pub-cache/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",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "fixnum",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1",
@@ -295,6 +325,12 @@
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "flutter_localizations",
"rootUri": "file:///home/pierre/dev/flutter/packages/flutter_localizations",
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "flutter_map",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map-8.2.1",
@@ -307,6 +343,12 @@
"packageUri": "lib/",
"languageVersion": "3.6"
},
{
"name": "flutter_plugin_android_lifecycle",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.29",
"packageUri": "lib/",
"languageVersion": "3.6"
},
{
"name": "flutter_svg",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.2.0",
@@ -435,7 +477,7 @@
},
{
"name": "http",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0",
"packageUri": "lib/",
"languageVersion": "3.4"
},
@@ -469,6 +511,54 @@
"packageUri": "lib/",
"languageVersion": "3.0"
},
{
"name": "image_picker",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2",
"packageUri": "lib/",
"languageVersion": "3.3"
},
{
"name": "image_picker_android",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.12+25",
"packageUri": "lib/",
"languageVersion": "3.6"
},
{
"name": "image_picker_for_web",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6",
"packageUri": "lib/",
"languageVersion": "3.4"
},
{
"name": "image_picker_ios",
"rootUri": "file:///home/pierre/.pub-cache/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",
"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",
"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",
"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",
"packageUri": "lib/",
"languageVersion": "2.19"
},
{
"name": "intl",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2",
@@ -717,7 +807,7 @@
},
{
"name": "shared_preferences_android",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.10",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.11",
"packageUri": "lib/",
"languageVersion": "3.6"
},
@@ -819,13 +909,13 @@
},
{
"name": "syncfusion_flutter_charts",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4",
"packageUri": "lib/",
"languageVersion": "3.7"
},
{
"name": "syncfusion_flutter_core",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4",
"packageUri": "lib/",
"languageVersion": "3.7"
},
@@ -891,7 +981,7 @@
},
{
"name": "url_launcher_android",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.16",
"rootUri": "file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.17",
"packageUri": "lib/",
"languageVersion": "3.6"
},

View File

@@ -52,8 +52,8 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/built_collection-5.1.1/
file:///home/pierre/.pub-cache/hosted/pub.dev/built_collection-5.1.1/lib/
built_value
3.0
file:///home/pierre/.pub-cache/hosted/pub.dev/built_value-8.11.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/built_value-8.11.0/lib/
file:///home/pierre/.pub-cache/hosted/pub.dev/built_value-8.11.1/
file:///home/pierre/.pub-cache/hosted/pub.dev/built_value-8.11.1/lib/
characters
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/characters-1.4.0/
@@ -94,6 +94,10 @@ convert
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/convert-3.1.2/
file:///home/pierre/.pub-cache/hosted/pub.dev/convert-3.1.2/lib/
cross_file
3.3
file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2/
file:///home/pierre/.pub-cache/hosted/pub.dev/cross_file-0.3.4+2/lib/
crypto
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/crypto-3.0.6/
@@ -124,8 +128,8 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/dbus-0.7.11/
file:///home/pierre/.pub-cache/hosted/pub.dev/dbus-0.7.11/lib/
dio
2.18
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.8.0+1/lib/
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/dio-5.9.0/lib/
dio_cache_interceptor
3.0
file:///home/pierre/.pub-cache/hosted/pub.dev/dio_cache_interceptor-4.0.3/
@@ -154,6 +158,22 @@ file
3.0
file:///home/pierre/.pub-cache/hosted/pub.dev/file-7.0.1/
file:///home/pierre/.pub-cache/hosted/pub.dev/file-7.0.1/lib/
file_selector_linux
3.3
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2/
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2/lib/
file_selector_macos
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+3/
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.4+3/lib/
file_selector_platform_interface
3.0
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_platform_interface-2.6.2/
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_platform_interface-2.6.2/lib/
file_selector_windows
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+4/
file:///home/pierre/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+4/lib/
fixnum
3.1
file:///home/pierre/.pub-cache/hosted/pub.dev/fixnum-1.1.1/
@@ -194,6 +214,10 @@ flutter_map_cache
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map_cache-2.0.0+1/
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_map_cache-2.0.0+1/lib/
flutter_plugin_android_lifecycle
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.29/
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.29/lib/
flutter_svg
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/flutter_svg-2.2.0/
@@ -272,8 +296,8 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/html-0.15.6/
file:///home/pierre/.pub-cache/hosted/pub.dev/html-0.15.6/lib/
http
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0-beta.2/lib/
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/
file:///home/pierre/.pub-cache/hosted/pub.dev/http-1.5.0/lib/
http_cache_core
3.0
file:///home/pierre/.pub-cache/hosted/pub.dev/http_cache_core-1.1.1/
@@ -294,6 +318,38 @@ image
3.0
file:///home/pierre/.pub-cache/hosted/pub.dev/image-4.5.4/
file:///home/pierre/.pub-cache/hosted/pub.dev/image-4.5.4/lib/
image_picker
3.3
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker-1.1.2/lib/
image_picker_android
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.12+25/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_android-0.8.12+25/lib/
image_picker_for_web
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.6/lib/
image_picker_ios
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.12+2/lib/
image_picker_linux
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+2/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+2/lib/
image_picker_macos
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+2/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+2/lib/
image_picker_platform_interface
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_platform_interface-2.10.1/lib/
image_picker_windows
2.19
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/
file:///home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/lib/
intl
3.3
file:///home/pierre/.pub-cache/hosted/pub.dev/intl-0.20.2/
@@ -460,8 +516,8 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences-2.5.3/lib/
shared_preferences_android
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.10/
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.10/lib/
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.11/
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.11/lib/
shared_preferences_foundation
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.4/
@@ -524,12 +580,12 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/
file:///home/pierre/.pub-cache/hosted/pub.dev/string_scanner-1.4.1/lib/
syncfusion_flutter_charts
3.7
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.1.42/lib/
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_charts-30.2.4/lib/
syncfusion_flutter_core
3.7
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.1.42/lib/
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/
file:///home/pierre/.pub-cache/hosted/pub.dev/syncfusion_flutter_core-30.2.4/lib/
synchronized
3.8
file:///home/pierre/.pub-cache/hosted/pub.dev/synchronized-3.4.0/
@@ -572,8 +628,8 @@ file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher-6.3.2/
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher-6.3.2/lib/
url_launcher_android
3.6
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.16/
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.16/lib/
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.17/
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.17/lib/
url_launcher_ios
3.4
file:///home/pierre/.pub-cache/hosted/pub.dev/url_launcher_ios-6.3.3/
@@ -670,6 +726,10 @@ flutter
3.7
file:///home/pierre/dev/flutter/packages/flutter/
file:///home/pierre/dev/flutter/packages/flutter/lib/
flutter_localizations
3.7
file:///home/pierre/dev/flutter/packages/flutter_localizations/
file:///home/pierre/dev/flutter/packages/flutter_localizations/lib/
flutter_test
3.7
file:///home/pierre/dev/flutter/packages/flutter_test/

View File

@@ -5,7 +5,7 @@
"packages": [
{
"name": "geosector_app",
"version": "3.0.4+304",
"version": "3.0.6+306",
"dependencies": [
"connectivity_plus",
"cupertino_icons",
@@ -13,6 +13,7 @@
"fl_chart",
"flutter",
"flutter_local_notifications",
"flutter_localizations",
"flutter_map",
"flutter_map_cache",
"flutter_svg",
@@ -22,6 +23,7 @@
"hive",
"hive_flutter",
"http_cache_file_store",
"image_picker",
"intl",
"latlong2",
"mqtt5_client",
@@ -101,6 +103,20 @@
"vm_service"
]
},
{
"name": "image_picker",
"version": "1.1.2",
"dependencies": [
"flutter",
"image_picker_android",
"image_picker_for_web",
"image_picker_ios",
"image_picker_linux",
"image_picker_macos",
"image_picker_platform_interface",
"image_picker_windows"
]
},
{
"name": "universal_html",
"version": "2.2.4",
@@ -136,16 +152,6 @@
"intl"
]
},
{
"name": "syncfusion_flutter_charts",
"version": "30.1.42",
"dependencies": [
"flutter",
"intl",
"syncfusion_flutter_core",
"vector_math"
]
},
{
"name": "fl_chart",
"version": "1.0.0",
@@ -203,6 +209,21 @@
"meta"
]
},
{
"name": "flutter_localizations",
"version": "0.0.0",
"dependencies": [
"characters",
"clock",
"collection",
"flutter",
"intl",
"material_color_utilities",
"meta",
"path",
"vector_math"
]
},
{
"name": "flutter",
"version": "0.0.0",
@@ -406,14 +427,6 @@
"package_info_plus"
]
},
{
"name": "syncfusion_flutter_core",
"version": "30.1.42",
"dependencies": [
"flutter",
"vector_math"
]
},
{
"name": "equatable",
"version": "2.0.7",
@@ -489,6 +502,24 @@
"url_launcher_windows"
]
},
{
"name": "syncfusion_flutter_charts",
"version": "30.2.4",
"dependencies": [
"flutter",
"intl",
"syncfusion_flutter_core",
"vector_math"
]
},
{
"name": "syncfusion_flutter_core",
"version": "30.2.4",
"dependencies": [
"flutter",
"vector_math"
]
},
{
"name": "connectivity_plus",
"version": "6.1.4",
@@ -560,6 +591,26 @@
"yaml"
]
},
{
"name": "image_picker_windows",
"version": "0.2.1+1",
"dependencies": [
"file_selector_platform_interface",
"file_selector_windows",
"flutter",
"image_picker_platform_interface"
]
},
{
"name": "image_picker_platform_interface",
"version": "2.10.1",
"dependencies": [
"cross_file",
"flutter",
"http",
"plugin_platform_interface"
]
},
{
"name": "typed_data",
"version": "1.4.0",
@@ -567,6 +618,13 @@
"collection"
]
},
{
"name": "plugin_platform_interface",
"version": "2.1.8",
"dependencies": [
"meta"
]
},
{
"name": "http_cache_core",
"version": "1.1.1",
@@ -656,16 +714,6 @@
"unicode"
]
},
{
"name": "http",
"version": "1.5.0-beta.2",
"dependencies": [
"async",
"http_parser",
"meta",
"web"
]
},
{
"name": "dart_earcut",
"version": "1.2.0",
@@ -701,6 +749,26 @@
"version": "2.7.0",
"dependencies": []
},
{
"name": "image_picker_macos",
"version": "0.2.1+2",
"dependencies": [
"file_selector_macos",
"file_selector_platform_interface",
"flutter",
"image_picker_platform_interface"
]
},
{
"name": "image_picker_linux",
"version": "0.2.1+2",
"dependencies": [
"file_selector_linux",
"file_selector_platform_interface",
"flutter",
"image_picker_platform_interface"
]
},
{
"name": "csslib",
"version": "1.0.2",
@@ -731,6 +799,16 @@
"version": "1.1.1",
"dependencies": []
},
{
"name": "http",
"version": "1.5.0",
"dependencies": [
"async",
"http_parser",
"meta",
"web"
]
},
{
"name": "url_launcher_platform_interface",
"version": "2.3.2",
@@ -739,13 +817,6 @@
"plugin_platform_interface"
]
},
{
"name": "plugin_platform_interface",
"version": "2.1.8",
"dependencies": [
"meta"
]
},
{
"name": "path_provider_windows",
"version": "2.3.0",
@@ -955,6 +1026,33 @@
"url_launcher_platform_interface"
]
},
{
"name": "xdg_directories",
"version": "1.1.0",
"dependencies": [
"meta",
"path"
]
},
{
"name": "file_selector_linux",
"version": "0.9.3+2",
"dependencies": [
"cross_file",
"file_selector_platform_interface",
"flutter"
]
},
{
"name": "file_selector_platform_interface",
"version": "2.6.2",
"dependencies": [
"cross_file",
"flutter",
"http",
"plugin_platform_interface"
]
},
{
"name": "http_parser",
"version": "4.1.2",
@@ -965,14 +1063,6 @@
"typed_data"
]
},
{
"name": "xdg_directories",
"version": "1.1.0",
"dependencies": [
"meta",
"path"
]
},
{
"name": "crypto",
"version": "3.0.6",
@@ -1094,6 +1184,22 @@
"path"
]
},
{
"name": "image_picker_for_web",
"version": "3.0.6",
"dependencies": [
"flutter",
"flutter_web_plugins",
"image_picker_platform_interface",
"mime",
"web"
]
},
{
"name": "mime",
"version": "2.0.0",
"dependencies": []
},
{
"name": "platform",
"version": "3.1.6",
@@ -1151,6 +1257,15 @@
"web"
]
},
{
"name": "file_selector_macos",
"version": "0.9.4+3",
"dependencies": [
"cross_file",
"file_selector_platform_interface",
"flutter"
]
},
{
"name": "logger",
"version": "2.6.1",
@@ -1203,15 +1318,24 @@
"vector_graphics_codec"
]
},
{
"name": "path_provider_android",
"version": "2.2.17",
"dependencies": [
"flutter",
"path_provider_platform_interface"
]
},
{
"name": "dio",
"version": "5.8.0+1",
"version": "5.9.0",
"dependencies": [
"async",
"collection",
"dio_web_adapter",
"http_parser",
"meta",
"mime",
"path"
]
},
@@ -1225,17 +1349,9 @@
"web"
]
},
{
"name": "path_provider_android",
"version": "2.2.17",
"dependencies": [
"flutter",
"path_provider_platform_interface"
]
},
{
"name": "shared_preferences_android",
"version": "2.4.10",
"version": "2.4.11",
"dependencies": [
"flutter",
"shared_preferences_platform_interface"
@@ -1434,16 +1550,6 @@
"path"
]
},
{
"name": "built_value",
"version": "8.11.0",
"dependencies": [
"built_collection",
"collection",
"fixnum",
"meta"
]
},
{
"name": "watcher",
"version": "1.1.2",
@@ -1484,6 +1590,16 @@
"collection"
]
},
{
"name": "built_value",
"version": "8.11.1",
"dependencies": [
"built_collection",
"collection",
"fixnum",
"meta"
]
},
{
"name": "stream_transform",
"version": "2.1.1",
@@ -1540,11 +1656,6 @@
"web_socket_channel"
]
},
{
"name": "mime",
"version": "2.0.0",
"dependencies": []
},
{
"name": "js",
"version": "0.7.2",
@@ -1602,6 +1713,14 @@
"stream_channel"
]
},
{
"name": "image_picker_ios",
"version": "0.8.12+2",
"dependencies": [
"flutter",
"image_picker_platform_interface"
]
},
{
"name": "win32",
"version": "5.14.0",
@@ -1609,13 +1728,46 @@
"ffi"
]
},
{
"name": "file_selector_windows",
"version": "0.9.3+4",
"dependencies": [
"cross_file",
"file_selector_platform_interface",
"flutter"
]
},
{
"name": "cross_file",
"version": "0.3.4+2",
"dependencies": [
"meta",
"web"
]
},
{
"name": "url_launcher_android",
"version": "6.3.16",
"version": "6.3.17",
"dependencies": [
"flutter",
"url_launcher_platform_interface"
]
},
{
"name": "image_picker_android",
"version": "0.8.12+25",
"dependencies": [
"flutter",
"flutter_plugin_android_lifecycle",
"image_picker_platform_interface"
]
},
{
"name": "flutter_plugin_android_lifecycle",
"version": "2.0.29",
"dependencies": [
"flutter"
]
}
],
"configVersion": 1

File diff suppressed because one or more lines are too long

View File

@@ -96,6 +96,7 @@ GEOSECTOR est une solution complète développée en Flutter qui révolutionne l
| **Géolocalisation** | Geolocator | 10.1.0 | Services de localisation |
| **Chat** | MQTT5 Client | 4.2.0 | Messagerie temps réel |
| **UI** | Material Design 3 | Native | Composants d'interface |
| **Logging** | LoggerService | Custom | Logs conditionnels par env |
### 🏛️ Architecture en couches
@@ -423,6 +424,355 @@ Interface : Affichage utilisateur via ApiException.showError()
Network Errors : "Problème de connexion réseau"
Timeout : "Délai d'attente dépassé"
## 🔧 Pattern de gestion des erreurs API dans les Repositories
### 🎯 Problème à résoudre
Les messages d'erreur spécifiques de l'API (comme "Cet email est déjà utilisé") n'étaient pas affichés à l'utilisateur. À la place, un message générique "Erreur inattendue" apparaissait.
### 📝 Modifications requises
#### **1. ApiService - Conversion automatique des DioException**
Toutes les méthodes HTTP génériques doivent convertir les `DioException` en `ApiException` :
```dart
// ✅ CORRECT - ApiService avec conversion automatique
Future<Response> put(String path, {dynamic data}) async {
try {
return await _dio.put(path, data: data);
} on DioException catch (e) {
throw ApiException.fromDioException(e); // ← Extraction automatique du message
} catch (e) {
if (e is ApiException) rethrow;
throw ApiException('Erreur inattendue lors de la requête PUT', originalError: e);
}
}
```
Appliquer le même pattern pour `post()`, `get()`, `delete()`.
#### **2. Repository - Simplification de la gestion des erreurs**
```dart
// ❌ INCORRECT - Code inutile qui ne sera jamais exécuté
Future<bool> updateMembre(MembreModel membre) async {
try {
final response = await ApiService.instance.put('/users/${membre.id}', data: data);
if (response.statusCode == 200) {
await saveMembreBox(membre);
return true;
}
// ⚠️ CE CODE NE SERA JAMAIS ATTEINT car Dio lance une exception pour les codes d'erreur
if (response.data != null && response.data is Map<String, dynamic>) {
final responseData = response.data as Map<String, dynamic>;
if (responseData['status'] == 'error') {
throw Exception(responseData['message']);
}
}
return false;
} catch (e) {
rethrow;
}
}
```
```dart
// ✅ CORRECT - Code simplifié et fonctionnel
Future<bool> updateMembre(MembreModel membre) async {
try {
final response = await ApiService.instance.put('/users/${membre.id}', data: data);
// Si on arrive ici, c'est que la requête a réussi (200)
await saveMembreBox(membre);
return true;
} catch (e) {
// L'ApiException contient déjà le message extrait de l'API
rethrow; // Propager l'exception pour affichage
}
}
```
#### **3. Amélioration des logs (avec LoggerService)**
```dart
// ✅ CORRECT - Logs propres sans détails techniques
catch (e) {
// Ne pas logger les détails techniques de DioException
if (e is ApiException) {
LoggerService.error('Erreur lors de la mise à jour: ${e.message}');
} else {
LoggerService.error('Erreur lors de la mise à jour');
}
rethrow;
}
```
N'oubliez pas d'importer `ApiException` :
```dart
import 'package:geosector_app/core/utils/api_exception.dart';
```
### 🔄 Flux d'erreur corrigé
```mermaid
sequenceDiagram
participant API as API Server
participant Dio as Dio Client
participant AS as ApiService
participant AE as ApiException
participant R as Repository
participant UI as Interface
UI->>R: updateData()
R->>AS: put('/endpoint')
AS->>Dio: HTTP PUT
Dio->>API: Request
alt Code d'erreur (409, 400, etc.)
API-->>Dio: Error + JSON body
Note over API: {"status": "error",<br/>"message": "Message spécifique"}
Dio-->>AS: DioException
AS->>AE: fromDioException()
Note over AE: Extrait message<br/>depuis response.data
AE-->>AS: ApiException("Message spécifique")
AS-->>R: throw ApiException
R-->>UI: throw ApiException
UI->>UI: showError("Message spécifique")
else Succès (200, 201)
API-->>Dio: Success
Dio-->>AS: Response
AS-->>R: Response
R->>R: Sauvegarde Hive
R-->>UI: return true
end
```
### ✅ Checklist de migration
Pour chaque repository :
- [ ] Vérifier que l'ApiService convertit les DioException en ApiException
- [ ] Simplifier le code : supprimer les vérifications de statut après l'appel API
- [ ] Propager les exceptions avec `rethrow`
- [ ] Améliorer les logs pour ne pas afficher les détails techniques
- [ ] Importer `ApiException` si nécessaire
- [ ] Tester avec une erreur 409 pour vérifier l'affichage du message
### 📊 Résultat attendu
| Avant | Après |
|-------|-------|
| "Erreur inattendue" | "Cet email est déjà utilisé par un autre utilisateur" |
| Logs avec stack trace Dio | Message d'erreur simple et clair |
| Code complexe avec vérifications inutiles | Code simplifié et maintenable |
Cette approche garantit que tous les messages d'erreur de l'API sont correctement affichés à l'utilisateur, améliorant ainsi l'expérience utilisateur et facilitant le débogage.
## 📝 Service de Logging Intelligent
### 🎯 Vue d'ensemble
GEOSECTOR v2.0 implémente un **LoggerService centralisé** qui désactive automatiquement les logs de debug en production, optimisant ainsi les performances et la sécurité tout en facilitant le développement.
### 🔍 Détection automatique de l'environnement
Le LoggerService détecte automatiquement l'environnement d'exécution :
```dart
// Détection basée sur l'URL pour le web
if (currentUrl.contains('dapp.geosector.fr')) DEV
if (currentUrl.contains('rapp.geosector.fr')) REC
Sinon PROD
// Pour mobile/desktop : utilise kReleaseMode de Flutter
```
**Comportement par environnement :**
| Environnement | Logs Debug | Logs Erreur | Stack Traces |
|---------------|------------|-------------|--------------|
| **DEV** | ✅ Activés | ✅ Activés | ✅ Complètes |
| **REC** | ✅ Activés | ✅ Activés | ✅ Complètes |
| **PROD** | ❌ Désactivés | ✅ Activés | ❌ Masquées |
### 🛠️ API du LoggerService
#### **Méthodes principales**
```dart
// Remplacement direct de debugPrint
LoggerService.log('Message simple');
// Logs catégorisés avec emojis automatiques
LoggerService.info('Information'); //
LoggerService.success('Opération réussie'); // ✅
LoggerService.warning('Attention'); // ⚠️
LoggerService.error('Erreur', exception); // ❌ (toujours affiché)
LoggerService.debug('Debug', emoji: '🔧'); // 🔧
// Logs spécialisés
LoggerService.api('Requête API envoyée'); // 🔗
LoggerService.database('Box Hive ouverte'); // 💾
LoggerService.navigation('Route: /admin'); // 🧭
LoggerService.performance('Temps: 45ms'); // ⏱️
```
#### **Fonctionnalités avancées**
```dart
// Logs groupés pour améliorer la lisibilité
LoggerService.group('Traitement utilisateur', [
'Validation des données',
'Appel API',
'Sauvegarde locale',
'Notification envoyée'
]);
// Affiche :
// ┌─ Traitement utilisateur
// ├─ Validation des données
// ├─ Appel API
// ├─ Sauvegarde locale
// └─ Notification envoyée
// Log JSON formaté
LoggerService.json('Payload API', {
'id': 123,
'name': 'Test',
'active': true
});
// Affiche :
// 📋 Payload API:
// id: 123
// name: Test
// active: true
// Log conditionnel
LoggerService.conditional(
'Debug spécifique',
condition: user.role > 2
);
```
### 🔄 Migration depuis debugPrint
#### **Avant (debugPrint partout)**
```dart
debugPrint('🔄 Début de la création d\'un nouveau membre');
debugPrint('📤 Données envoyées à l\'API: $data');
debugPrint('❌ Erreur: $e');
```
#### **Après (LoggerService)**
```dart
LoggerService.info('Début de la création d\'un nouveau membre');
LoggerService.api('Données envoyées à l\'API: $data');
LoggerService.error('Erreur lors de la création', e);
```
### 📋 Utilisation avec les extensions
Pour une syntaxe encore plus concise, utilisez les extensions String :
```dart
// Import nécessaire
import 'package:geosector_app/core/services/logger_service.dart';
// Utilisation directe sur les strings
'Connexion réussie'.logSuccess();
'Erreur de validation'.logError(exception);
'Requête GET /users'.logApi();
'Box membres ouverte'.logDatabase();
```
### 🎯 Bonnes pratiques
#### **1. Catégorisation des logs**
```dart
// ✅ BON - Log catégorisé et clair
LoggerService.api('POST /users - Création membre');
LoggerService.database('Sauvegarde dans Box membres');
// ❌ MAUVAIS - Log générique sans contexte
debugPrint('Traitement en cours...');
```
#### **2. Gestion des erreurs**
```dart
try {
await operation();
LoggerService.success('Opération terminée');
} catch (e, stackTrace) {
// Les erreurs sont TOUJOURS loggées, même en PROD
LoggerService.error('Échec de l\'opération', e, stackTrace);
}
```
#### **3. Logs de performance**
```dart
final stopwatch = Stopwatch()..start();
await heavyOperation();
stopwatch.stop();
LoggerService.performance('Opération lourde: ${stopwatch.elapsedMilliseconds}ms');
```
### 🔒 Sécurité et performances
#### **Avantages en production**
1. **Sécurité** : Aucune information sensible exposée dans la console
2. **Performance** : Pas d'appels inutiles à debugPrint
3. **Taille** : Bundle JavaScript plus léger (tree shaking)
4. **Professionnalisme** : Console propre pour les utilisateurs finaux
#### **Conservation des logs d'erreur**
Les erreurs restent visibles en production pour faciliter le support :
```dart
// Toujours affiché, même en PROD
LoggerService.error('Erreur critique détectée', error);
// Mais sans la stack trace complète qui pourrait révéler
// des détails d'implémentation
```
### 📊 Impact sur le codebase
| Métrique | Avant | Après |
|----------|-------|-------|
| **Logs en PROD** | Tous visibles | Erreurs uniquement |
| **Performance web** | debugPrint actifs | Désactivés automatiquement |
| **Maintenance** | debugPrint dispersés | Service centralisé |
| **Lisibilité** | Emojis manuels | Catégorisation automatique |
### 🚀 Configuration et initialisation
Le LoggerService fonctionne automatiquement sans configuration :
```dart
// main.dart - Aucune initialisation requise
void main() async {
// LoggerService détecte automatiquement l'environnement
// via ApiService.getCurrentEnvironment()
runApp(GeosectorApp());
}
// Utilisation immédiate dans n'importe quel fichier
LoggerService.info('Application démarrée');
```
Cette architecture garantit un système de logging professionnel, sécurisé et performant, adapté aux besoins de développement tout en protégeant la production. 📝✨
## 🎯 Gestion des rôles
### Hiérarchie des permissions

766
app/TODO-APP.md Normal file
View File

@@ -0,0 +1,766 @@
# TODO-APP.md
## 📋 Liste des tâches à effectuer sur l'application GEOSECTOR
### 🔧 Migration du pattern de gestion des erreurs API
#### 🎯 Objectif
Appliquer le nouveau pattern de gestion des erreurs pour que tous les messages spécifiques de l'API soient correctement affichés aux utilisateurs (au lieu du message générique "Erreur inattendue").
---
### ✅ Repositories déjà migrés
- [x] **MembreRepository** (`lib/core/repositories/membre_repository.dart`)
- ✅ Conversion DioException → ApiException
- ✅ Simplification du code
- ✅ Logs avec LoggerService
---
### 📝 Repositories à migrer
#### 1. **UserRepository** (`lib/core/repositories/user_repository.dart`)
- [ ] Vérifier/ajouter l'import `ApiException`
- [ ] Simplifier `updateUser()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `syncUser()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `syncAllUsers()` - supprimer les vérifications de statut après l'appel API
- [ ] Remplacer `debugPrint` par `LoggerService`
- [ ] Améliorer la gestion des erreurs dans le bloc catch (ne pas logger DioException)
#### 2. **OperationRepository** (`lib/core/repositories/operation_repository.dart`)
- [ ] Vérifier/ajouter l'import `ApiException`
- [ ] Simplifier `createOperation()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `updateOperation()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `deleteOperation()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `activateOperation()` - supprimer les vérifications de statut après l'appel API
- [ ] Remplacer `debugPrint` par `LoggerService`
- [ ] Améliorer la gestion des erreurs dans le bloc catch (ne pas logger DioException)
#### 3. **PassageRepository** (`lib/core/repositories/passage_repository.dart`)
- [ ] Vérifier/ajouter l'import `ApiException`
- [ ] Simplifier `createPassage()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `updatePassage()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `updatePassageStatus()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `syncPassages()` - supprimer les vérifications de statut après l'appel API
- [ ] Remplacer `debugPrint` par `LoggerService`
- [ ] Améliorer la gestion des erreurs dans le bloc catch (ne pas logger DioException)
#### 4. **SectorRepository** (`lib/core/repositories/sector_repository.dart`)
- [ ] Vérifier/ajouter l'import `ApiException`
- [ ] Simplifier `createSector()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `updateSector()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `deleteSector()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `assignUserToSector()` - supprimer les vérifications de statut après l'appel API
- [ ] Remplacer `debugPrint` par `LoggerService`
- [ ] Améliorer la gestion des erreurs dans le bloc catch (ne pas logger DioException)
#### 5. **AmicaleRepository** (`lib/core/repositories/amicale_repository.dart`)
- [ ] Vérifier/ajouter l'import `ApiException`
- [ ] Simplifier `updateAmicale()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `createAmicale()` - supprimer les vérifications de statut après l'appel API
- [ ] Simplifier `syncAmicales()` - supprimer les vérifications de statut après l'appel API
- [ ] Remplacer `debugPrint` par `LoggerService`
- [ ] Améliorer la gestion des erreurs dans le bloc catch (ne pas logger DioException)
---
### 🛠️ Pattern à appliquer
#### ❌ Code à supprimer (ancien pattern)
```dart
try {
final response = await ApiService.instance.put('/endpoint', data: data);
if (response.statusCode == 200) {
// Traitement succès
return true;
}
// ⚠️ Ce code ne sera JAMAIS exécuté
if (response.data != null && response.data is Map<String, dynamic>) {
final responseData = response.data as Map<String, dynamic>;
if (responseData['status'] == 'error') {
throw Exception(responseData['message']);
}
}
return false;
} catch (e) {
debugPrint('Erreur: $e');
rethrow;
}
```
#### ✅ Nouveau pattern à utiliser
```dart
try {
final response = await ApiService.instance.put('/endpoint', data: data);
// Si on arrive ici, la requête a réussi (200/201)
// Traitement succès
return true;
} catch (e) {
// Log propre sans détails techniques
if (e is ApiException) {
LoggerService.error('Erreur lors de l\'opération: ${e.message}');
} else {
LoggerService.error('Erreur lors de l\'opération');
}
rethrow; // Propager pour affichage UI
}
```
---
### 📦 Imports nécessaires
Ajouter ces imports dans chaque repository si manquants :
```dart
import 'package:geosector_app/core/services/logger_service.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
```
---
### 🧪 Tests de validation
Pour chaque repository migré, tester :
1. **Test d'erreur 409 (Conflict)** :
- Créer/modifier avec un email déjà existant
- Vérifier que le message "Cet email est déjà utilisé" s'affiche
2. **Test d'erreur 400 (Bad Request)** :
- Envoyer des données invalides
- Vérifier que le message d'erreur spécifique s'affiche
3. **Test d'erreur réseau** :
- Couper la connexion
- Vérifier que "Problème de connexion réseau" s'affiche
4. **Test de succès** :
- Opération normale
- Vérifier que tout fonctionne comme avant
---
### 📊 Critères de succès
- [ ] Tous les repositories utilisent le nouveau pattern
- [ ] Plus aucun `debugPrint` dans les repositories (remplacé par `LoggerService`)
- [ ] Les messages d'erreur spécifiques de l'API s'affichent correctement
- [ ] Les logs en production sont désactivés (sauf erreurs)
- [ ] Le code est plus simple et maintenable
---
### 🔍 Commandes utiles pour vérifier
```bash
# Rechercher les anciens patterns à remplacer
grep -r "response.statusCode ==" lib/core/repositories/
grep -r "responseData\['status'\] == 'error'" lib/core/repositories/
grep -r "debugPrint" lib/core/repositories/
# Vérifier les imports manquants
grep -L "LoggerService" lib/core/repositories/*.dart
grep -L "ApiException" lib/core/repositories/*.dart
```
---
### 📅 Priorité
1. **Haute** : UserRepository (utilisé partout)
2. **Haute** : OperationRepository (fonctionnalité critique)
3. **Moyenne** : PassageRepository (utilisé fréquemment)
4. **Moyenne** : AmicaleRepository (gestion administrative)
5. **Basse** : SectorRepository (moins critique)
---
### 📝 Notes
- Cette migration améliore l'UX en affichant des messages d'erreur clairs
- Le LoggerService désactive automatiquement les logs en production
- Le code devient plus maintenable et cohérent
- Documenter dans README-APP.md une fois terminé
---
## 🧪 Tests unitaires Flutter à implémenter
### 📁 Structure des tests
Le dossier `/test` doit contenir des tests unitaires pour valider le comportement de l'application.
### 📝 Tests prioritaires à créer
#### 1. **Tests des Repositories** (`test/repositories/`)
##### `test/repositories/membre_repository_test.dart`
- [ ] Test de création d'un membre avec succès
- [ ] Test de création avec email déjà existant (409)
- [ ] Test de mise à jour d'un membre
- [ ] Test de suppression d'un membre
- [ ] Test de gestion des erreurs réseau
- [ ] Test du cache Hive local
##### `test/repositories/user_repository_test.dart`
- [ ] Test de connexion réussie
- [ ] Test de connexion avec mauvais identifiants
- [ ] Test de mise à jour du profil utilisateur
- [ ] Test de synchronisation des données
- [ ] Test de gestion de session
##### `test/repositories/operation_repository_test.dart`
- [ ] Test de création d'opération
- [ ] Test d'activation/désactivation
- [ ] Test de récupération des opérations par amicale
- [ ] Test de suppression avec transfert de passages
#### 2. **Tests des Services** (`test/services/`)
##### `test/services/api_service_test.dart`
- [ ] Test de détection d'environnement (DEV/REC/PROD)
- [ ] Test de gestion des sessions
- [ ] Test de conversion DioException vers ApiException
- [ ] Test des différents codes d'erreur HTTP
- [ ] Test du retry automatique
##### `test/services/logger_service_test.dart`
- [ ] Test de désactivation des logs en PROD
- [ ] Test des différents niveaux de log
- [ ] Test du formatage des messages
##### `test/services/hive_service_test.dart`
- [ ] Test d'initialisation des boîtes Hive
- [ ] Test de sauvegarde et récupération
- [ ] Test de suppression de données
- [ ] Test de migration de schéma
#### 3. **Tests des Models** (`test/models/`)
##### `test/models/membre_model_test.dart`
- [ ] Test de sérialisation JSON
- [ ] Test de désérialisation JSON
- [ ] Test de conversion vers UserModel
- [ ] Test des valeurs par défaut
##### `test/models/amicale_model_test.dart`
- [ ] Test de sérialisation/désérialisation
- [ ] Test des nouveaux champs booléens (chk_mdp_manuel, chk_username_manuel)
- [ ] Test de validation des données
#### 4. **Tests des Widgets** (`test/widgets/`)
##### `test/widgets/user_form_test.dart`
- [ ] Test d'affichage conditionnel des champs (username/password)
- [ ] Test de validation du mot de passe (regex)
- [ ] Test de validation de l'username (pas d'espaces)
- [ ] Test de génération automatique d'username
- [ ] Test du mode création vs modification
##### `test/widgets/membre_table_widget_test.dart`
- [ ] Test d'affichage responsive (mobile vs web)
- [ ] Test de masquage des colonnes sur mobile
- [ ] Test des icônes de statut
- [ ] Test du tri et filtrage
#### 5. **Tests d'intégration** (`test/integration/`)
##### `test/integration/auth_flow_test.dart`
- [ ] Test du flow complet de connexion
- [ ] Test de persistance de session
- [ ] Test de déconnexion
- [ ] Test de redirection après expiration
##### `test/integration/membre_management_test.dart`
- [ ] Test de création complète d'un membre
- [ ] Test de modification avec validation
- [ ] Test de suppression avec transfert
- [ ] Test de gestion des erreurs
### 🛠️ Configuration des tests
#### Fichier `test/test_helpers.dart`
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:dio/dio.dart';
import 'package:hive_flutter/hive_flutter.dart';
// Mocks nécessaires
class MockDio extends Mock implements Dio {}
class MockBox<T> extends Mock implements Box<T> {}
class MockApiService extends Mock implements ApiService {}
// Helper pour initialiser Hive en tests
Future<void> setupTestHive() async {
await Hive.initFlutter();
// Initialiser les boîtes de test
}
// Helper pour nettoyer après les tests
Future<void> tearDownTestHive() async {
await Hive.deleteFromDisk();
}
// Helper pour créer des données de test
class TestDataFactory {
static UserModel createTestUser() {
return UserModel(
id: 1,
username: 'test_user',
email: 'test@example.com',
// ...
);
}
static MembreModel createTestMembre() {
return MembreModel(
id: 1,
fkEntite: 100,
name: 'Test',
firstName: 'User',
// ...
);
}
}
```
### 📋 Commandes de test
```bash
# Lancer tous les tests
flutter test
# Lancer un test spécifique
flutter test test/repositories/membre_repository_test.dart
# Lancer avec coverage
flutter test --coverage
# Générer un rapport HTML de couverture
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
```
### 🎯 Objectifs de couverture
- [ ] **Repositories** : 80% minimum
- [ ] **Services** : 90% minimum
- [ ] **Models** : 95% minimum
- [ ] **Widgets critiques** : 70% minimum
- [ ] **Couverture globale** : 75% minimum
### 📚 Dépendances de test à ajouter
Dans `pubspec.yaml` :
```yaml
dev_dependencies:
flutter_test:
sdk: flutter
mockito: ^5.4.4
build_runner: ^2.4.8
test: ^1.25.2
flutter_lints: ^4.0.0
coverage: ^1.7.2
```
### 🔄 Intégration CI/CD
Ajouter dans le pipeline CI :
```yaml
test:
stage: test
script:
- flutter test --coverage
- genhtml coverage/lcov.info -o coverage/html
artifacts:
paths:
- coverage/
coverage: '/lines\.*: \d+\.\d+\%/'
```
### 📝 Bonnes pratiques
1. **Isolation** : Chaque test doit être indépendant
2. **Mocking** : Utiliser des mocks pour les dépendances externes
3. **Nommage** : Utiliser des noms descriptifs (test_should_xxx_when_yyy)
4. **AAA** : Suivre le pattern Arrange-Act-Assert
5. **Edge cases** : Tester les cas limites et erreurs
6. **Performance** : Les tests unitaires doivent être rapides (<100ms)
---
## 💬 Module Chat en ligne GEOSECTOR
### 📋 Vue d'ensemble
Le module chat est partiellement implémenté avec une architecture MQTT pour les notifications en temps réel. Il nécessite des développements supplémentaires pour être pleinement fonctionnel.
### 🏗️ Architecture existante
```
lib/chat/
├── models/ # ✅ Modèles créés avec Hive
│ ├── conversation_model.dart # Conversations (one-to-one, groupe, annonce)
│ ├── message_model.dart # Messages avec pièces jointes
│ ├── participant_model.dart # Participants aux conversations
│ └── audience_target_model.dart # Cibles pour les annonces
├── repositories/ # ⚠️ À compléter
│ └── chat_repository.dart # Logique métier du chat
├── services/ # ⚠️ Partiellement implémenté
│ ├── chat_api_service.dart # Communication avec l'API
│ ├── offline_queue_service.dart # File d'attente hors ligne
│ └── notifications/ # 🔧 MQTT configuré
│ ├── mqtt_notification_service.dart
│ └── mqtt_config.dart
├── widgets/ # ⚠️ À implémenter
│ ├── chat_screen.dart # Écran principal du chat
│ ├── conversations_list.dart # Liste des conversations
│ ├── message_bubble.dart # Bulles de messages
│ └── chat_input.dart # Zone de saisie
└── pages/ # ⚠️ À compléter
└── chat_page.dart # Page principale
```
### 📝 Tâches de développement Chat
#### 1. **Finalisation des modèles Hive**
- [ ] Compléter les adaptateurs Hive pour tous les modèles
- [ ] Ajouter la gestion des pièces jointes (images, documents)
- [ ] Implémenter le modèle `AnonymousUserModel` pour les utilisateurs temporaires
- [ ] Ajouter les timestamps et statuts de lecture
#### 2. **Repository ChatRepository**
- [ ] Implémenter `createConversation()` avec participants
- [ ] Implémenter `sendMessage()` avec queue hors ligne
- [ ] Implémenter `getConversations()` avec pagination
- [ ] Implémenter `getMessages()` avec lazy loading
- [ ] Ajouter la gestion des participants (ajout/suppression)
- [ ] Implémenter les annonces ciblées par groupe
#### 3. **Services**
- [ ] Compléter `ChatApiService` avec endpoints REST
- [ ] Implémenter la synchronisation bidirectionnelle
- [ ] Configurer `OfflineQueueService` pour messages en attente
- [ ] Implémenter le retry automatique avec exponential backoff
- [ ] Ajouter la compression des images avant envoi
#### 4. **Notifications MQTT**
- [ ] Installer et configurer Mosquitto dans le container Incus
- [ ] Configurer SSL/TLS pour MQTT (port 8883)
- [ ] Implémenter l'authentification par token JWT
- [ ] Créer les topics par utilisateur/groupe/conversation
- [ ] Implémenter le système de présence (online/offline/typing)
- [ ] Ajouter les ACLs pour sécuriser les topics
#### 5. **Interface utilisateur**
- [ ] Créer `ChatScreen` avec liste de messages
- [ ] Implémenter `ConversationsList` avec badges non-lus
- [ ] Designer `MessageBubble` (texte, images, documents)
- [ ] Créer `ChatInput` avec:
- [ ] Saisie de texte avec emoji picker
- [ ] Bouton d'envoi de fichiers
- [ ] Indicateur "en train d'écrire"
- [ ] Enregistrement vocal (optionnel)
- [ ] Ajouter les animations (apparition messages, typing indicator)
- [ ] Implémenter le swipe pour répondre
- [ ] Ajouter la recherche dans les conversations
#### 6. **Fonctionnalités avancées**
- [ ] Notifications push locales via `flutter_local_notifications`
- [ ] Chiffrement end-to-end des messages sensibles
- [ ] Réactions aux messages (emojis)
- [ ] Messages éphémères avec auto-suppression
- [ ] Partage de localisation en temps réel
- [ ] Appels audio/vidéo via WebRTC (phase 2)
### 🔧 Configuration backend requise
#### Base de données
```sql
-- Tables à créer (voir chat_tables.sql)
CREATE TABLE chat_conversations (
id INT PRIMARY KEY AUTO_INCREMENT,
type ENUM('one_to_one', 'group', 'announcement'),
created_by INT,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE TABLE chat_messages (
id INT PRIMARY KEY AUTO_INCREMENT,
conversation_id INT,
sender_id INT,
content TEXT,
type ENUM('text', 'image', 'file', 'location'),
status ENUM('sent', 'delivered', 'read'),
created_at TIMESTAMP
);
CREATE TABLE chat_participants (
conversation_id INT,
user_id INT,
role ENUM('admin', 'member'),
joined_at TIMESTAMP,
last_read_message_id INT
);
```
#### Configuration MQTT
```bash
# Installation Mosquitto
apt-get install mosquitto mosquitto-clients
# Configuration /etc/mosquitto/mosquitto.conf
listener 1883
listener 8883
cafile /etc/mosquitto/ca.crt
certfile /etc/mosquitto/server.crt
keyfile /etc/mosquitto/server.key
allow_anonymous false
password_file /etc/mosquitto/passwd
```
### 📦 Dépendances à ajouter
```yaml
dependencies:
# MQTT et notifications
mqtt5_client: ^4.0.0
flutter_local_notifications: ^17.0.0
# UI et UX
emoji_picker_flutter: ^2.0.0
cached_network_image: ^3.3.1
photo_view: ^0.14.0
file_picker: ^6.1.1
# Utilitaires
path_provider: ^2.1.2
image_picker: ^1.0.7
image: ^4.1.7 # Pour compression
intl: ^0.19.0 # Pour formatage dates
```
### 🧪 Tests à implémenter
- [ ] Tests unitaires des repositories
- [ ] Tests d'intégration MQTT
- [ ] Tests de performance (1000+ messages)
- [ ] Tests hors ligne/online
- [ ] Tests de sécurité (injection, XSS)
---
## 💳 Module de paiement Stripe
### 📋 Vue d'ensemble
Intégration de Stripe pour permettre aux amicales ayant activé `chk_stripe` d'accepter les paiements par carte bancaire avec une commission de 1.4%.
### 🎯 Objectifs
1. Permettre le paiement en ligne lors des passages
2. Gérer les comptes Stripe des amicales
3. Suivre les transactions et commissions
4. Offrir une expérience de paiement fluide
### 📝 Tâches de développement Stripe
#### 1. **Configuration initiale Stripe**
- [ ] Créer un compte Stripe Connect Platform
- [ ] Configurer les webhooks Stripe
- [ ] Mettre en place l'environnement de test (sandbox)
- [ ] Configurer les clés API (publishable/secret)
- [ ] Implémenter la gestion sécurisée des clés
#### 2. **Onboarding des amicales**
- [ ] Créer un workflow d'inscription Stripe pour les amicales
- [ ] Implémenter Stripe Connect Onboarding
- [ ] Gérer le KYC (Know Your Customer) requis par Stripe
- [ ] Stocker de manière sécurisée le `stripe_account_id`
- [ ] Créer une page de statut du compte Stripe
- [ ] Gérer les documents requis (RIB, statuts, etc.)
#### 3. **Modèles de données**
- [ ] Créer `StripeAccountModel` pour les comptes Connect
- [ ] Créer `PaymentIntentModel` pour les intentions de paiement
- [ ] Créer `TransactionModel` pour l'historique
- [ ] Ajouter les champs Stripe dans `PassageModel`
- [ ] Implémenter la table des commissions
#### 4. **Service StripeService**
```dart
class StripeService {
// Compte Connect
Future<String> createConnectAccount(AmicaleModel amicale);
Future<void> updateAccountStatus(String accountId);
Future<String> createAccountLink(String accountId);
// Paiements
Future<PaymentIntent> createPaymentIntent({
required double amount,
required String currency,
required String connectedAccountId,
});
Future<void> confirmPayment(String paymentIntentId);
Future<void> refundPayment(String paymentIntentId);
// Commissions
double calculateApplicationFee(double amount); // 1.4%
}
```
#### 5. **Interface de paiement dans PassageForm**
- [ ] Détecter si l'amicale accepte Stripe (`chk_stripe`)
- [ ] Ajouter l'option "Paiement par carte" dans le dropdown
- [ ] Intégrer Stripe Elements pour la saisie de carte
- [ ] Implémenter le flow de paiement 3D Secure
- [ ] Gérer les erreurs de paiement
- [ ] Afficher le reçu de paiement
- [ ] Permettre l'envoi du reçu par email
#### 6. **Widget StripePaymentSheet**
```dart
class StripePaymentSheet extends StatefulWidget {
final double amount;
final String currency;
final AmicaleModel amicale;
final Function(String) onSuccess;
final Function(String) onError;
// UI avec:
// - Montant à payer
// - Formulaire de carte (Stripe Elements)
// - Bouton de validation
// - Indicateur de traitement
// - Gestion 3D Secure
}
```
#### 7. **Tableau de bord financier**
- [ ] Page de suivi des transactions Stripe
- [ ] Graphiques des paiements par période
- [ ] Export des transactions (CSV/Excel)
- [ ] Calcul automatique des commissions
- [ ] Rapprochement bancaire
- [ ] Dashboard temps réel des paiements
#### 8. **Webhooks Stripe**
```php
// Backend PHP pour gérer les webhooks
class StripeWebhookHandler {
// Events à gérer:
- payment_intent.succeeded
- payment_intent.failed
- account.updated
- payout.created
- refund.created
}
```
#### 9. **Sécurité**
- [ ] Chiffrement des données sensibles
- [ ] Validation PCI DSS
- [ ] Audit trail des transactions
- [ ] Détection de fraude
- [ ] Rate limiting sur les API
- [ ] Tokenisation des cartes
#### 10. **Tests et conformité**
- [ ] Tests avec cartes de test Stripe
- [ ] Tests des cas d'erreur (carte refusée, etc.)
- [ ] Tests 3D Secure
- [ ] Tests de performance
- [ ] Conformité RGPD pour les données de paiement
- [ ] Documentation utilisateur
### 🔧 Configuration backend requise
#### Tables base de données
```sql
CREATE TABLE stripe_accounts (
id INT PRIMARY KEY AUTO_INCREMENT,
fk_entite INT NOT NULL,
stripe_account_id VARCHAR(255) ENCRYPTED,
status ENUM('pending', 'active', 'restricted'),
charges_enabled BOOLEAN DEFAULT FALSE,
payouts_enabled BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE TABLE stripe_transactions (
id INT PRIMARY KEY AUTO_INCREMENT,
fk_passage INT,
stripe_payment_intent_id VARCHAR(255),
amount DECIMAL(10,2),
currency VARCHAR(3),
status VARCHAR(50),
application_fee DECIMAL(10,2),
created_at TIMESTAMP
);
```
#### Variables d'environnement
```env
STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
STRIPE_APPLICATION_FEE_PERCENT=1.4
```
### 📦 Dépendances Stripe
```yaml
dependencies:
# Stripe
flutter_stripe: ^10.1.1
# Sécurité
flutter_secure_storage: ^9.0.0
# UI
shimmer: ^3.0.0 # Loading states
lottie: ^3.1.0 # Animations succès/échec
```
### 🚀 Roadmap d'implémentation
**Phase 1 (2 semaines)**
- Configuration Stripe Connect
- Onboarding basique des amicales
- Tests en sandbox
**Phase 2 (3 semaines)**
- Intégration dans PassageForm
- Gestion des paiements simples
- Webhooks essentiels
**Phase 3 (2 semaines)**
- Dashboard financier
- Export et rapports
- Tests complets
**Phase 4 (1 semaine)**
- Mise en production
- Monitoring
- Documentation
### 💰 Estimation des coûts
- **Stripe Connect** : 0.25 par compte actif/mois
- **Transactions** : 1.4% + 0.25 par transaction
- **Commission application** : 1.4% (reversée à GEOSECTOR)
- **Coût net pour l'amicale** : ~2.8% + 0.25 par transaction
---
**Date de création** : 2025-08-07
**Auteur** : Architecture Team
**Version** : 1.2.0

View File

@@ -60,6 +60,211 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
aFileChooser
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2011 - 2013 Paul Burke
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
--------------------------------------------------------------------------------
abseil-cpp
@@ -919,6 +1124,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
accessibility
engine
image_picker
spring_animation
tonic
url_launcher_web
@@ -2952,6 +3158,72 @@ AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
--------------------------------------------------------------------------------
cross_file
file_selector_linux
file_selector_macos
file_selector_platform_interface
file_selector_windows
flutter_lints
flutter_plugin_android_lifecycle
go_router
image_picker_for_web
image_picker_linux
image_picker_macos
image_picker_platform_interface
image_picker_windows
path_provider
path_provider_android
path_provider_foundation
path_provider_linux
path_provider_platform_interface
path_provider_windows
plugin_platform_interface
shared_preferences
shared_preferences_android
shared_preferences_foundation
shared_preferences_linux
shared_preferences_platform_interface
shared_preferences_web
shared_preferences_windows
url_launcher
url_launcher_android
url_launcher_ios
url_launcher_linux
url_launcher_macos
url_launcher_platform_interface
url_launcher_windows
vector_graphics
vector_graphics_codec
vector_graphics_compiler
xdg_directories
Copyright 2013 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
cupertino_icons
@@ -6244,61 +6516,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
flutter_lints
go_router
path_provider
path_provider_android
path_provider_foundation
path_provider_linux
path_provider_platform_interface
path_provider_windows
plugin_platform_interface
shared_preferences
shared_preferences_android
shared_preferences_foundation
shared_preferences_linux
shared_preferences_platform_interface
shared_preferences_web
shared_preferences_windows
url_launcher
url_launcher_android
url_launcher_ios
url_launcher_linux
url_launcher_macos
url_launcher_platform_interface
url_launcher_windows
vector_graphics
vector_graphics_codec
vector_graphics_compiler
xdg_directories
Copyright 2013 The Flutter Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
flutter_local_notifications
flutter_local_notifications_linux

View File

@@ -39,6 +39,6 @@ _flutter.buildConfig = {"engineRevision":"ef0cd000916d64fa0c5d09cc809fa7ad244a57
_flutter.loader.load({
serviceWorkerSettings: {
serviceWorkerVersion: "2510524361"
serviceWorkerVersion: "2172432993"
}
});

View File

@@ -11,7 +11,7 @@ const RESOURCES = {"icons/Icon-maskable-192.png": "f36879dd176101fac324b68793e46
"icons/Icon-512.png": "4495c4d7eeff38c1a967d16a8129bd2e",
"icons/Icon-maskable-512.png": "4495c4d7eeff38c1a967d16a8129bd2e",
"flutter.js": "83d881c1dbb6d6bcd6b42e274605b69c",
"flutter_bootstrap.js": "c8b920ced05cd73436a61d1da7f46009",
"flutter_bootstrap.js": "3b1d0de9e76904bfbfa6d48681275b2a",
"favicon-64.png": "259540a3217e969237530444ca0eaed3",
"index.html": "2aab03d10fea3b608e3eddc0fc0077e5",
"/": "2aab03d10fea3b608e3eddc0fc0077e5",
@@ -25,12 +25,12 @@ const RESOURCES = {"icons/Icon-maskable-192.png": "f36879dd176101fac324b68793e46
"canvaskit/skwasm.js.symbols": "e72c79950c8a8483d826a7f0560573a1",
"canvaskit/skwasm.js": "ea559890a088fe28b4ddf70e17e60052",
"favicon-32.png": "21510778ead066ac826ad69302400773",
"version.json": "2b834918f7d5565834c60f21a3a20768",
"version.json": "843ab81033c65b1ade260713d5708244",
"favicon.png": "21510778ead066ac826ad69302400773",
"favicon-16.png": "106142fb24eba190e475dbe6513cc9ff",
"assets/AssetManifest.json": "ee827821edbe97bd24fe72882535afca",
"assets/AssetManifest.bin": "f1f694a4898aea6648eb53d349609844",
"assets/fonts/MaterialIcons-Regular.otf": "a6c0d7cbaaafc86349d90ed45dca8016",
"assets/fonts/MaterialIcons-Regular.otf": "78116d2cb29578b69c97b0f052fc3f88",
"assets/FontManifest.json": "2eb88ea349cfc4d8628e771303d003ca",
"assets/packages/flutter_map/lib/assets/flutter_map_logo.png": "208d63cc917af9713fc9572bd5c09362",
"assets/packages/cupertino_icons/assets/CupertinoIcons.ttf": "33b7d9392238c04c131b6ce224e13711",
@@ -44,9 +44,9 @@ const RESOURCES = {"icons/Icon-maskable-192.png": "f36879dd176101fac324b68793e46
"assets/assets/fonts/Figtree-VariableFont_wght.ttf": "d25a5457a34fbf1c36b2937df1cf543b",
"assets/assets/animations/geo_main.json": "e1c9755530d5f83718d4d43b0a36a703",
"assets/shaders/ink_sparkle.frag": "ecc85a2e95f5e9f53123dcaf8cb9b6ce",
"assets/NOTICES": "bbf2212d442929639b961f94116a2af7",
"assets/NOTICES": "d1d24a6d37f88e05b7d4c2d8cc066528",
"assets/AssetManifest.bin.json": "01a86053322475f2d9ce5c0a8d863d63",
"main.dart.js": "9ec485d1d107e45a00e6761577a32bbf",
"main.dart.js": "0f0789895f81dcd8d2a7e047d31ba345",
"manifest.json": "4c436b37549165212484247d584e67cc"};
// The application shell files that are downloaded before a service worker can
// start.

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"app_name":"geosector_app","version":"3.0.4","build_number":"304","package_name":"geosector_app"}
{"app_name":"geosector_app","version":"3.0.6","build_number":"306","package_name":"geosector_app"}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:geosector_app/core/theme/app_theme.dart';
import 'package:geosector_app/core/services/theme_service.dart';
import 'package:go_router/go_router.dart';
@@ -44,6 +45,17 @@ class GeosectorApp extends StatelessWidget {
themeMode: themeService.themeMode,
routerConfig: _createRouter(),
debugShowCheckedModeBanner: false,
// Configuration des localisations pour le français
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('fr', 'FR'), // Français comme langue principale
Locale('en', 'US'), // Anglais en fallback
],
locale: const Locale('fr', 'FR'), // Forcer le français par défaut
);
},
);

View File

@@ -70,6 +70,15 @@ class AmicaleModel extends HiveObject {
@HiveField(21)
final DateTime? updatedAt;
@HiveField(22)
final bool chkMdpManuel;
@HiveField(23)
final bool chkUsernameManuel;
@HiveField(24)
final String? logoBase64; // Logo en base64 (data:image/png;base64,...)
AmicaleModel({
required this.id,
required this.name,
@@ -93,6 +102,9 @@ class AmicaleModel extends HiveObject {
this.chkStripe = false,
this.createdAt,
this.updatedAt,
this.chkMdpManuel = false,
this.chkUsernameManuel = false,
this.logoBase64,
});
// Factory pour convertir depuis JSON (API)
@@ -123,6 +135,17 @@ class AmicaleModel extends HiveObject {
json['chk_active'] == 1 || json['chk_active'] == true;
final bool chkStripe =
json['chk_stripe'] == 1 || json['chk_stripe'] == true;
final bool chkMdpManuel =
json['chk_mdp_manuel'] == 1 || json['chk_mdp_manuel'] == true;
final bool chkUsernameManuel =
json['chk_username_manuel'] == 1 || json['chk_username_manuel'] == true;
// Traiter le logo si présent
String? logoBase64;
if (json['logo'] != null && json['logo'] is Map) {
final logoData = json['logo'] as Map<String, dynamic>;
logoBase64 = logoData['data_url'] as String?;
}
// Traiter les dates si présentes
DateTime? createdAt;
@@ -166,6 +189,9 @@ class AmicaleModel extends HiveObject {
chkStripe: chkStripe,
createdAt: createdAt,
updatedAt: updatedAt,
chkMdpManuel: chkMdpManuel,
chkUsernameManuel: chkUsernameManuel,
logoBase64: logoBase64,
);
}
@@ -194,6 +220,9 @@ class AmicaleModel extends HiveObject {
'chk_stripe': chkStripe ? 1 : 0,
'created_at': createdAt?.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
'chk_mdp_manuel': chkMdpManuel ? 1 : 0,
'chk_username_manuel': chkUsernameManuel ? 1 : 0,
// Note: logoBase64 n'est pas envoyé via toJson (lecture seule depuis l'API)
};
}
@@ -220,6 +249,9 @@ class AmicaleModel extends HiveObject {
bool? chkStripe,
DateTime? createdAt,
DateTime? updatedAt,
bool? chkMdpManuel,
bool? chkUsernameManuel,
String? logoBase64,
}) {
return AmicaleModel(
id: id,
@@ -244,6 +276,9 @@ class AmicaleModel extends HiveObject {
chkStripe: chkStripe ?? this.chkStripe,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
chkMdpManuel: chkMdpManuel ?? this.chkMdpManuel,
chkUsernameManuel: chkUsernameManuel ?? this.chkUsernameManuel,
logoBase64: logoBase64 ?? this.logoBase64,
);
}
}

View File

@@ -39,13 +39,16 @@ class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
chkStripe: fields[19] as bool,
createdAt: fields[20] as DateTime?,
updatedAt: fields[21] as DateTime?,
chkMdpManuel: fields[22] as bool,
chkUsernameManuel: fields[23] as bool,
logoBase64: fields[24] as String?,
);
}
@override
void write(BinaryWriter writer, AmicaleModel obj) {
writer
..writeByte(22)
..writeByte(25)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@@ -89,7 +92,13 @@ class AmicaleModelAdapter extends TypeAdapter<AmicaleModel> {
..writeByte(20)
..write(obj.createdAt)
..writeByte(21)
..write(obj.updatedAt);
..write(obj.updatedAt)
..writeByte(22)
..write(obj.chkMdpManuel)
..writeByte(23)
..write(obj.chkUsernameManuel)
..writeByte(24)
..write(obj.logoBase64);
}
@override

View File

@@ -70,6 +70,12 @@ class ClientModel extends HiveObject {
@HiveField(21)
final DateTime? updatedAt;
@HiveField(22)
final bool? chkMdpManuel;
@HiveField(23)
final bool? chkUsernameManuel;
ClientModel({
required this.id,
required this.name,
@@ -93,6 +99,8 @@ class ClientModel extends HiveObject {
this.chkStripe,
this.createdAt,
this.updatedAt,
this.chkMdpManuel,
this.chkUsernameManuel,
});
// Factory pour convertir depuis JSON (API)
@@ -138,6 +146,8 @@ class ClientModel extends HiveObject {
chkStripe: json['chk_stripe'] == 1 || json['chk_stripe'] == true,
createdAt: json['created_at'] != null ? DateTime.parse(json['created_at']) : null,
updatedAt: json['updated_at'] != null ? DateTime.parse(json['updated_at']) : null,
chkMdpManuel: json['chk_mdp_manuel'] == 1 || json['chk_mdp_manuel'] == true,
chkUsernameManuel: json['chk_username_manuel'] == 1 || json['chk_username_manuel'] == true,
);
}
@@ -166,6 +176,8 @@ class ClientModel extends HiveObject {
'chk_stripe': chkStripe,
'created_at': createdAt?.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
'chk_mdp_manuel': chkMdpManuel,
'chk_username_manuel': chkUsernameManuel,
};
}
@@ -192,6 +204,8 @@ class ClientModel extends HiveObject {
bool? chkStripe,
DateTime? createdAt,
DateTime? updatedAt,
bool? chkMdpManuel,
bool? chkUsernameManuel,
}) {
return ClientModel(
id: id,
@@ -216,6 +230,8 @@ class ClientModel extends HiveObject {
chkStripe: chkStripe ?? this.chkStripe,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
chkMdpManuel: chkMdpManuel ?? this.chkMdpManuel,
chkUsernameManuel: chkUsernameManuel ?? this.chkUsernameManuel,
);
}
}

View File

@@ -39,13 +39,15 @@ class ClientModelAdapter extends TypeAdapter<ClientModel> {
chkStripe: fields[19] as bool?,
createdAt: fields[20] as DateTime?,
updatedAt: fields[21] as DateTime?,
chkMdpManuel: fields[22] as bool?,
chkUsernameManuel: fields[23] as bool?,
);
}
@override
void write(BinaryWriter writer, ClientModel obj) {
writer
..writeByte(22)
..writeByte(24)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@@ -89,7 +91,11 @@ class ClientModelAdapter extends TypeAdapter<ClientModel> {
..writeByte(20)
..write(obj.createdAt)
..writeByte(21)
..write(obj.updatedAt);
..write(obj.updatedAt)
..writeByte(22)
..write(obj.chkMdpManuel)
..writeByte(23)
..write(obj.chkUsernameManuel);
}
@override

View File

@@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/core/services/logger_service.dart';
import 'package:geosector_app/core/utils/api_exception.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
class MembreRepository extends ChangeNotifier {
@@ -19,7 +21,7 @@ class MembreRepository extends ChangeNotifier {
throw Exception('La boîte ${AppKeys.membresBoxName} n\'est pas ouverte. Initialisez d\'abord l\'application.');
}
_cachedMembreBox = Hive.box<MembreModel>(AppKeys.membresBoxName);
debugPrint('MembreRepository: Box ${AppKeys.membresBoxName} mise en cache');
LoggerService.database('MembreRepository: Box ${AppKeys.membresBoxName} mise en cache');
}
return _cachedMembreBox!;
}
@@ -120,7 +122,7 @@ class MembreRepository extends ChangeNotifier {
// === MÉTHODES API ===
// Créer un membre via l'API
Future<MembreModel?> createMembre(MembreModel membre) async {
Future<MembreModel?> createMembre(MembreModel membre, {String? password}) async {
_isLoading = true;
notifyListeners();
@@ -130,48 +132,97 @@ class MembreRepository extends ChangeNotifier {
final data = userModel.toJson();
data.remove('id'); // L'API génère l'ID
data.remove('created_at'); // L'API génère created_at
// Supprimer les champs de session qui ne doivent pas être envoyés
data.remove('session_id');
data.remove('session_expiry');
data.remove('last_path');
// Convertir is_active en chk_active pour l'API
if (data.containsKey('is_active')) {
data['chk_active'] = data['is_active'] ? 1 : 0;
data.remove('is_active');
}
// Convertir role en fk_role pour l'API
if (data.containsKey('role')) {
data['fk_role'] = data['role'];
data.remove('role');
}
// Ajouter le mot de passe si fourni (sera ignoré par l'API si chk_mdp_manuel=0)
if (password != null && password.isNotEmpty) {
data['password'] = password;
debugPrint('🔑 Mot de passe inclus dans la requête');
} else {
debugPrint('⚠️ Pas de mot de passe fourni');
}
// Vérifier la présence de l'username (sera ignoré par l'API si chk_username_manuel=0)
if (data.containsKey('username') && data['username'] != null && data['username'].toString().isNotEmpty) {
debugPrint('👤 Username inclus dans la requête: ${data['username']}');
} else {
debugPrint('⚠️ Username manquant ou vide dans la requête');
// Si pas d'username, s'assurer qu'il n'est pas envoyé du tout
data.remove('username');
}
LoggerService.api('Données envoyées à l\'API pour création membre: $data');
// Appeler l'API users
final response = await ApiService.instance.post('/users', data: data);
if (response.statusCode == 201) {
// Extraire l'ID de la réponse API
final responseData = response.data;
debugPrint('🎉 Réponse API création utilisateur: $responseData');
// Vérifier d'abord si on a une réponse avec un statut d'erreur
if (response.data != null && response.data is Map<String, dynamic>) {
final responseData = response.data as Map<String, dynamic>;
// Si l'API retourne un status error, propager le message
if (responseData['status'] == 'error' && responseData['message'] != null) {
throw Exception(responseData['message']);
}
// Si succès avec code 201
if (response.statusCode == 201 && responseData['status'] == 'success') {
debugPrint('🎉 Réponse API création utilisateur: $responseData');
// L'API retourne {"status":"success","message":"Utilisateur créé avec succès","id":"10027748"}
final userId = responseData['id'] is String ? int.parse(responseData['id']) : responseData['id'] as int;
// L'API retourne {"status":"success","message":"Utilisateur créé avec succès","id":"10027748"}
final userId = responseData['id'] is String ? int.parse(responseData['id']) : responseData['id'] as int;
// Créer le nouveau membre avec l'ID retourné par l'API
final createdMember = MembreModel(
id: userId,
fkEntite: membre.fkEntite,
role: membre.role,
fkTitre: membre.fkTitre,
name: membre.name,
firstName: membre.firstName,
username: membre.username,
sectName: membre.sectName,
email: membre.email,
phone: membre.phone,
mobile: membre.mobile,
dateNaissance: membre.dateNaissance,
dateEmbauche: membre.dateEmbauche,
createdAt: DateTime.now(),
isActive: membre.isActive,
);
// Créer le nouveau membre avec l'ID retourné par l'API
final createdMember = MembreModel(
id: userId,
fkEntite: membre.fkEntite,
role: membre.role,
fkTitre: membre.fkTitre,
name: membre.name,
firstName: membre.firstName,
username: membre.username,
sectName: membre.sectName,
email: membre.email,
phone: membre.phone,
mobile: membre.mobile,
dateNaissance: membre.dateNaissance,
dateEmbauche: membre.dateEmbauche,
createdAt: DateTime.now(),
isActive: membre.isActive,
);
// Sauvegarder localement dans Hive (saveMembreBox gère déjà _resetCache)
await saveMembreBox(createdMember);
// Sauvegarder localement dans Hive (saveMembreBox gère déjà _resetCache)
await saveMembreBox(createdMember);
debugPrint('✅ Membre créé avec l\'ID: $userId et sauvegardé localement');
return createdMember;
debugPrint('✅ Membre créé avec l\'ID: $userId et sauvegardé localement');
return createdMember;
}
}
debugPrint('Échec création membre - Code: ${response.statusCode}');
return null;
LoggerService.error('Échec création membre - Code: ${response.statusCode}');
throw Exception('Erreur lors de la création du membre');
} catch (e) {
debugPrint('❌ Erreur lors de la création du membre: $e');
// Ne pas logger les détails techniques de DioException
if (e is ApiException) {
LoggerService.error('Erreur lors de la création du membre: ${e.message}');
} else {
LoggerService.error('Erreur lors de la création du membre');
}
rethrow; // Propager l'exception pour la gestion d'erreurs
} finally {
_isLoading = false;
@@ -180,27 +231,67 @@ class MembreRepository extends ChangeNotifier {
}
// Mettre à jour un membre via l'API
Future<bool> updateMembre(MembreModel membre) async {
Future<bool> updateMembre(MembreModel membre, {String? password}) async {
_isLoading = true;
notifyListeners();
try {
// Convertir en UserModel pour l'API
final userModel = membre.toUserModel();
final data = userModel.toJson();
// Supprimer les champs de session qui ne doivent pas être envoyés
data.remove('session_id');
data.remove('session_expiry');
data.remove('last_path');
// Convertir is_active en chk_active pour l'API
if (data.containsKey('is_active')) {
data['chk_active'] = data['is_active'] ? 1 : 0;
data.remove('is_active');
}
// Convertir role en fk_role pour l'API
if (data.containsKey('role')) {
data['fk_role'] = data['role'];
data.remove('role');
}
// Ajouter le mot de passe si fourni (sera ignoré par l'API si chk_mdp_manuel=0)
if (password != null && password.isNotEmpty) {
data['password'] = password;
debugPrint('🔑 Mot de passe inclus dans la requête de mise à jour');
} else {
debugPrint('⚠️ Pas de mot de passe fourni pour la mise à jour');
}
// L'username ne devrait pas être modifiable en update, mais on le garde pour l'API
if (data.containsKey('username') && data['username'] != null && data['username'].toString().isNotEmpty) {
debugPrint('👤 Username présent dans la requête de mise à jour: ${data['username']}');
} else {
debugPrint('⚠️ Username manquant dans la requête de mise à jour');
}
LoggerService.api('Données envoyées à l\'API pour mise à jour membre: $data');
// Appeler l'API users au lieu de membres
final response = await ApiService.instance.put('/users/${membre.id}', data: userModel.toJson());
final response = await ApiService.instance.put('/users/${membre.id}', data: data);
if (response.statusCode == 200) {
// Sauvegarder le membre mis à jour localement
await saveMembreBox(membre);
return true;
}
return false;
// Si on arrive ici, c'est que la requête a réussi (200)
// Sauvegarder le membre mis à jour localement
await saveMembreBox(membre);
return true;
} catch (e) {
debugPrint('Erreur lors de la mise à jour du membre: $e');
return false;
// Ne pas logger les détails techniques de DioException
if (e is ApiException) {
LoggerService.error('Erreur lors de la mise à jour du membre: ${e.message}');
} else {
LoggerService.error('Erreur lors de la mise à jour du membre');
}
// Si c'est une DioException, elle sera automatiquement convertie en ApiException
// par ApiException.fromDioException() qui extrait le message de l'API
rethrow; // Propager l'exception pour que le message d'erreur soit affiché
} finally {
_isLoading = false;
notifyListeners();
@@ -215,13 +306,26 @@ class MembreRepository extends ChangeNotifier {
try {
final response = await ApiService.instance.post('/users/$membreId/reset-password');
// Vérifier si on a une réponse avec un statut d'erreur
if (response.data != null && response.data is Map<String, dynamic>) {
final responseData = response.data as Map<String, dynamic>;
if (responseData['status'] == 'error' && responseData['message'] != null) {
throw Exception(responseData['message']);
}
}
if (response.statusCode == 200) {
return true;
}
return false;
throw Exception('Erreur lors de la réinitialisation du mot de passe');
} catch (e) {
debugPrint('Erreur lors de la réinitialisation du mot de passe: $e');
// Ne pas logger les détails techniques de DioException
if (e is ApiException) {
LoggerService.error('Erreur lors de la réinitialisation du mot de passe: ${e.message}');
} else {
LoggerService.error('Erreur lors de la réinitialisation du mot de passe');
}
rethrow;
} finally {
_isLoading = false;
@@ -254,19 +358,32 @@ class MembreRepository extends ChangeNotifier {
endpoint += '?${queryParams.join('&')}';
}
debugPrint('🔗 DELETE endpoint: $endpoint');
LoggerService.api('DELETE endpoint: $endpoint');
final response = await ApiService.instance.delete(endpoint);
// Vérifier si on a une réponse avec un statut d'erreur
if (response.data != null && response.data is Map<String, dynamic>) {
final responseData = response.data as Map<String, dynamic>;
if (responseData['status'] == 'error' && responseData['message'] != null) {
throw Exception(responseData['message']);
}
}
if (response.statusCode == 200 || response.statusCode == 204) {
// Supprimer le membre localement
await deleteMembreBox(membreId);
return true;
}
return false;
throw Exception('Erreur lors de la suppression du membre');
} catch (e) {
debugPrint('Erreur lors de la suppression du membre: $e');
// Ne pas logger les détails techniques de DioException
if (e is ApiException) {
LoggerService.error('Erreur lors de la suppression du membre: ${e.message}');
} else {
LoggerService.error('Erreur lors de la suppression du membre');
}
rethrow;
} finally {
_isLoading = false;

View File

@@ -15,12 +15,10 @@ import 'package:geosector_app/core/data/models/operation_model.dart';
import 'package:geosector_app/core/data/models/sector_model.dart';
import 'package:geosector_app/core/data/models/passage_model.dart';
import 'package:geosector_app/core/data/models/membre_model.dart';
import 'package:geosector_app/presentation/widgets/loading_progress_overlay.dart';
import 'package:geosector_app/presentation/widgets/loading_spin_overlay.dart';
import 'package:geosector_app/core/models/loading_state.dart';
class UserRepository extends ChangeNotifier {
// Overlay pour afficher la progression du chargement
OverlayEntry? _progressOverlay;
bool _isLoading = false;
// Constructeur simplifié - plus d'injection d'ApiService
@@ -336,64 +334,39 @@ class UserRepository extends ChangeNotifier {
}
}
/// Connexion avec interface utilisateur et progression
Future<bool> loginWithUI(
/// Connexion avec spinner moderne (pour remplacer la barre de progression)
Future<bool> loginWithSpinner(
BuildContext context, String username, String password,
{required String type}) async {
OverlayEntry? spinOverlay;
try {
// Créer et afficher l'overlay de progression
_progressOverlay = LoadingProgressOverlayUtils.show(
// Déterminer le message selon le type de connexion
final message = type == 'admin'
? 'Connexion administrateur...'
: 'Connexion utilisateur...';
// Afficher le spinner moderne
spinOverlay = LoadingSpinOverlayUtils.show(
context: context,
message: 'Connexion en cours...',
progress: 0.0,
stepDescription: 'Préparation',
blurAmount: 5.0,
message: message,
blurAmount: 10.0,
showCard: true,
);
// Écouter les changements d'état du DataLoadingService
void listener() {
if (_progressOverlay != null) {
final loadingState = DataLoadingService.instance.loadingState;
LoadingProgressOverlayUtils.update(
overlayEntry: _progressOverlay!,
message: loadingState.message,
progress: loadingState.progress,
stepDescription: loadingState.stepDescription,
);
}
}
// Configurer le callback de progression
DataLoadingService.instance.setProgressCallback((_) => listener());
// Exécuter la connexion
final result = await login(username, password, type: type);
// Attendre un court instant pour que l'utilisateur voie le résultat
if (result) {
await Future.delayed(const Duration(milliseconds: 500));
} else {
await Future.delayed(const Duration(seconds: 2));
}
// Petit délai pour une meilleure UX
await Future.delayed(const Duration(milliseconds: 300));
// Supprimer l'overlay
if (_progressOverlay != null) {
_progressOverlay!.remove();
_progressOverlay = null;
}
// Nettoyer le callback
DataLoadingService.instance.setProgressCallback(null);
// Fermer le spinner
LoadingSpinOverlayUtils.hideSpecific(spinOverlay);
return result;
} catch (e) {
// En cas d'erreur, supprimer l'overlay
if (_progressOverlay != null) {
_progressOverlay!.remove();
_progressOverlay = null;
}
DataLoadingService.instance.setProgressCallback(null);
LoadingSpinOverlayUtils.hideSpecific(spinOverlay);
return false;
}
}

View File

@@ -18,6 +18,10 @@ class ApiService {
late final String _baseUrl;
late final String _appIdentifier;
String? _sessionId;
// Getters pour les propriétés (lecture seule)
String? get sessionId => _sessionId;
String get baseUrl => _baseUrl;
// Singleton thread-safe
static ApiService get instance {
@@ -142,8 +146,11 @@ class ApiService {
Future<Response> post(String path, {dynamic data}) async {
try {
return await _dio.post(path, data: data);
} on DioException catch (e) {
throw ApiException.fromDioException(e);
} catch (e) {
rethrow;
if (e is ApiException) rethrow;
throw ApiException('Erreur inattendue lors de la requête POST', originalError: e);
}
}
@@ -151,8 +158,11 @@ class ApiService {
Future<Response> get(String path, {Map<String, dynamic>? queryParameters}) async {
try {
return await _dio.get(path, queryParameters: queryParameters);
} on DioException catch (e) {
throw ApiException.fromDioException(e);
} catch (e) {
rethrow;
if (e is ApiException) rethrow;
throw ApiException('Erreur inattendue lors de la requête GET', originalError: e);
}
}
@@ -160,8 +170,11 @@ class ApiService {
Future<Response> put(String path, {dynamic data}) async {
try {
return await _dio.put(path, data: data);
} on DioException catch (e) {
throw ApiException.fromDioException(e);
} catch (e) {
rethrow;
if (e is ApiException) rethrow;
throw ApiException('Erreur inattendue lors de la requête PUT', originalError: e);
}
}
@@ -169,8 +182,81 @@ class ApiService {
Future<Response> delete(String path) async {
try {
return await _dio.delete(path);
} on DioException catch (e) {
throw ApiException.fromDioException(e);
} catch (e) {
rethrow;
if (e is ApiException) rethrow;
throw ApiException('Erreur inattendue lors de la requête DELETE', originalError: e);
}
}
// Méthode pour uploader un logo d'amicale
Future<Map<String, dynamic>> uploadLogo(int entiteId, dynamic imageFile) async {
try {
FormData formData;
// Gestion différente selon la plateforme (Web ou Mobile)
if (kIsWeb) {
// Pour le web, imageFile est un XFile
final bytes = await imageFile.readAsBytes();
// Vérification de la taille (5 Mo max)
const int maxSize = 5 * 1024 * 1024;
if (bytes.length > maxSize) {
throw ApiException(
'Le fichier est trop volumineux. Taille maximale: 5 Mo',
statusCode: 413
);
}
formData = FormData.fromMap({
'logo': MultipartFile.fromBytes(
bytes,
filename: imageFile.name,
),
});
} else {
// Pour mobile, imageFile est un File
final fileLength = await imageFile.length();
// Vérification de la taille (5 Mo max)
const int maxSize = 5 * 1024 * 1024;
if (fileLength > maxSize) {
throw ApiException(
'Le fichier est trop volumineux. Taille maximale: 5 Mo',
statusCode: 413
);
}
formData = FormData.fromMap({
'logo': await MultipartFile.fromFile(
imageFile.path,
filename: imageFile.path.split('/').last,
),
});
}
final response = await _dio.post(
'/entites/$entiteId/logo',
data: formData,
options: Options(
headers: {
'Content-Type': 'multipart/form-data',
},
),
);
if (response.statusCode == 200 || response.statusCode == 201) {
return response.data;
} else {
throw ApiException('Erreur lors de l\'upload du logo',
statusCode: response.statusCode);
}
} on DioException catch (e) {
throw ApiException.fromDioException(e);
} catch (e) {
if (e is ApiException) rethrow;
throw ApiException('Erreur inattendue lors de l\'upload du logo', originalError: e);
}
}

View File

@@ -0,0 +1,163 @@
import 'package:flutter/foundation.dart';
import 'package:geosector_app/core/services/api_service.dart';
/// Service de logging centralisé qui désactive automatiquement les logs en production
class LoggerService {
static LoggerService? _instance;
static bool? _isProduction;
// Singleton
static LoggerService get instance {
_instance ??= LoggerService._internal();
return _instance!;
}
LoggerService._internal();
/// Détermine si on est en production
static bool get isProduction {
if (_isProduction != null) return _isProduction!;
try {
// Vérifier si ApiService est initialisé
final env = ApiService.instance.getCurrentEnvironment();
_isProduction = env == 'PROD';
} catch (e) {
// Si ApiService n'est pas encore initialisé, utiliser kReleaseMode
_isProduction = kReleaseMode;
}
return _isProduction!;
}
/// Réinitialiser le cache de l'environnement (utile pour les tests)
static void resetEnvironmentCache() {
_isProduction = null;
}
/// Log simple (remplace debugPrint)
static void log(String message, {String? prefix}) {
if (!isProduction) {
if (prefix != null) {
debugPrint('$prefix $message');
} else {
debugPrint(message);
}
}
}
/// Log d'information
static void info(String message) {
if (!isProduction) {
debugPrint(' $message');
}
}
/// Log de succès
static void success(String message) {
if (!isProduction) {
debugPrint('$message');
}
}
/// Log d'avertissement
static void warning(String message) {
if (!isProduction) {
debugPrint('⚠️ $message');
}
}
/// Log d'erreur (toujours affiché même en production pour le debugging)
static void error(String message, [dynamic error, StackTrace? stackTrace]) {
// Les erreurs sont toujours loggées, même en production
debugPrint('$message');
if (error != null) {
debugPrint(' Error: $error');
}
if (stackTrace != null && !isProduction) {
// La stack trace n'est affichée qu'en DEV/REC
debugPrint(' Stack trace:\n$stackTrace');
}
}
/// Log de debug (avec emoji personnalisé)
static void debug(String message, {String emoji = '🔧'}) {
if (!isProduction) {
debugPrint('$emoji $message');
}
}
/// Log de requête API
static void api(String message) {
if (!isProduction) {
debugPrint('🔗 $message');
}
}
/// Log de base de données
static void database(String message) {
if (!isProduction) {
debugPrint('💾 $message');
}
}
/// Log de navigation
static void navigation(String message) {
if (!isProduction) {
debugPrint('🧭 $message');
}
}
/// Log de performance/timing
static void performance(String message) {
if (!isProduction) {
debugPrint('⏱️ $message');
}
}
/// Log conditionnel basé sur un flag custom
static void conditional(String message, {required bool condition}) {
if (!isProduction && condition) {
debugPrint(message);
}
}
/// Groupe de logs (pour regrouper visuellement des logs liés)
static void group(String title, List<String> messages) {
if (!isProduction) {
debugPrint('┌─ $title');
for (int i = 0; i < messages.length; i++) {
if (i == messages.length - 1) {
debugPrint('└─ ${messages[i]}');
} else {
debugPrint('├─ ${messages[i]}');
}
}
}
}
/// Log d'objet JSON (formaté)
static void json(String label, Map<String, dynamic> data) {
if (!isProduction) {
debugPrint('📋 $label:');
data.forEach((key, value) {
debugPrint(' $key: $value');
});
}
}
}
/// Extension pour faciliter l'utilisation
extension LoggerExtension on String {
void log({String? prefix}) => LoggerService.log(this, prefix: prefix);
void logInfo() => LoggerService.info(this);
void logSuccess() => LoggerService.success(this);
void logWarning() => LoggerService.warning(this);
void logError([dynamic error, StackTrace? stackTrace]) =>
LoggerService.error(this, error, stackTrace);
void logDebug({String emoji = '🔧'}) => LoggerService.debug(this, emoji: emoji);
void logApi() => LoggerService.api(this);
void logDatabase() => LoggerService.database(this);
void logNavigation() => LoggerService.navigation(this);
void logPerformance() => LoggerService.performance(this);
}

View File

@@ -97,6 +97,9 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
}
void _handleEditMembre(MembreModel membre) {
// Récupérer l'amicale actuelle
final amicale = widget.amicaleRepository.getUserAmicale(_currentUser!.fkEntite!);
showDialog(
context: context,
builder: (context) => UserFormDialog(
@@ -104,8 +107,9 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
user: membre.toUserModel(),
showRoleSelector: true,
showActiveCheckbox: true, // Activer la checkbox
allowUsernameEdit: true, // Permettre l'édition du username
// allowSectNameEdit sera automatiquement true via UserForm
allowUsernameEdit: amicale?.chkUsernameManuel == true, // Conditionnel selon amicale
amicale: amicale, // Passer l'amicale
isAdmin: true, // Car on est dans la page admin
availableRoles: const [
RoleOption(
value: 1,
@@ -118,23 +122,22 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
description: 'Peut gérer l\'amicale et ses membres',
),
],
onSubmit: (updatedUser) async {
onSubmit: (updatedUser, {String? password}) async {
try {
// Convertir le UserModel mis à jour vers MembreModel
final updatedMembre =
MembreModel.fromUserModel(updatedUser, membre);
// Utiliser directement updateMembre qui passe par l'API /users
final success =
await widget.membreRepository.updateMembre(updatedMembre);
final success = await widget.membreRepository.updateMembre(
updatedMembre,
password: password,
);
if (success && mounted) {
Navigator.of(context).pop();
ApiException.showSuccess(context,
'Membre ${updatedMembre.firstName} ${updatedMembre.name} mis à jour');
} else if (!success && mounted) {
ApiException.showError(
context, Exception('Erreur lors de la mise à jour'));
}
} catch (e) {
debugPrint('❌ Erreur mise à jour membre: $e');
@@ -557,9 +560,6 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
if (success && mounted) {
ApiException.showSuccess(context,
'Membre ${membre.firstName} ${membre.name} désactivé avec succès');
} else if (mounted) {
ApiException.showError(
context, Exception('Erreur lors de la désactivation'));
}
} catch (e) {
if (mounted) {
@@ -571,6 +571,9 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
void _handleAddMembre() {
if (_currentUser?.fkEntite == null) return;
// Récupérer l'amicale actuelle
final amicale = widget.amicaleRepository.getUserAmicale(_currentUser!.fkEntite!);
// Créer un UserModel vide avec les valeurs par défaut
final newUser = UserModel(
id: 0, // ID temporaire pour nouveau membre
@@ -596,7 +599,9 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
user: newUser,
showRoleSelector: true,
showActiveCheckbox: true,
allowUsernameEdit: true,
allowUsernameEdit: amicale?.chkUsernameManuel == true, // Conditionnel selon amicale
amicale: amicale, // Passer l'amicale
isAdmin: true, // Car on est dans la page admin
availableRoles: const [
RoleOption(
value: 1,
@@ -609,7 +614,7 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
description: 'Peut gérer l\'amicale et ses membres',
),
],
onSubmit: (newUserData) async {
onSubmit: (newUserData, {String? password}) async {
try {
// Créer un nouveau MembreModel directement
final newMembre = MembreModel(
@@ -631,8 +636,10 @@ class _AdminAmicalePageState extends State<AdminAmicalePage> {
);
// Créer le membre via l'API (retourne maintenant le membre créé)
final createdMembre =
await widget.membreRepository.createMembre(newMembre);
final createdMembre = await widget.membreRepository.createMembre(
newMembre,
password: password,
);
if (createdMembre != null && mounted) {
// Fermer le dialog

View File

@@ -434,7 +434,8 @@ class _LoginPageState extends State<LoginPage> {
'Login: Tentative avec type: $_loginType');
final success =
await userRepository.login(
await userRepository.loginWithSpinner(
context,
_usernameController.text.trim(),
_passwordController.text,
type: _loginType,
@@ -575,9 +576,9 @@ class _LoginPageState extends State<LoginPage> {
print(
'Login: Tentative avec type: $_loginType');
// Utiliser directement userRepository avec l'overlay de chargement
// Utiliser le nouveau spinner moderne pour la connexion
final success = await userRepository
.loginWithUI(
.loginWithSpinner(
context,
_usernameController.text.trim(),
_passwordController.text,

View File

@@ -1,11 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:typed_data';
import 'dart:convert';
import 'package:flutter_map/flutter_map.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/repositories/user_repository.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'package:geosector_app/presentation/widgets/mapbox_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:io';
import 'package:geosector_app/presentation/widgets/loading_spin_overlay.dart';
import 'custom_text_field.dart';
class AmicaleForm extends StatefulWidget {
@@ -52,6 +58,13 @@ class _AmicaleFormState extends State<AmicaleForm> {
bool _chkAcceptSms = false;
bool _chkActive = true;
bool _chkStripe = false;
bool _chkMdpManuel = false;
bool _chkUsernameManuel = false;
// Pour l'upload du logo
final ImagePicker _picker = ImagePicker();
XFile? _selectedImage;
String? _logoUrl;
@override
void initState() {
@@ -78,6 +91,10 @@ class _AmicaleFormState extends State<AmicaleForm> {
_chkAcceptSms = amicale?.chkAcceptSms ?? false;
_chkActive = amicale?.chkActive ?? true;
_chkStripe = amicale?.chkStripe ?? false;
_chkMdpManuel = amicale?.chkMdpManuel ?? false;
_chkUsernameManuel = amicale?.chkUsernameManuel ?? false;
// Note : Le logo sera chargé dynamiquement depuis l'API
}
@override
@@ -133,6 +150,8 @@ class _AmicaleFormState extends State<AmicaleForm> {
'chk_copie_mail_recu': amicale.chkCopieMailRecu ? 1 : 0,
'chk_accept_sms': amicale.chkAcceptSms ? 1 : 0,
'chk_stripe': amicale.chkStripe ? 1 : 0,
'chk_mdp_manuel': amicale.chkMdpManuel ? 1 : 0,
'chk_username_manuel': amicale.chkUsernameManuel ? 1 : 0,
};
// Ajouter les champs réservés aux administrateurs si l'utilisateur est admin
@@ -232,6 +251,115 @@ class _AmicaleFormState extends State<AmicaleForm> {
}
}
// Méthode pour sélectionner une image
Future<void> _selectImage() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,
maxWidth: 1024,
maxHeight: 1024,
imageQuality: 85,
);
if (image != null) {
// Vérifier la taille du fichier (limite 5 Mo)
final int fileSize = await image.length();
const int maxSize = 5 * 1024 * 1024; // 5 Mo en octets
if (fileSize > maxSize) {
// Fichier trop volumineux
final double sizeMB = fileSize / (1024 * 1024);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Le fichier est trop volumineux (${sizeMB.toStringAsFixed(2)} Mo). '
'La taille maximale autorisée est de 5 Mo.',
),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 5),
),
);
}
return;
}
setState(() {
_selectedImage = image;
});
// Upload immédiatement après sélection
if (widget.amicale?.id != null) {
await _uploadLogo();
}
}
} catch (e) {
debugPrint('Erreur lors de la sélection de l\'image: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la sélection de l\'image: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
// Méthode pour uploader le logo
Future<void> _uploadLogo() async {
if (_selectedImage == null || widget.amicale?.id == null) return;
OverlayEntry? spinOverlay;
try {
// Afficher le spinner
spinOverlay = LoadingSpinOverlayUtils.show(
context: context,
message: 'Upload du logo en cours...',
blurAmount: 10.0,
showCard: true,
);
// Appeler l'API pour uploader le logo
final response = await widget.apiService?.uploadLogo(
widget.amicale!.id,
_selectedImage!,
);
if (response != null && response['status'] == 'success') {
// Succès
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Logo uploadé avec succès'),
backgroundColor: Colors.green,
),
);
}
// Mettre à jour l'amicale avec le nouveau logo en base64
// Note : Le serveur devrait aussi mettre à jour le logo dans la session
// Pour l'instant on garde l'image sélectionnée en preview
setState(() {
// L'image reste en preview jusqu'au prochain login
});
}
} catch (e) {
debugPrint('Erreur lors de l\'upload du logo: $e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de l\'upload: ${e.toString()}'),
backgroundColor: Colors.red,
),
);
}
} finally {
// Fermer le spinner
LoadingSpinOverlayUtils.hideSpecific(spinOverlay);
}
}
void _submitForm() {
debugPrint('🔧 _submitForm appelée');
@@ -271,6 +399,8 @@ class _AmicaleFormState extends State<AmicaleForm> {
chkAcceptSms: _chkAcceptSms,
chkActive: _chkActive,
chkStripe: _chkStripe,
chkMdpManuel: _chkMdpManuel,
chkUsernameManuel: _chkUsernameManuel,
) ??
AmicaleModel(
id: 0, // Sera remplacé par l'API
@@ -291,6 +421,9 @@ class _AmicaleFormState extends State<AmicaleForm> {
chkCopieMailRecu: _chkCopieMailRecu,
chkAcceptSms: _chkAcceptSms,
chkActive: _chkActive,
chkStripe: _chkStripe,
chkMdpManuel: _chkMdpManuel,
chkUsernameManuel: _chkUsernameManuel,
);
debugPrint('🔧 AmicaleModel créé: ${amicale.name}');
@@ -305,6 +438,10 @@ class _AmicaleFormState extends State<AmicaleForm> {
// Construire la section logo
Widget _buildLogoSection() {
// Vérifier si on est admin d'amicale (role 2)
final userRole = widget.userRepository.getUserRole();
final canUploadLogo = userRole == 2 && !widget.readOnly;
return Container(
width: 150,
height: 150,
@@ -323,40 +460,40 @@ class _AmicaleFormState extends State<AmicaleForm> {
borderRadius: BorderRadius.circular(8),
child: Stack(
children: [
// Image par défaut
// Afficher l'image sélectionnée, ou le logo depuis l'API, ou l'image par défaut
Center(
child: Image.asset(
'assets/images/logo_recu.png',
width: 150,
height: 150,
fit: BoxFit.contain,
),
child: _buildLogoImage(),
),
// Overlay pour indiquer que l'image est modifiable (si non en lecture seule)
if (!widget.readOnly)
// Overlay pour indiquer que l'image est modifiable (si admin d'amicale)
if (canUploadLogo)
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
// TODO: Implémenter la sélection d'image
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Fonctionnalité de modification du logo à venir'),
),
);
},
onTap: _selectImage,
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.1),
color: Colors.black.withOpacity(0.3),
),
child: const Center(
child: Icon(
Icons.camera_alt,
color: Colors.white,
size: 40,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(
Icons.camera_alt,
color: Colors.white,
size: 32,
),
SizedBox(height: 4),
Text(
'Modifier',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
@@ -367,6 +504,102 @@ class _AmicaleFormState extends State<AmicaleForm> {
),
);
}
// Méthode pour construire l'image du logo
Widget _buildLogoImage() {
// 1. Si une image a été sélectionnée localement (preview)
if (_selectedImage != null) {
if (kIsWeb) {
return FutureBuilder<Uint8List>(
future: _selectedImage!.readAsBytes(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Image.memory(
snapshot.data!,
width: 150,
height: 150,
fit: BoxFit.contain,
);
}
return const CircularProgressIndicator();
},
);
} else {
return Image.file(
File(_selectedImage!.path),
width: 150,
height: 150,
fit: BoxFit.contain,
);
}
}
// 2. Si l'amicale a un logo en base64 stocké dans Hive
if (widget.amicale?.logoBase64 != null && widget.amicale!.logoBase64!.isNotEmpty) {
try {
// Le logoBase64 contient déjà le data URL complet (data:image/png;base64,...)
final dataUrl = widget.amicale!.logoBase64!;
// Extraire le base64 du data URL
final base64Data = dataUrl.split(',').last;
final bytes = base64Decode(base64Data);
return Image.memory(
bytes,
width: 150,
height: 150,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
debugPrint('Erreur affichage logo base64: $error');
// En cas d'erreur, essayer l'API
return _buildLogoFromApi();
},
);
} catch (e) {
debugPrint('Erreur décodage base64: $e');
// En cas d'erreur, essayer l'API
return _buildLogoFromApi();
}
}
// 3. Sinon, essayer de charger depuis l'API
return _buildLogoFromApi();
}
// Méthode pour charger le logo depuis l'API
Widget _buildLogoFromApi() {
if (widget.amicale?.id != null && widget.apiService != null) {
// Construire l'URL complète du logo
final logoUrl = '${widget.apiService!.baseUrl}/entites/${widget.amicale!.id}/logo';
return Image.network(
logoUrl,
width: 150,
height: 150,
fit: BoxFit.contain,
headers: {
'Authorization': 'Bearer ${widget.apiService!.sessionId ?? ""}',
},
errorBuilder: (context, error, stackTrace) {
// En cas d'erreur, afficher l'image par défaut
return Image.asset(
'assets/images/logo_recu.png',
width: 150,
height: 150,
fit: BoxFit.contain,
);
},
);
}
// Par défaut, afficher l'image locale
return Image.asset(
'assets/images/logo_recu.png',
width: 150,
height: 150,
fit: BoxFit.contain,
);
}
// Construire la minimap
Widget _buildMiniMap() {
@@ -825,59 +1058,106 @@ class _AmicaleFormState extends State<AmicaleForm> {
),
const SizedBox(height: 8),
// Checkbox Demo
_buildCheckboxOption(
label: "Mode démo",
value: _chkDemo,
onChanged: restrictedFieldsReadOnly
? null
: (value) {
setState(() {
_chkDemo = value!;
});
},
),
const SizedBox(height: 8),
// Options organisées sur 2 colonnes
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Colonne de gauche
Expanded(
child: Column(
children: [
// Checkbox Demo
_buildCheckboxOption(
label: "Mode démo",
value: _chkDemo,
onChanged: restrictedFieldsReadOnly
? null
: (value) {
setState(() {
_chkDemo = value!;
});
},
),
const SizedBox(height: 8),
// Checkbox Copie Mail Reçu
_buildCheckboxOption(
label: "Copie des mails reçus",
value: _chkCopieMailRecu,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_chkCopieMailRecu = value!;
});
},
),
const SizedBox(height: 8),
// Checkbox Copie Mail Reçu
_buildCheckboxOption(
label: "Copie des mails reçus",
value: _chkCopieMailRecu,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_chkCopieMailRecu = value!;
});
},
),
const SizedBox(height: 8),
// Checkbox Accept SMS
_buildCheckboxOption(
label: "Accepte les SMS",
value: _chkAcceptSms,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_chkAcceptSms = value!;
});
},
),
const SizedBox(height: 8),
// Checkbox Accept SMS
_buildCheckboxOption(
label: "Accepte les SMS",
value: _chkAcceptSms,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_chkAcceptSms = value!;
});
},
),
],
),
),
const SizedBox(width: 32),
// Colonne de droite
Expanded(
child: Column(
children: [
// Checkbox Active
_buildCheckboxOption(
label: "Actif",
value: _chkActive,
onChanged: restrictedFieldsReadOnly
? null
: (value) {
setState(() {
_chkActive = value!;
});
},
),
const SizedBox(height: 8),
// Checkbox Active
_buildCheckboxOption(
label: "Actif",
value: _chkActive,
onChanged: restrictedFieldsReadOnly
? null
: (value) {
setState(() {
_chkActive = value!;
});
},
// Checkbox Mot de passe manuel
_buildCheckboxOption(
label: "Saisie manuelle des mots de passe",
value: _chkMdpManuel,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_chkMdpManuel = value!;
});
},
),
const SizedBox(height: 8),
// Checkbox Username manuel
_buildCheckboxOption(
label: "Saisie manuelle des identifiants",
value: _chkUsernameManuel,
onChanged: widget.readOnly
? null
: (value) {
setState(() {
_chkUsernameManuel = value!;
});
},
),
],
),
),
],
),
const SizedBox(height: 25),

View File

@@ -171,9 +171,10 @@ class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
user: user,
readOnly: false,
showRoleSelector: false,
onSubmit: (updatedUser) async {
onSubmit: (updatedUser, {String? password}) async {
try {
// Sauvegarder les modifications de l'utilisateur
// Note: password est ignoré ici car l'utilisateur normal ne peut pas changer son mot de passe
await userRepository.updateUser(updatedUser);
if (context.mounted) {

View File

@@ -1,199 +0,0 @@
import 'package:flutter/material.dart';
import 'dart:ui';
/// Widget d'overlay de chargement amélioré qui affiche une barre de progression
/// avec un effet de flou sur l'arrière-plan et un message détaillé sur l'étape en cours
class LoadingProgressOverlay extends StatefulWidget {
final String? message;
final double progress;
final String? stepDescription;
final Color backgroundColor;
final Color progressColor;
final Color textColor;
final double blurAmount;
final bool showPercentage;
const LoadingProgressOverlay({
super.key,
this.message,
required this.progress,
this.stepDescription,
this.backgroundColor = Colors.black54,
this.progressColor = Colors.white,
this.textColor = Colors.white,
this.blurAmount = 5.0,
this.showPercentage = true,
});
@override
State<LoadingProgressOverlay> createState() => _LoadingProgressOverlayState();
}
class _LoadingProgressOverlayState extends State<LoadingProgressOverlay>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _progressAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_progressAnimation = Tween<double>(begin: 0, end: widget.progress).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
_animationController.forward();
}
@override
void didUpdateWidget(LoadingProgressOverlay oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.progress != widget.progress) {
_progressAnimation = Tween<double>(
begin: oldWidget.progress,
end: widget.progress,
).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
_animationController.reset();
_animationController.forward();
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BackdropFilter(
filter: ImageFilter.blur(
sigmaX: widget.blurAmount, sigmaY: widget.blurAmount),
child: Container(
color: widget.backgroundColor,
child: Center(
child: Container(
width: MediaQuery.of(context).size.width * 0.85,
padding: const EdgeInsets.all(30),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.9),
borderRadius: BorderRadius.circular(20),
boxShadow: const [
BoxShadow(
color: Colors.black45,
blurRadius: 15,
spreadRadius: 5,
offset: Offset(0, 5),
),
],
border: Border.all(
color: Colors.white.withOpacity(0.1),
width: 1.5,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.message != null) ...[
Text(
widget.message!,
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: widget.textColor,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
],
AnimatedBuilder(
animation: _progressAnimation,
builder: (context, child) {
return Column(
children: [
LinearProgressIndicator(
value: _progressAnimation.value,
backgroundColor:
widget.progressColor.withOpacity(0.3),
valueColor: AlwaysStoppedAnimation<Color>(
widget.progressColor),
minHeight: 15,
borderRadius: BorderRadius.circular(8),
),
if (widget.showPercentage) ...[
const SizedBox(height: 8),
Text(
'${(_progressAnimation.value * 100).toInt()}%',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: widget.textColor,
letterSpacing: 1.2,
),
),
],
],
);
},
),
if (widget.stepDescription != null) ...[
const SizedBox(height: 16),
Text(
widget.stepDescription!,
style: TextStyle(
fontSize: 16,
color: widget.textColor.withOpacity(0.9),
fontStyle: FontStyle.italic,
),
textAlign: TextAlign.center,
),
],
],
),
),
),
),
);
}
}
/// Classe utilitaire pour gérer l'overlay de chargement avec progression
class LoadingProgressOverlayUtils {
/// Méthode pour afficher l'overlay de chargement avec progression
static OverlayEntry show({
required BuildContext context,
String? message,
double progress = 0.0,
String? stepDescription,
double blurAmount = 5.0,
bool showPercentage = true,
}) {
final overlayEntry = OverlayEntry(
builder: (context) => LoadingProgressOverlay(
message: message,
progress: progress,
stepDescription: stepDescription,
blurAmount: blurAmount,
showPercentage: showPercentage,
),
);
Overlay.of(context).insert(overlayEntry);
return overlayEntry;
}
/// Méthode pour mettre à jour l'overlay existant
static void update({
required OverlayEntry overlayEntry,
String? message,
required double progress,
String? stepDescription,
}) {
overlayEntry.markNeedsBuild();
}
}

View File

@@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
import 'dart:ui';
/// Widget d'overlay de chargement moderne avec spinner circulaire
/// Affiche un spinner animé avec fond flou et message optionnel
class LoadingSpinOverlay extends StatefulWidget {
final String? message;
final Color backgroundColor;
final Color spinnerColor;
final Color textColor;
final double blurAmount;
final double spinnerSize;
final bool showCard;
const LoadingSpinOverlay({
super.key,
this.message,
this.backgroundColor = Colors.black54,
this.spinnerColor = Colors.blue,
this.textColor = Colors.white,
this.blurAmount = 8.0,
this.spinnerSize = 50.0,
this.showCard = true,
});
@override
State<LoadingSpinOverlay> createState() => _LoadingSpinOverlayState();
}
class _LoadingSpinOverlayState extends State<LoadingSpinOverlay>
with TickerProviderStateMixin {
late AnimationController _fadeController;
late AnimationController _rotationController;
late Animation<double> _fadeAnimation;
late Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
_fadeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_rotationController = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeController,
curve: Curves.easeInOut,
));
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 2 * 3.14159,
).animate(CurvedAnimation(
parent: _rotationController,
curve: Curves.linear,
));
_fadeController.forward();
_rotationController.repeat();
}
@override
void dispose() {
_fadeController.dispose();
_rotationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _fadeAnimation,
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: widget.blurAmount,
sigmaY: widget.blurAmount,
),
child: Container(
color: widget.backgroundColor,
child: Center(
child: widget.showCard
? _buildCardContent()
: _buildSimpleContent(),
),
),
),
);
}
Widget _buildCardContent() {
return Material(
color: Colors.transparent,
child: Container(
padding: const EdgeInsets.all(32),
constraints: const BoxConstraints(
maxWidth: 280,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.92), // Semi-transparent
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 20,
spreadRadius: 2,
offset: const Offset(0, 8),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Spinner simple de Flutter
SizedBox(
width: widget.spinnerSize,
height: widget.spinnerSize,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(widget.spinnerColor),
),
),
if (widget.message != null) ...[
const SizedBox(height: 24),
Text(
widget.message!,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.grey[800],
letterSpacing: 0.3,
),
textAlign: TextAlign.center,
),
],
],
),
),
);
}
Widget _buildSimpleContent() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: widget.spinnerSize,
height: widget.spinnerSize,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(widget.spinnerColor),
),
),
if (widget.message != null) ...[
const SizedBox(height: 20),
Text(
widget.message!,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: widget.textColor,
letterSpacing: 0.5,
),
textAlign: TextAlign.center,
),
],
],
);
}
}
/// Classe utilitaire pour gérer l'overlay de chargement avec spinner
class LoadingSpinOverlayUtils {
static OverlayEntry? _currentOverlay;
/// Affiche l'overlay de chargement avec spinner
static OverlayEntry show({
required BuildContext context,
String? message,
double blurAmount = 8.0,
bool showCard = true,
Color? spinnerColor,
}) {
// Fermer l'overlay existant s'il y en a un
hide();
final theme = Theme.of(context);
final overlayEntry = OverlayEntry(
builder: (context) => LoadingSpinOverlay(
message: message,
blurAmount: blurAmount,
showCard: showCard,
spinnerColor: spinnerColor ?? theme.colorScheme.primary,
),
);
_currentOverlay = overlayEntry;
Overlay.of(context).insert(overlayEntry);
return overlayEntry;
}
/// Met à jour le message de l'overlay existant
static void updateMessage({
required OverlayEntry overlayEntry,
String? message,
}) {
overlayEntry.markNeedsBuild();
}
/// Cache l'overlay de chargement
static void hide() {
_currentOverlay?.remove();
_currentOverlay = null;
}
/// Cache un overlay spécifique
static void hideSpecific(OverlayEntry? overlayEntry) {
overlayEntry?.remove();
if (_currentOverlay == overlayEntry) {
_currentOverlay = null;
}
}
}

View File

@@ -8,6 +8,7 @@ class MembreRowWidget extends StatelessWidget {
final Function(MembreModel)? onResetPassword;
final bool isAlternate;
final VoidCallback? onTap;
final bool isMobile;
const MembreRowWidget({
super.key,
@@ -17,6 +18,7 @@ class MembreRowWidget extends StatelessWidget {
this.onResetPassword,
this.isAlternate = false,
this.onTap,
this.isMobile = false,
});
@override
@@ -37,20 +39,29 @@ class MembreRowWidget extends StatelessWidget {
),
child: Row(
children: [
// ... existing row content ...
// ID
Expanded(
flex: 1,
child: Text(
membre.id.toString() ?? '',
style: theme.textTheme.bodyMedium,
// ID - masqué en mobile
if (!isMobile)
Expanded(
flex: 1,
child: Text(
membre.id.toString() ?? '',
style: theme.textTheme.bodyMedium,
),
),
// Identifiant (username) - masqué en mobile
if (!isMobile)
Expanded(
flex: 2,
child: Text(
membre.username ?? '',
style: theme.textTheme.bodyMedium,
),
),
),
// Prénom
Expanded(
flex: 2,
flex: isMobile ? 2 : 2,
child: Text(
membre.firstName ?? '',
style: theme.textTheme.bodyMedium,
@@ -59,47 +70,44 @@ class MembreRowWidget extends StatelessWidget {
// Nom
Expanded(
flex: 2,
flex: isMobile ? 2 : 2,
child: Text(
membre.name ?? '',
style: theme.textTheme.bodyMedium,
),
),
// Email
Expanded(
flex: 3,
child: Text(
membre.email ?? '',
style: theme.textTheme.bodyMedium,
// Email - masqué en mobile
if (!isMobile)
Expanded(
flex: 3,
child: Text(
membre.email ?? '',
style: theme.textTheme.bodyMedium,
),
),
),
// Rôle
Expanded(
flex: 1,
child: Text(
_getRoleName(membre.role),
style: theme.textTheme.bodyMedium,
// Rôle - masqué en mobile
if (!isMobile)
Expanded(
flex: 1,
child: Text(
_getRoleName(membre.role),
style: theme.textTheme.bodyMedium,
),
),
),
// Statut
Expanded(
flex: 1,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
decoration: BoxDecoration(
color: _getStatusColor(membre.isActive),
borderRadius: BorderRadius.circular(12.0),
),
child: Text(
membre.isActive == true ? 'Actif' : 'Inactif',
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.w500,
child: Center(
child: Tooltip(
message: membre.isActive == true ? 'Actif' : 'Inactif',
child: Icon(
membre.isActive == true ? Icons.check_circle : Icons.cancel,
color: membre.isActive == true ? Colors.green : Colors.red,
size: 24,
),
textAlign: TextAlign.center,
),
),
),
@@ -107,21 +115,21 @@ class MembreRowWidget extends StatelessWidget {
// Actions
if (onEdit != null || onDelete != null || onResetPassword != null)
Expanded(
flex: 2,
flex: isMobile ? 2 : 2,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
// Bouton reset password (uniquement pour les membres actifs)
if (onResetPassword != null && membre.isActive == true)
IconButton(
icon: const Icon(Icons.lock_reset, size: 22),
icon: Icon(Icons.lock_reset, size: isMobile ? 20 : 22),
onPressed: () => onResetPassword!(membre),
tooltip: 'Réinitialiser le mot de passe',
color: theme.colorScheme.primary,
),
if (onDelete != null)
IconButton(
icon: const Icon(Icons.delete, size: 22),
icon: Icon(Icons.delete, size: isMobile ? 20 : 22),
onPressed: () => onDelete!(membre),
tooltip: 'Supprimer',
color: theme.colorScheme.error,

View File

@@ -32,6 +32,8 @@ class MembreTableWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final screenWidth = MediaQuery.of(context).size.width;
final isMobile = screenWidth < 768;
return Container(
height: height,
@@ -61,21 +63,35 @@ class MembreTableWidget extends StatelessWidget {
),
child: Row(
children: [
// ID
Expanded(
flex: 1,
child: Text(
'ID',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
// ID - masqué en mobile
if (!isMobile)
Expanded(
flex: 1,
child: Text(
'ID',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
),
// Identifiant (username) - masqué en mobile
if (!isMobile)
Expanded(
flex: 2,
child: Text(
'Identifiant',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
),
),
// Prénom (firstName)
Expanded(
flex: 2,
flex: isMobile ? 2 : 2,
child: Text(
'Prénom',
style: theme.textTheme.titleSmall?.copyWith(
@@ -87,7 +103,7 @@ class MembreTableWidget extends StatelessWidget {
// Nom (name)
Expanded(
flex: 2,
flex: isMobile ? 2 : 2,
child: Text(
'Nom',
style: theme.textTheme.titleSmall?.copyWith(
@@ -97,29 +113,31 @@ class MembreTableWidget extends StatelessWidget {
),
),
// Email
Expanded(
flex: 3,
child: Text(
'Email',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
// Email - masqué en mobile
if (!isMobile)
Expanded(
flex: 3,
child: Text(
'Email',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
),
),
// Rôle (fkRole)
Expanded(
flex: 1,
child: Text(
'Rôle',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
// Rôle (fkRole) - masqué en mobile
if (!isMobile)
Expanded(
flex: 1,
child: Text(
'Rôle',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
),
),
// Statut
Expanded(
@@ -136,7 +154,7 @@ class MembreTableWidget extends StatelessWidget {
// Actions (si onEdit, onDelete ou onResetPassword sont fournis)
if (onEdit != null || onDelete != null || onResetPassword != null)
Expanded(
flex: 2,
flex: isMobile ? 2 : 2,
child: Text(
'Actions',
style: theme.textTheme.titleSmall?.copyWith(
@@ -152,14 +170,14 @@ class MembreTableWidget extends StatelessWidget {
// Corps du tableau
Expanded(
child: _buildTableContent(context),
child: _buildTableContent(context, isMobile),
),
],
),
);
}
Widget _buildTableContent(BuildContext context) {
Widget _buildTableContent(BuildContext context, bool isMobile) {
// Afficher un indicateur de chargement si isLoading est true
if (isLoading) {
return const Center(child: CircularProgressIndicator());
@@ -193,6 +211,7 @@ class MembreTableWidget extends StatelessWidget {
onResetPassword: onResetPassword,
isAlternate: index % 2 == 1,
onTap: onEdit != null ? () => onEdit!(membre) : null,
isMobile: isMobile,
);
},
);

View File

@@ -1,7 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'dart:math';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/core/services/api_service.dart';
import 'custom_text_field.dart';
class UserForm extends StatefulWidget {
@@ -10,6 +13,8 @@ class UserForm extends StatefulWidget {
final bool readOnly;
final bool allowUsernameEdit;
final bool allowSectNameEdit;
final AmicaleModel? amicale; // Nouveau paramètre pour l'amicale
final bool isAdmin; // Nouveau paramètre pour savoir si c'est un admin
const UserForm({
super.key,
@@ -18,6 +23,8 @@ class UserForm extends StatefulWidget {
this.readOnly = false,
this.allowUsernameEdit = false,
this.allowSectNameEdit = false,
this.amicale,
this.isAdmin = false,
});
@override
@@ -37,11 +44,19 @@ class _UserFormState extends State<UserForm> {
late final TextEditingController _emailController;
late final TextEditingController _dateNaissanceController;
late final TextEditingController _dateEmbaucheController;
late final TextEditingController _passwordController; // Nouveau controller pour le mot de passe
// Form values
int _fkTitre = 1; // 1 = M., 2 = Mme
DateTime? _dateNaissance;
DateTime? _dateEmbauche;
// Pour la génération automatique d'username
bool _isGeneratingUsername = false;
final Random _random = Random();
// Pour afficher/masquer le mot de passe
bool _obscurePassword = true;
@override
void initState() {
@@ -64,11 +79,34 @@ class _UserFormState extends State<UserForm> {
_dateEmbaucheController = TextEditingController(text: _dateEmbauche != null ? DateFormat('dd/MM/yyyy').format(_dateEmbauche!) : '');
_passwordController = TextEditingController(); // Initialiser le controller du mot de passe
_fkTitre = user?.fkTitre ?? 1;
// Ajouter des listeners pour auto-générer l'username en création
if (widget.user?.id == 0 && widget.isAdmin && widget.amicale?.chkUsernameManuel == true) {
_nameController.addListener(_onNameOrSectNameChanged);
_sectNameController.addListener(_onNameOrSectNameChanged);
}
}
void _onNameOrSectNameChanged() {
// Auto-générer username seulement en création et si le champ username est vide
if (widget.user?.id == 0 &&
_usernameController.text.isEmpty &&
(_nameController.text.isNotEmpty || _sectNameController.text.isNotEmpty)) {
_generateAndCheckUsername();
}
}
@override
void dispose() {
// Retirer les listeners si ajoutés
if (widget.user?.id == 0 && widget.isAdmin && widget.amicale?.chkUsernameManuel == true) {
_nameController.removeListener(_onNameOrSectNameChanged);
_sectNameController.removeListener(_onNameOrSectNameChanged);
}
_usernameController.dispose();
_firstNameController.dispose();
_nameController.dispose();
@@ -78,6 +116,7 @@ class _UserFormState extends State<UserForm> {
_emailController.dispose();
_dateNaissanceController.dispose();
_dateEmbaucheController.dispose();
_passwordController.dispose();
super.dispose();
}
@@ -98,13 +137,49 @@ class _UserFormState extends State<UserForm> {
void _selectDate(BuildContext context, bool isDateNaissance) {
// Utiliser un bloc try-catch pour capturer toutes les erreurs possibles
try {
// Afficher le sélecteur de date sans spécifier de locale
// Déterminer la date initiale
DateTime initialDate;
if (isDateNaissance) {
initialDate = _dateNaissance ?? DateTime.now().subtract(const Duration(days: 365 * 30)); // 30 ans par défaut
} else {
initialDate = _dateEmbauche ?? DateTime.now();
}
// S'assurer que la date initiale est dans la plage autorisée
if (initialDate.isAfter(DateTime.now())) {
initialDate = DateTime.now();
}
if (initialDate.isBefore(DateTime(1900))) {
initialDate = DateTime(1950);
}
// Afficher le sélecteur de date avec locale française
showDatePicker(
context: context,
initialDate: DateTime.now(), // Toujours utiliser la date actuelle
initialDate: initialDate,
firstDate: DateTime(1900),
lastDate: DateTime.now(),
// Ne pas spécifier de locale pour éviter les problèmes
locale: const Locale('fr', 'FR'), // Forcer la locale française
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: Theme.of(context).colorScheme.copyWith(
primary: Theme.of(context).colorScheme.primary,
onPrimary: Colors.white,
surface: Colors.white,
onSurface: Colors.black,
),
),
child: child!,
);
},
helpText: isDateNaissance ? 'SÉLECTIONNER LA DATE DE NAISSANCE' : 'SÉLECTIONNER LA DATE D\'EMBAUCHE',
cancelText: 'ANNULER',
confirmText: 'VALIDER',
fieldLabelText: 'Entrer une date',
fieldHintText: 'jj/mm/aaaa',
errorFormatText: 'Format de date invalide',
errorInvalidText: 'Date invalide',
).then((DateTime? picked) {
// Vérifier si une date a été sélectionnée
if (picked != null) {
@@ -141,30 +216,247 @@ class _UserFormState extends State<UserForm> {
}
}
// Nettoyer une chaîne pour l'username
String _cleanString(String input) {
return input.toLowerCase()
.replaceAll(RegExp(r'[^a-z0-9]'), ''); // Garder seulement lettres et chiffres
}
// Extraire une partie aléatoire d'une chaîne
String _extractRandomPart(String input, int minLength, int maxLength) {
if (input.isEmpty) return '';
final cleaned = _cleanString(input);
if (cleaned.isEmpty) return '';
final length = minLength + _random.nextInt(maxLength - minLength + 1);
if (cleaned.length <= length) return cleaned;
// Prendre les premiers caractères jusqu'à la longueur désirée
return cleaned.substring(0, length);
}
// Générer un username selon l'algorithme spécifié
String _generateUsername() {
// Récupérer les données nécessaires
final nom = _nameController.text.isNotEmpty ? _nameController.text : _sectNameController.text;
final codePostal = widget.amicale?.codePostal ?? '';
final ville = widget.amicale?.ville ?? '';
// Nettoyer et extraire les parties
final nomPart = _extractRandomPart(nom, 2, 5);
final cpPart = _extractRandomPart(codePostal, 2, 3);
final villePart = _extractRandomPart(ville, 2, 4);
final nombreAleatoire = 10 + _random.nextInt(990); // 10 à 999
// Choisir des séparateurs aléatoires (uniquement ceux autorisés: ., _, -)
final separateurs = ['', '.', '_', '-'];
final sep1 = separateurs[_random.nextInt(separateurs.length)];
final sep2 = separateurs[_random.nextInt(separateurs.length)];
// Assembler l'username
String username = '$nomPart$sep1$cpPart$sep2$villePart$nombreAleatoire';
// Si trop court, ajouter des chiffres pour atteindre minimum 10 caractères
while (username.length < 10) {
username += _random.nextInt(10).toString();
}
// S'assurer que l'username ne contient que des caractères autorisés (a-z, 0-9, ., -, _)
// Normalement déjà le cas avec notre algorithme, mais au cas où
username = username.toLowerCase().replaceAll(RegExp(r'[^a-z0-9._-]'), '');
return username;
}
// Vérifier la disponibilité d'un username via l'API
Future<Map<String, dynamic>> _checkUsernameAvailability(String username) async {
try {
final response = await ApiService.instance.post(
'/users/check-username',
data: {'username': username},
);
if (response.statusCode == 200) {
return response.data;
}
return {'available': false};
} catch (e) {
debugPrint('Erreur lors de la vérification de l\'username: $e');
return {'available': false};
}
}
// Générer et vérifier un username jusqu'à en trouver un disponible
Future<void> _generateAndCheckUsername() async {
if (_isGeneratingUsername) return; // Éviter les appels multiples
setState(() {
_isGeneratingUsername = true;
});
try {
int attempts = 0;
const maxAttempts = 10;
while (attempts < maxAttempts) {
final username = _generateUsername();
debugPrint('Tentative ${attempts + 1}: Vérification de $username');
final result = await _checkUsernameAvailability(username);
if (result['available'] == true) {
// Username disponible, l'utiliser
setState(() {
_usernameController.text = username;
});
debugPrint('✅ Username disponible trouvé: $username');
break;
} else {
// Si l'API propose des suggestions, essayer la première
if (result['suggestions'] != null && result['suggestions'].isNotEmpty) {
final suggestion = result['suggestions'][0];
debugPrint('Vérification de la suggestion: $suggestion');
final suggestionResult = await _checkUsernameAvailability(suggestion);
if (suggestionResult['available'] == true) {
setState(() {
_usernameController.text = suggestion;
});
debugPrint('✅ Suggestion disponible utilisée: $suggestion');
break;
}
}
}
attempts++;
}
if (attempts >= maxAttempts) {
debugPrint('⚠️ Impossible de trouver un username disponible après $maxAttempts tentatives');
}
} finally {
setState(() {
_isGeneratingUsername = false;
});
}
}
// Valider le mot de passe selon les règles
String? _validatePassword(String? value) {
if (value == null || value.isEmpty) {
// Pour un nouveau membre, le mot de passe est obligatoire si le champ est affiché
if (widget.user?.id == 0) {
return "Veuillez entrer un mot de passe";
}
return null; // En modification, vide = garder l'ancien
}
// Faire un trim pour retirer les espaces en début/fin
final trimmedValue = value.trim();
if (trimmedValue.isEmpty) {
return "Le mot de passe ne peut pas être vide";
}
// Vérifier qu'il n'y a pas d'espaces dans le mot de passe
if (trimmedValue.contains(' ')) {
return "Le mot de passe ne doit pas contenir d'espaces";
}
// Vérifier la longueur
if (trimmedValue.length < 12) {
return "Le mot de passe doit contenir au moins 12 caractères";
}
if (trimmedValue.length > 16) {
return "Le mot de passe ne doit pas dépasser 16 caractères";
}
// Vérifier qu'il n'est pas égal au username (après trim des deux)
if (trimmedValue == _usernameController.text.trim()) {
return "Le mot de passe ne doit pas être identique au nom d'utilisateur";
}
// Vérifier la présence d'au moins une minuscule
if (!trimmedValue.contains(RegExp(r'[a-z]'))) {
return "Le mot de passe doit contenir au moins une lettre minuscule";
}
// Vérifier la présence d'au moins une majuscule
if (!trimmedValue.contains(RegExp(r'[A-Z]'))) {
return "Le mot de passe doit contenir au moins une lettre majuscule";
}
// Vérifier la présence d'au moins un chiffre
if (!trimmedValue.contains(RegExp(r'[0-9]'))) {
return "Le mot de passe doit contenir au moins un chiffre";
}
// Vérifier la présence d'au moins un caractère spécial
if (!trimmedValue.contains(RegExp(r'[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]'))) {
return "Le mot de passe doit contenir au moins un caractère spécial (!@#\$%^&*()_+-=[]{}|;:,.<>?)";
}
return null;
}
// Générer un mot de passe aléatoire respectant les règles
String _generatePassword() {
const String lowercase = 'abcdefghijklmnopqrstuvwxyz';
const String uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const String digits = '0123456789';
const String special = '!@#\$%^&*()_+-=[]{}|;:,.<>?';
// Longueur aléatoire entre 12 et 16
final length = 12 + _random.nextInt(5);
// S'assurer d'avoir au moins un caractère de chaque type
List<String> password = [];
password.add(lowercase[_random.nextInt(lowercase.length)]);
password.add(uppercase[_random.nextInt(uppercase.length)]);
password.add(digits[_random.nextInt(digits.length)]);
password.add(special[_random.nextInt(special.length)]);
// Compléter avec des caractères aléatoires
const String allChars = lowercase + uppercase + digits + special;
for (int i = password.length; i < length; i++) {
password.add(allChars[_random.nextInt(allChars.length)]);
}
// Mélanger les caractères
password.shuffle(_random);
return password.join('');
}
// Méthode publique pour récupérer le mot de passe si défini
String? getPassword() {
final password = _passwordController.text.trim();
return password.isNotEmpty ? password : null;
}
// Méthode publique pour valider et récupérer l'utilisateur
UserModel? validateAndGetUser() {
if (_formKey.currentState!.validate()) {
return widget.user?.copyWith(
username: _usernameController.text,
firstName: _firstNameController.text,
name: _nameController.text,
sectName: _sectNameController.text,
phone: _phoneController.text,
mobile: _mobileController.text,
email: _emailController.text,
username: _usernameController.text.trim(), // Appliquer trim
firstName: _firstNameController.text.trim(),
name: _nameController.text.trim(),
sectName: _sectNameController.text.trim(),
phone: _phoneController.text.trim(),
mobile: _mobileController.text.trim(),
email: _emailController.text.trim(),
fkTitre: _fkTitre,
dateNaissance: _dateNaissance,
dateEmbauche: _dateEmbauche,
) ??
UserModel(
id: 0,
username: _usernameController.text,
firstName: _firstNameController.text,
name: _nameController.text,
sectName: _sectNameController.text,
phone: _phoneController.text,
mobile: _mobileController.text,
email: _emailController.text,
username: _usernameController.text.trim(), // Appliquer trim
firstName: _firstNameController.text.trim(),
name: _nameController.text.trim(),
sectName: _sectNameController.text.trim(),
phone: _phoneController.text.trim(),
mobile: _mobileController.text.trim(),
email: _emailController.text.trim(),
fkTitre: _fkTitre,
dateNaissance: _dateNaissance,
dateEmbauche: _dateEmbauche,
@@ -180,90 +472,36 @@ class _UserFormState extends State<UserForm> {
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isWideScreen = MediaQuery.of(context).size.width > 900;
// Déterminer si on doit afficher le champ username selon les règles
final bool shouldShowUsernameField = widget.isAdmin && widget.amicale?.chkUsernameManuel == true;
// Déterminer si le username est éditable (seulement en création, jamais en modification)
final bool canEditUsername = shouldShowUsernameField && widget.allowUsernameEdit && widget.user?.id == 0;
// Déterminer si on doit afficher le champ mot de passe
final bool shouldShowPasswordField = widget.isAdmin && widget.amicale?.chkMdpManuel == true;
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Ligne 1: Username et Email (si écran large)
if (isWideScreen)
Row(
children: [
Expanded(
child: CustomTextField(
controller: _usernameController,
label: "Nom d'utilisateur",
readOnly: !widget.allowUsernameEdit, // Utiliser le paramètre
prefixIcon: Icons.account_circle,
isRequired: widget.allowUsernameEdit,
validator: widget.allowUsernameEdit
? (value) {
if (value == null || value.isEmpty) {
return "Veuillez entrer le nom d'utilisateur";
}
return null;
}
: null,
),
),
const SizedBox(width: 16),
Expanded(
child: CustomTextField(
controller: _emailController,
label: "Email",
keyboardType: TextInputType.emailAddress,
readOnly: widget.readOnly,
isRequired: true, // Email toujours obligatoire
validator: (value) {
if (value == null || value.isEmpty) {
return "Veuillez entrer l'adresse email";
}
if (!value.contains('@') || !value.contains('.')) {
return "Veuillez entrer une adresse email valide";
}
return null;
},
),
),
],
)
else ...[
// Version mobile: Username seul
CustomTextField(
controller: _usernameController,
label: "Nom d'utilisateur",
readOnly: !widget.allowUsernameEdit, // Utiliser le paramètre
prefixIcon: Icons.account_circle,
isRequired: widget.allowUsernameEdit, // Obligatoire si éditable
validator: widget.allowUsernameEdit
? (value) {
if (value == null || value.isEmpty) {
return "Veuillez entrer le nom d'utilisateur";
}
return null;
}
: null,
),
const SizedBox(height: 16),
// Email seul en mobile
CustomTextField(
controller: _emailController,
label: "Email",
keyboardType: TextInputType.emailAddress,
readOnly: widget.readOnly,
isRequired: true, // Email toujours obligatoire
validator: (value) {
if (value == null || value.isEmpty) {
return "Veuillez entrer l'adresse email";
}
if (!value.contains('@') || !value.contains('.')) {
return "Veuillez entrer une adresse email valide";
}
return null;
},
),
],
// Email seul sur la première ligne
CustomTextField(
controller: _emailController,
label: "Email",
keyboardType: TextInputType.emailAddress,
readOnly: widget.readOnly,
isRequired: true,
validator: (value) {
if (value == null || value.isEmpty) {
return "Veuillez entrer l'adresse email";
}
if (!value.contains('@') || !value.contains('.')) {
return "Veuillez entrer une adresse email valide";
}
return null;
},
),
const SizedBox(height: 16),
// Titre (M. ou Mme)
@@ -378,7 +616,7 @@ class _UserFormState extends State<UserForm> {
const SizedBox(height: 16),
],
// Ligne 3: Téléphones (fixe et mobile)
// Ligne 2: Téléphones (fixe et mobile)
if (isWideScreen)
Row(
children: [
@@ -458,6 +696,224 @@ class _UserFormState extends State<UserForm> {
),
],
const SizedBox(height: 16),
// Ligne 3: Username et Password (si applicable)
if (shouldShowUsernameField || shouldShowPasswordField) ...[
if (isWideScreen)
Row(
children: [
if (shouldShowUsernameField)
Expanded(
child: CustomTextField(
controller: _usernameController,
label: "Nom d'utilisateur",
readOnly: !canEditUsername,
prefixIcon: Icons.account_circle,
isRequired: canEditUsername,
suffixIcon: (widget.user?.id == 0 && canEditUsername)
? _isGeneratingUsername
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
)
: IconButton(
icon: Icon(Icons.refresh),
onPressed: _generateAndCheckUsername,
tooltip: "Générer un nom d'utilisateur",
)
: null,
helperText: canEditUsername
? "Min. 10 caractères (a-z, 0-9, . - _)"
: null,
validator: canEditUsername
? (value) {
if (value == null || value.isEmpty) {
return "Veuillez entrer le nom d'utilisateur";
}
// Faire un trim pour retirer les espaces en début/fin
final trimmedValue = value.trim();
if (trimmedValue.isEmpty) {
return "Le nom d'utilisateur ne peut pas être vide";
}
// Vérifier qu'il n'y a pas d'espaces dans l'username
if (trimmedValue.contains(' ')) {
return "Le nom d'utilisateur ne doit pas contenir d'espaces";
}
// Vérifier la longueur minimale
if (trimmedValue.length < 10) {
return "Le nom d'utilisateur doit contenir au moins 10 caractères";
}
// Vérifier les caractères autorisés (a-z, 0-9, ., -, _)
if (!RegExp(r'^[a-z0-9._-]+$').hasMatch(trimmedValue)) {
return "Caractères autorisés : lettres minuscules, chiffres, . - _";
}
return null;
}
: null,
),
),
if (shouldShowUsernameField && shouldShowPasswordField)
const SizedBox(width: 16),
if (shouldShowPasswordField)
Expanded(
child: CustomTextField(
controller: _passwordController,
label: "Mot de passe",
obscureText: _obscurePassword,
readOnly: widget.readOnly,
prefixIcon: Icons.lock,
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Bouton pour afficher/masquer le mot de passe
IconButton(
icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
tooltip: _obscurePassword ? "Afficher le mot de passe" : "Masquer le mot de passe",
),
// Bouton pour générer un mot de passe (seulement si éditable)
if (!widget.readOnly)
IconButton(
icon: Icon(Icons.auto_awesome),
onPressed: () {
final newPassword = _generatePassword();
setState(() {
_passwordController.text = newPassword;
_obscurePassword = false; // Afficher le mot de passe généré
});
// Revalider le formulaire
_formKey.currentState?.validate();
},
tooltip: "Générer un mot de passe sécurisé",
),
],
),
helperText: widget.user?.id != 0
? "Laissez vide pour conserver le mot de passe actuel"
: "12-16 car. avec min/maj, chiffres et spéciaux (!@#\$%^&*()_+-=[]{}|;:,.<>?)",
validator: _validatePassword,
),
),
// Si seulement un des deux est affiché, ajouter un Expanded vide pour garder l'alignement
if ((shouldShowUsernameField && !shouldShowPasswordField) || (!shouldShowUsernameField && shouldShowPasswordField))
const Expanded(child: SizedBox()),
],
)
else ...[
// Version mobile: Username et Password séparés
if (shouldShowUsernameField) ...[
CustomTextField(
controller: _usernameController,
label: "Nom d'utilisateur",
readOnly: !canEditUsername,
prefixIcon: Icons.account_circle,
isRequired: canEditUsername,
suffixIcon: (widget.user?.id == 0 && canEditUsername)
? _isGeneratingUsername
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
)
: IconButton(
icon: Icon(Icons.refresh),
onPressed: _generateAndCheckUsername,
tooltip: "Générer un nom d'utilisateur",
)
: null,
helperText: canEditUsername
? "Min. 10 caractères (a-z, 0-9, . - _)"
: null,
validator: canEditUsername
? (value) {
if (value == null || value.isEmpty) {
return "Veuillez entrer le nom d'utilisateur";
}
// Faire un trim pour retirer les espaces en début/fin
final trimmedValue = value.trim();
if (trimmedValue.isEmpty) {
return "Le nom d'utilisateur ne peut pas être vide";
}
// Vérifier qu'il n'y a pas d'espaces dans l'username
if (trimmedValue.contains(' ')) {
return "Le nom d'utilisateur ne doit pas contenir d'espaces";
}
// Vérifier la longueur minimale
if (trimmedValue.length < 10) {
return "Le nom d'utilisateur doit contenir au moins 10 caractères";
}
// Vérifier les caractères autorisés (a-z, 0-9, ., -, _)
if (!RegExp(r'^[a-z0-9._-]+$').hasMatch(trimmedValue)) {
return "Caractères autorisés : lettres minuscules, chiffres, . - _";
}
return null;
}
: null,
),
const SizedBox(height: 16),
],
if (shouldShowPasswordField) ...[
CustomTextField(
controller: _passwordController,
label: "Mot de passe",
obscureText: _obscurePassword,
readOnly: widget.readOnly,
prefixIcon: Icons.lock,
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Bouton pour afficher/masquer le mot de passe
IconButton(
icon: Icon(_obscurePassword ? Icons.visibility : Icons.visibility_off),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
tooltip: _obscurePassword ? "Afficher le mot de passe" : "Masquer le mot de passe",
),
// Bouton pour générer un mot de passe (seulement si éditable)
if (!widget.readOnly)
IconButton(
icon: Icon(Icons.auto_awesome),
onPressed: () {
final newPassword = _generatePassword();
setState(() {
_passwordController.text = newPassword;
_obscurePassword = false; // Afficher le mot de passe généré
});
// Revalider le formulaire
_formKey.currentState?.validate();
},
tooltip: "Générer un mot de passe sécurisé",
),
],
),
helperText: widget.user?.id != 0
? "Laissez vide pour conserver le mot de passe actuel"
: "12-16 car. avec min/maj, chiffres et spéciaux (!@#\$%^&*()_+-=[]{}|;:,.<>?)",
validator: _validatePassword,
),
const SizedBox(height: 16),
],
],
const SizedBox(height: 16),
],
// Ligne 4: Dates (naissance et embauche)
if (isWideScreen)

View File

@@ -1,16 +1,19 @@
import 'package:flutter/material.dart';
import 'package:geosector_app/core/data/models/user_model.dart';
import 'package:geosector_app/core/data/models/amicale_model.dart';
import 'package:geosector_app/presentation/widgets/user_form.dart';
class UserFormDialog extends StatefulWidget {
final UserModel? user;
final String title;
final bool readOnly;
final Function(UserModel)? onSubmit;
final Function(UserModel, {String? password})? onSubmit; // Modifié pour inclure le mot de passe
final bool showRoleSelector;
final List<RoleOption>? availableRoles;
final bool showActiveCheckbox;
final bool allowUsernameEdit;
final AmicaleModel? amicale; // Nouveau paramètre
final bool isAdmin; // Nouveau paramètre
const UserFormDialog({
super.key,
@@ -22,6 +25,8 @@ class UserFormDialog extends StatefulWidget {
this.availableRoles,
this.showActiveCheckbox = false,
this.allowUsernameEdit = false,
this.amicale,
this.isAdmin = false,
});
@override
@@ -55,6 +60,7 @@ class _UserFormDialogState extends State<UserFormDialog> {
void _handleSubmit() async {
// Utiliser la méthode validateAndGetUser du UserForm
final userData = _userFormKey.currentState?.validateAndGetUser();
final password = _userFormKey.currentState?.getPassword(); // Récupérer le mot de passe
if (userData != null) {
var finalUser = userData;
@@ -70,7 +76,7 @@ class _UserFormDialogState extends State<UserFormDialog> {
}
if (widget.onSubmit != null) {
widget.onSubmit!(finalUser);
widget.onSubmit!(finalUser, password: password); // Passer le mot de passe
}
}
}
@@ -200,6 +206,8 @@ class _UserFormDialogState extends State<UserFormDialog> {
readOnly: widget.readOnly,
allowUsernameEdit: widget.allowUsernameEdit,
allowSectNameEdit: widget.allowUsernameEdit,
amicale: widget.amicale, // Passer l'amicale
isAdmin: widget.isAdmin, // Passer isAdmin
onSubmit: null, // Pas besoin de callback
),
],

View File

@@ -0,0 +1 @@
/home/pierre/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.3+2/

View File

@@ -0,0 +1 @@
/home/pierre/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+2/

View File

@@ -6,9 +6,13 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
url_launcher_linux
)

View File

@@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation
import connectivity_plus
import file_selector_macos
import flutter_local_notifications
import geolocator_apple
import package_info_plus
@@ -15,6 +16,7 @@ import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))

View File

@@ -3,8 +3,8 @@ FLUTTER_ROOT=/home/pierre/dev/flutter
FLUTTER_APPLICATION_PATH=/home/pierre/dev/geosector/app
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=3.0.4
FLUTTER_BUILD_NUMBER=304
FLUTTER_BUILD_NAME=3.0.6
FLUTTER_BUILD_NUMBER=306
DART_OBFUSCATION=false
TRACK_WIDGET_CREATION=true
TREE_SHAKE_ICONS=false

View File

@@ -4,8 +4,8 @@ export "FLUTTER_ROOT=/home/pierre/dev/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.0.4"
export "FLUTTER_BUILD_NUMBER=304"
export "FLUTTER_BUILD_NAME=3.0.6"
export "FLUTTER_BUILD_NUMBER=306"
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false"

View File

@@ -114,10 +114,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: "0b1b12a0a549605e5f04476031cd0bc91ead1d7c8e830773a18ee54179b3cb62"
sha256: ba95c961bafcd8686d1cf63be864eb59447e795e124d98d6a27d91fcd13602fb
url: "https://pub.dev"
source: hosted
version: "8.11.0"
version: "8.11.1"
characters:
dependency: transitive
description:
@@ -198,6 +198,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
crypto:
dependency: transitive
description:
@@ -258,10 +266,10 @@ packages:
dependency: "direct main"
description:
name: dio
sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9"
sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9
url: "https://pub.dev"
source: hosted
version: "5.8.0+1"
version: "5.9.0"
dio_cache_interceptor:
dependency: transitive
description:
@@ -318,6 +326,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
url: "https://pub.dev"
source: hosted
version: "0.9.3+2"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711"
url: "https://pub.dev"
source: hosted
version: "0.9.4+3"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.dev"
source: hosted
version: "2.6.2"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
url: "https://pub.dev"
source: hosted
version: "0.9.3+4"
fixnum:
dependency: transitive
description:
@@ -387,6 +427,11 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_map:
dependency: "direct main"
description:
@@ -403,6 +448,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0+1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab"
url: "https://pub.dev"
source: hosted
version: "2.0.29"
flutter_svg:
dependency: "direct main"
description:
@@ -569,10 +622,10 @@ packages:
dependency: transitive
description:
name: http
sha256: "85ab0074f9bf2b24625906d8382bbec84d3d6919d285ba9c106b07b65791fb99"
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
url: "https://pub.dev"
source: hosted
version: "1.5.0-beta.2"
version: "1.5.0"
http_cache_core:
dependency: transitive
description:
@@ -613,6 +666,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.5.4"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: b08e9a04d0f8d91f4a6e767a745b9871bfbc585410205c311d0492de20a7ccd6
url: "https://pub.dev"
source: hosted
version: "0.8.12+25"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100"
url: "https://pub.dev"
source: hosted
version: "0.8.12+2"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9"
url: "https://pub.dev"
source: hosted
version: "0.2.1+2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1"
url: "https://pub.dev"
source: hosted
version: "0.2.1+2"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0"
url: "https://pub.dev"
source: hosted
version: "2.10.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
intl:
dependency: "direct main"
description:
@@ -945,10 +1062,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"
url: "https://pub.dev"
source: hosted
version: "2.4.10"
version: "2.4.11"
shared_preferences_foundation:
dependency: transitive
description:
@@ -1078,18 +1195,18 @@ packages:
dependency: "direct main"
description:
name: syncfusion_flutter_charts
sha256: "55bc6210265483eba4ea168931e4edd3df24510d1ea99c177df46d69ab0deb6f"
sha256: a2263d6221b17d49ea9c23709df8796abde4cd2d2664960d48fac62b9d8c3eb8
url: "https://pub.dev"
source: hosted
version: "30.1.42"
version: "30.2.4"
syncfusion_flutter_core:
dependency: transitive
description:
name: syncfusion_flutter_core
sha256: f999f11af9c54bd46ceb478273e06774d28723ffa2c622d2d6e7e84a358c23e0
sha256: "9d6b7722331e8c84e837d852da03f3735bf436fe6ebf2acc3455a2a21a0d47ff"
url: "https://pub.dev"
source: hosted
version: "30.1.42"
version: "30.2.4"
synchronized:
dependency: transitive
description:
@@ -1174,10 +1291,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
sha256: "0aedad096a85b49df2e4725fa32118f9fa580f3b14af7a2d2221896a02cd5656"
url: "https://pub.dev"
source: hosted
version: "6.3.16"
version: "6.3.17"
url_launcher_ios:
dependency: transitive
description:

View File

@@ -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.0.4+304
version: 3.0.6+306
environment:
sdk: '>=3.0.0 <4.0.0'
@@ -9,6 +9,8 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
cupertino_icons: ^1.0.6
# Navigation
@@ -49,6 +51,9 @@ dependencies:
mqtt5_client: ^4.11.0
flutter_local_notifications: ^19.0.1
# Upload d'images
image_picker: ^1.1.2
dev_dependencies:
flutter_test:
sdk: flutter

View File

@@ -0,0 +1 @@
/home/pierre/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+4/

View File

@@ -0,0 +1 @@
/home/pierre/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/

View File

@@ -7,12 +7,15 @@
#include "generated_plugin_registrant.h"
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <geolocator_windows/geolocator_windows.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
GeolocatorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("GeolocatorWindows"));
UrlLauncherWindowsRegisterWithRegistrar(

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
connectivity_plus
file_selector_windows
geolocator_windows
url_launcher_windows
)