diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..c3f18a6 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,55 @@ +{ + "project": { + "name": "P'titsPas", + "description": "Application de gestion de la garde d'enfants pour les collectivités locales" + }, + "conventions": { + "language": "fr", + "naming": { + "package": "p_tits_pas", + "classes": "PascalCase", + "variables": "camelCase", + "constants": "UPPER_CASE" + } + }, + "formatting": { + "indentation": 2, + "max_line_length": 80 + }, + "documents": { + "cahier_des_charges": "docs/SuperNounou_Cahier_Des_Charges_Complet_V1.1.md", + "evolutions": "docs/EVOLUTIONS_CDC.md", + "charte_graphique": "docs/CHARTE_GRAPHIQUE.md", + "specifications_techniques": "docs/SuperNounou_SSS-001.md" + }, + "launch_commands": { + "backend": { + "start": "cd backend && npm run dev", + "description": "Démarre le serveur backend sur le port 3000" + }, + "frontend": { + "start": "cd frontend && flutter run -d chrome", + "description": "Démarre l'application Flutter dans Chrome" + }, + "full": { + "start": "cd backend && npm run dev & cd frontend && flutter run -d chrome", + "description": "Démarre le backend et le frontend en parallèle" + } + }, + "rules": [ + "Toujours répondre en français", + "Utiliser le nom 'P'titsPas' dans l'interface utilisateur et la documentation", + "Utiliser 'p_tits_pas' pour les noms techniques (packages, fichiers, etc.)", + "Respecter les conventions de nommage Flutter/Dart", + "Maintenir une cohérence dans le style de code", + "Utiliser le camelCase pour les noms de variables, fonctions et méthodes", + "Le camelCase doit commencer par une minuscule (ex: maVariable, maFonction)", + "Toujours se référer à la documentation officielle en cas de doute", + "En cas d'incertitude, poser des questions pour clarifier les besoins", + "Si les instructions diffèrent des conventions établies, proposer une évolution à écrire dans le document d'évolution", + "Se référer au cahier des charges pour les spécifications fonctionnelles", + "Suivre la charte graphique pour tous les éléments visuels", + "Consulter les spécifications techniques pour les aspects techniques", + "Documenter les évolutions dans le fichier EVOLUTIONS_CDC.md" + ] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09057bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# Dependencies +node_modules/ +.pub-cache/ +.dart_tool/ +.packages +build/ +dist/ + +# Environment variables +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Database +*.sqlite +*.sqlite3 + +# Flutter +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Coverage +coverage/ +*.lcov + +# Temporary files +*.tmp +*.temp +.cache/ +Archives/** +Xcf/** + +# Release notes +CHANGELOG.md +Ressources/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a78759 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# P'titsPas + +Plateforme de gestion de la garde d'enfants pour les collectivités locales. + +## Workflow de développement + +Le projet suit un workflow simple : +- `develop` : branche principale de développement +- `main` : branche des versions stables + +### Processus de release +1. Développement sur la branche `develop` +2. Tests et validation +3. Merge vers `main` avec tag de version +4. Mise à jour du CHANGELOG.md + +## Charte graphique + +- **Nom** : P'titsPas +- **Couleurs principales** : + - Bleu : #2B6CB0 (inspiré du logo) + - Blanc : #FFFFFF + - Gris clair : #F7FAFC +- **Typographie** : + - Titres : Google Fonts "Comfortaa" + - Corps : Google Fonts "Roboto" + +## Prérequis + +### Backend +- Node.js (version 18 ou supérieure) +- PostgreSQL (version 15 ou supérieure) +- npm (version 9 ou supérieure) + +### Frontend +- Flutter SDK (version 3.0 ou supérieure) +- Dart SDK (version 3.0 ou supérieure) +- Chrome (pour le développement web) + +## Installation + +1. Cloner le dépôt : +```bash +git clone [URL_DU_REPO] +cd ptitspas +git checkout develop +``` + +2. Installer le backend : +```bash +cd backend +npm install +``` + +3. Installer le frontend : +```bash +cd ../frontend +flutter pub get +``` + +4. Configurer la base de données : +```bash +# Créer la base de données +createdb ptitspas + +# Configurer les variables d'environnement +cp .env.example .env +# Éditer le fichier .env avec vos paramètres de base de données +``` + +## Démarrage + +1. Démarrer le backend : +```bash +cd backend +npm run dev +``` + +2. Démarrer le frontend : +```bash +cd frontend +flutter run -d chrome +``` + +## Accès aux services + +- Frontend : http://localhost:8080 +- Backend API : http://localhost:3000 +- Base de données PostgreSQL : localhost:5432 + +## Structure du projet + +``` +ptitspas/ +├── backend/ # API Node.js/Express +│ ├── src/ # Code source +│ ├── prisma/ # Configuration de la base de données +│ └── tests/ # Tests +├── frontend/ # Application Flutter +│ ├── lib/ # Code source +│ ├── assets/ # Images, polices, etc. +│ └── test/ # Tests +├── CHANGELOG.md # Historique des versions +└── README.md # Documentation +``` + +## Développement + +### Backend +- Langage : TypeScript +- Framework : Express +- Base de données : PostgreSQL avec Prisma +- API : REST avec OpenAPI 3 + +### Frontend +- Framework : Flutter +- État : Provider +- Navigation : Go Router +- UI : Material Design avec thème personnalisé + +## Tests + +```bash +# Backend +cd backend +npm test + +# Frontend +cd frontend +flutter test +``` + +## Licence + +Propriétaire - Tous droits réservés \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..17b230d --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,3320 @@ +{ + "name": "petitspas-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "petitspas-backend", + "version": "1.0.0", + "dependencies": { + "@nestjs/common": "^11.1.0", + "@prisma/client": "^6.7.0", + "@types/jsonwebtoken": "^9.0.9", + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "express": "^4.18.2", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/helmet": "^4.0.0", + "@types/morgan": "^1.9.9", + "@types/node": "^20.11.19", + "prisma": "^6.7.0", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.3.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nestjs/common": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.0.tgz", + "integrity": "sha512-8MrajltjtIN6eW9cTpv+1IZogqz2Zsrc8YDt0LwQPUq8cSq0j50DETdQpPsNMeib+p9avkV41+NrzGk1z2o5Wg==", + "license": "MIT", + "dependencies": { + "file-type": "20.4.1", + "iterare": "1.2.1", + "load-esm": "1.0.2", + "tslib": "2.8.1", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@prisma/client": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.7.0.tgz", + "integrity": "sha512-+k61zZn1XHjbZul8q6TdQLpuI/cvyfil87zqK2zpreNIXyXtpUv3+H/oM69hcsFcZXaokHJIzPAt5Z8C8eK2QA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.7.0.tgz", + "integrity": "sha512-di8QDdvSz7DLUi3OOcCHSwxRNeW7jtGRUD2+Z3SdNE3A+pPiNT8WgUJoUyOwJmUr5t+JA2W15P78C/N+8RXrOA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "esbuild": ">=0.12 <1", + "esbuild-register": "3.6.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.7.0.tgz", + "integrity": "sha512-RabHn9emKoYFsv99RLxvfG2GHzWk2ZI1BuVzqYtmMSIcuGboHY5uFt3Q3boOREM9de6z5s3bQoyKeWnq8Fz22w==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.7.0.tgz", + "integrity": "sha512-3wDMesnOxPrOsq++e5oKV9LmIiEazFTRFZrlULDQ8fxdub5w4NgRBoxtWbvXmj2nJVCnzuz6eFix3OhIqsZ1jw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.7.0", + "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", + "@prisma/fetch-engine": "6.7.0", + "@prisma/get-platform": "6.7.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed.tgz", + "integrity": "sha512-EvpOFEWf1KkJpDsBCrih0kg3HdHuaCnXmMn7XFPObpFTzagK1N0Q0FMnYPsEhvARfANP5Ok11QyoTIRA2hgJTA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.7.0.tgz", + "integrity": "sha512-zLlAGnrkmioPKJR4Yf7NfW3hftcvqeNNEHleMZK9yX7RZSkhmxacAYyfGsCcqRt47jiZ7RKdgE0Wh2fWnm7WsQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.7.0", + "@prisma/engines-version": "6.7.0-36.3cff47a7f5d65c3ea74883f1d736e41d68ce91ed", + "@prisma/get-platform": "6.7.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.7.0.tgz", + "integrity": "sha512-i9IH5lO4fQwnMLvQLYNdgVh9TK3PuWBfQd7QLk/YurnAIg+VeADcZDbmhAi4XBBDD+hDif9hrKyASu0hbjwabw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.7.0" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", + "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fflate": "^0.8.2", + "token-types": "^6.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/inflate/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@tokenizer/inflate/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/helmet": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/helmet/-/helmet-4.0.0.tgz", + "integrity": "sha512-ONIn/nSNQA57yRge3oaMQESef/6QhoeX7llWeDli0UZIfz8TQMkfNPTXA8VnnyeA1WUjG2pGqdjEIueYonMdfQ==", + "deprecated": "This is a stub types definition. helmet provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "helmet": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/morgan": { + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", + "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.17.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.32.tgz", + "integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", + "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.3", + "@esbuild/android-arm": "0.25.3", + "@esbuild/android-arm64": "0.25.3", + "@esbuild/android-x64": "0.25.3", + "@esbuild/darwin-arm64": "0.25.3", + "@esbuild/darwin-x64": "0.25.3", + "@esbuild/freebsd-arm64": "0.25.3", + "@esbuild/freebsd-x64": "0.25.3", + "@esbuild/linux-arm": "0.25.3", + "@esbuild/linux-arm64": "0.25.3", + "@esbuild/linux-ia32": "0.25.3", + "@esbuild/linux-loong64": "0.25.3", + "@esbuild/linux-mips64el": "0.25.3", + "@esbuild/linux-ppc64": "0.25.3", + "@esbuild/linux-riscv64": "0.25.3", + "@esbuild/linux-s390x": "0.25.3", + "@esbuild/linux-x64": "0.25.3", + "@esbuild/netbsd-arm64": "0.25.3", + "@esbuild/netbsd-x64": "0.25.3", + "@esbuild/openbsd-arm64": "0.25.3", + "@esbuild/openbsd-x64": "0.25.3", + "@esbuild/sunos-x64": "0.25.3", + "@esbuild/win32-arm64": "0.25.3", + "@esbuild/win32-ia32": "0.25.3", + "@esbuild/win32-x64": "0.25.3" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/esbuild-register/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/esbuild-register/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-20.4.1.tgz", + "integrity": "sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==", + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.2.6", + "strtok3": "^10.2.0", + "token-types": "^6.0.0", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/load-esm": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/load-esm/-/load-esm-1.0.2.tgz", + "integrity": "sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + }, + { + "type": "buymeacoffee", + "url": "https://buymeacoffee.com/borewit" + } + ], + "license": "MIT", + "engines": { + "node": ">=13.2.0" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/peek-readable": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-7.0.0.tgz", + "integrity": "sha512-nri2TO5JE3/mRryik9LlHFT53cgHfRK0Lt0BAZQXku/AW3E6XLt2GaY8siWi7dvW/m1z0ecn+J+bpDa9ZN3IsQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prisma": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.7.0.tgz", + "integrity": "sha512-vArg+4UqnQ13CVhc2WUosemwh6hr6cr6FY2uzDvCIFwH8pu8BXVv38PktoMLVjtX7sbYThxbnZF5YiR8sN2clw==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.7.0", + "@prisma/engines": "6.7.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strtok3": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.2.2.tgz", + "integrity": "sha512-Xt18+h4s7Z8xyZ0tmBoRmzxcop97R4BAh+dXouUDCYn+Em+1P3qpkUfI5ueWLT8ynC5hZ+q4iPEmGG1urvQGBg==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "peek-readable": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/token-types": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.0.tgz", + "integrity": "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA==", + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "license": "MIT", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uint8array-extras": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.4.0.tgz", + "integrity": "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..3730147 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,36 @@ +{ + "name": "petitspas-backend", + "version": "1.0.0", + "description": "Backend pour l'application P'titsPas", + "main": "dist/index.js", + "scripts": { + "start": "node dist/index.js", + "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "build": "tsc", + "test": "jest", + "init-admin": "ts-node src/scripts/initAdmin.ts" + }, + "dependencies": { + "@nestjs/common": "^11.1.0", + "@prisma/client": "^6.7.0", + "@types/jsonwebtoken": "^9.0.9", + "bcrypt": "^5.1.1", + "cors": "^2.8.5", + "express": "^4.18.2", + "helmet": "^7.1.0", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/helmet": "^4.0.0", + "@types/morgan": "^1.9.9", + "@types/node": "^20.11.19", + "prisma": "^6.7.0", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.3.3" + } +} diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma new file mode 100644 index 0000000..c462f65 --- /dev/null +++ b/backend/prisma/schema.prisma @@ -0,0 +1,108 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// Modèle pour les parents +model Parent { + id String @id @default(uuid()) + email String @unique + password String + firstName String + lastName String + phoneNumber String? + address String? + status AccountStatus @default(PENDING) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + children Child[] + contracts Contract[] +} + +// Modèle pour les enfants +model Child { + id String @id @default(uuid()) + firstName String + dateOfBirth DateTime + photoUrl String? + photoConsent Boolean @default(false) + isMultiple Boolean @default(false) + isUnborn Boolean @default(false) + parentId String + parent Parent @relation(fields: [parentId], references: [id]) + contracts Contract[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Modèle pour les contrats +model Contract { + id String @id @default(uuid()) + parentId String + childId String + startDate DateTime + endDate DateTime? + status ContractStatus @default(ACTIVE) + parent Parent @relation(fields: [parentId], references: [id]) + child Child @relation(fields: [childId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Modèle pour les thèmes +model Theme { + id String @id @default(uuid()) + name String @unique + primaryColor String + secondaryColor String + backgroundColor String + textColor String + isActive Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + appSettings AppSettings[] +} + +// Modèle pour les paramètres de l'application +model AppSettings { + id String @id @default(uuid()) + currentThemeId String + currentTheme Theme @relation(fields: [currentThemeId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([currentThemeId]) +} + +// Modèle pour les administrateurs +model Admin { + id String @id @default(uuid()) + email String @unique + password String + firstName String + lastName String + passwordChanged Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +// Enums +enum AccountStatus { + PENDING + VALIDATED + REJECTED + SUSPENDED +} + +enum ContractStatus { + ACTIVE + ENDED + CANCELLED +} \ No newline at end of file diff --git a/backend/src/admin/admin.controller.ts b/backend/src/admin/admin.controller.ts new file mode 100644 index 0000000..52a81db --- /dev/null +++ b/backend/src/admin/admin.controller.ts @@ -0,0 +1,18 @@ +import { Controller, Post, Body, UseGuards, Req } from '@nestjs/common'; +import { AdminService } from './admin.service'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; + +@Controller('admin') +export class AdminController { + constructor(private readonly adminService: AdminService) {} + + @Post('change-password') + @UseGuards(JwtAuthGuard) + async changePassword( + @Req() req, + @Body('oldPassword') oldPassword: string, + @Body('newPassword') newPassword: string, + ) { + return this.adminService.changePassword(req.user.id, oldPassword, newPassword); + } +} \ No newline at end of file diff --git a/backend/src/admin/admin.module.ts b/backend/src/admin/admin.module.ts new file mode 100644 index 0000000..9d47b62 --- /dev/null +++ b/backend/src/admin/admin.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { AdminController } from './admin.controller'; +import { AdminService } from './admin.service'; +import { PrismaModule } from '../prisma/prisma.module'; +import { JwtModule } from '@nestjs/jwt'; + +@Module({ + imports: [ + PrismaModule, + JwtModule.register({ + secret: process.env.JWT_SECRET, + signOptions: { expiresIn: '1d' }, + }), + ], + controllers: [AdminController], + providers: [AdminService], +}) +export class AdminModule {} \ No newline at end of file diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts new file mode 100644 index 0000000..c671645 --- /dev/null +++ b/backend/src/admin/admin.service.ts @@ -0,0 +1,40 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { PrismaService } from '../prisma/prisma.service'; +import * as bcrypt from 'bcrypt'; + +@Injectable() +export class AdminService { + constructor( + private prisma: PrismaService, + private jwtService: JwtService, + ) {} + + async changePassword(adminId: string, oldPassword: string, newPassword: string) { + // Récupérer l'administrateur + const admin = await this.prisma.admin.findUnique({ + where: { id: adminId }, + }); + + if (!admin) { + throw new UnauthorizedException('Administrateur non trouvé'); + } + + // Vérifier l'ancien mot de passe + const isPasswordValid = await bcrypt.compare(oldPassword, admin.password); + if (!isPasswordValid) { + throw new UnauthorizedException('Ancien mot de passe incorrect'); + } + + // Hasher le nouveau mot de passe + const hashedPassword = await bcrypt.hash(newPassword, 10); + + // Mettre à jour le mot de passe + await this.prisma.admin.update({ + where: { id: adminId }, + data: { password: hashedPassword }, + }); + + return { message: 'Mot de passe modifié avec succès' }; + } +} \ No newline at end of file diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts new file mode 100644 index 0000000..81069a9 --- /dev/null +++ b/backend/src/app.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { PrismaModule } from './prisma/prisma.module'; +import { AuthModule } from './auth/auth.module'; +import { AdminModule } from './admin/admin.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + PrismaModule, + AuthModule, + AdminModule, + ], +}) +export class AppModule {} \ No newline at end of file diff --git a/backend/src/controllers/theme.controller.ts b/backend/src/controllers/theme.controller.ts new file mode 100644 index 0000000..30c1108 --- /dev/null +++ b/backend/src/controllers/theme.controller.ts @@ -0,0 +1,72 @@ +import { Request, Response } from 'express'; +import { ThemeService, ThemeData } from '../services/theme.service'; + +export class ThemeController { + // Créer un nouveau thème + static async createTheme(req: Request, res: Response) { + try { + const themeData: ThemeData = req.body; + const theme = await ThemeService.createTheme(themeData); + res.status(201).json(theme); + } catch (error) { + res.status(500).json({ error: 'Erreur lors de la création du thème' }); + } + } + + // Récupérer tous les thèmes + static async getAllThemes(req: Request, res: Response) { + try { + const themes = await ThemeService.getAllThemes(); + res.json(themes); + } catch (error) { + res.status(500).json({ error: 'Erreur lors de la récupération des thèmes' }); + } + } + + // Récupérer le thème actif + static async getActiveTheme(req: Request, res: Response) { + try { + const theme = await ThemeService.getActiveTheme(); + if (!theme) { + return res.status(404).json({ error: 'Aucun thème actif trouvé' }); + } + res.json(theme); + } catch (error) { + res.status(500).json({ error: 'Erreur lors de la récupération du thème actif' }); + } + } + + // Activer un thème + static async activateTheme(req: Request, res: Response) { + try { + const { themeId } = req.params; + const theme = await ThemeService.activateTheme(themeId); + res.json(theme); + } catch (error) { + res.status(500).json({ error: 'Erreur lors de l\'activation du thème' }); + } + } + + // Mettre à jour un thème + static async updateTheme(req: Request, res: Response) { + try { + const { themeId } = req.params; + const themeData: Partial = req.body; + const theme = await ThemeService.updateTheme(themeId, themeData); + res.json(theme); + } catch (error) { + res.status(500).json({ error: 'Erreur lors de la mise à jour du thème' }); + } + } + + // Supprimer un thème + static async deleteTheme(req: Request, res: Response) { + try { + const { themeId } = req.params; + await ThemeService.deleteTheme(themeId); + res.status(204).send(); + } catch (error) { + res.status(500).json({ error: 'Erreur lors de la suppression du thème' }); + } + } +} \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..490853f --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,28 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import morgan from 'morgan'; +import themeRoutes from './routes/theme.routes'; + +const app = express(); +const port = process.env.PORT || 3000; + +// Middleware +app.use(cors()); +app.use(helmet()); +app.use(morgan('dev')); +app.use(express.json()); + +// Routes +app.use('/api/themes', themeRoutes); + +// Gestion des erreurs +app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + console.error(err.stack); + res.status(500).json({ error: 'Une erreur est survenue' }); +}); + +// Démarrage du serveur +app.listen(port, () => { + console.log(`Serveur démarré sur le port ${port}`); +}); \ No newline at end of file diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts new file mode 100644 index 0000000..31cef32 --- /dev/null +++ b/backend/src/routes/auth.ts @@ -0,0 +1,95 @@ +import { Router } from 'express'; +import { PrismaClient } from '@prisma/client'; +import * as bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; + +const router = Router(); +const prisma = new PrismaClient(); +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; + +// Route de connexion +router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + + // Vérifier les identifiants + const admin = await prisma.admin.findUnique({ + where: { email } + }); + + if (!admin) { + return res.status(401).json({ error: 'Identifiants invalides' }); + } + + // Vérifier le mot de passe + const validPassword = await bcrypt.compare(password, admin.password); + if (!validPassword) { + return res.status(401).json({ error: 'Identifiants invalides' }); + } + + // Vérifier si le mot de passe doit être changé + if (!admin.passwordChanged) { + return res.status(403).json({ + error: 'Changement de mot de passe requis', + requiresPasswordChange: true + }); + } + + // Générer le token JWT + const token = jwt.sign( + { + id: admin.id, + email: admin.email, + role: 'admin' + }, + JWT_SECRET, + { expiresIn: '24h' } + ); + + res.json({ token }); + } catch (error) { + console.error('Erreur lors de la connexion:', error); + res.status(500).json({ error: 'Erreur serveur' }); + } +}); + +// Route de changement de mot de passe +router.post('/change-password', async (req, res) => { + try { + const { email, currentPassword, newPassword } = req.body; + + // Vérifier l'administrateur + const admin = await prisma.admin.findUnique({ + where: { email } + }); + + if (!admin) { + return res.status(404).json({ error: 'Administrateur non trouvé' }); + } + + // Vérifier l'ancien mot de passe + const validPassword = await bcrypt.compare(currentPassword, admin.password); + if (!validPassword) { + return res.status(401).json({ error: 'Mot de passe actuel incorrect' }); + } + + // Hasher le nouveau mot de passe + const hashedPassword = await bcrypt.hash(newPassword, 10); + + // Mettre à jour le mot de passe + await prisma.admin.update({ + where: { id: admin.id }, + data: { + password: hashedPassword, + passwordChanged: true + } + }); + + res.json({ message: 'Mot de passe changé avec succès' }); + } catch (error) { + console.error('Erreur lors du changement de mot de passe:', error); + res.status(500).json({ error: 'Erreur serveur' }); + } +}); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/theme.routes.ts b/backend/src/routes/theme.routes.ts new file mode 100644 index 0000000..e43e388 --- /dev/null +++ b/backend/src/routes/theme.routes.ts @@ -0,0 +1,14 @@ +import { Router } from 'express'; +import { ThemeController } from '../controllers/theme.controller'; + +const router = Router(); + +// Routes pour les thèmes +router.post('/', ThemeController.createTheme); +router.get('/', ThemeController.getAllThemes); +router.get('/active', ThemeController.getActiveTheme); +router.put('/:themeId/activate', ThemeController.activateTheme); +router.put('/:themeId', ThemeController.updateTheme); +router.delete('/:themeId', ThemeController.deleteTheme); + +export default router; \ No newline at end of file diff --git a/backend/src/scripts/initAdmin.ts b/backend/src/scripts/initAdmin.ts new file mode 100644 index 0000000..dd59a96 --- /dev/null +++ b/backend/src/scripts/initAdmin.ts @@ -0,0 +1,39 @@ +import { PrismaClient } from '@prisma/client'; +import * as bcrypt from 'bcrypt'; + +const prisma = new PrismaClient(); + +async function main() { + try { + // Vérifier si l'administrateur existe déjà + const existingAdmin = await prisma.admin.findUnique({ + where: { email: 'administrateur@ptitspas.fr' } + }); + + if (!existingAdmin) { + // Hasher le mot de passe + const hashedPassword = await bcrypt.hash('password', 10); + + // Créer l'administrateur + await prisma.admin.create({ + data: { + email: 'administrateur@ptitspas.fr', + password: hashedPassword, + firstName: 'Administrateur', + lastName: 'P\'titsPas', + passwordChanged: false + } + }); + + console.log('✅ Administrateur créé avec succès'); + } else { + console.log('ℹ️ L\'administrateur existe déjà'); + } + } catch (error) { + console.error('❌ Erreur lors de la création de l\'administrateur:', error); + } finally { + await prisma.$disconnect(); + } +} + +main(); \ No newline at end of file diff --git a/backend/src/services/theme.service.ts b/backend/src/services/theme.service.ts new file mode 100644 index 0000000..47a0c79 --- /dev/null +++ b/backend/src/services/theme.service.ts @@ -0,0 +1,77 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +export interface ThemeData { + name: string; + primaryColor: string; + secondaryColor: string; + backgroundColor: string; + textColor: string; +} + +export class ThemeService { + // Créer un nouveau thème + static async createTheme(data: ThemeData) { + return prisma.theme.create({ + data: { + ...data, + isActive: false, + }, + }); + } + + // Récupérer tous les thèmes + static async getAllThemes() { + return prisma.theme.findMany(); + } + + // Récupérer le thème actif + static async getActiveTheme() { + const settings = await prisma.appSettings.findFirst({ + include: { + currentTheme: true, + }, + }); + return settings?.currentTheme; + } + + // Activer un thème + static async activateTheme(themeId: string) { + // Désactiver tous les thèmes + await prisma.theme.updateMany({ + where: { isActive: true }, + data: { isActive: false }, + }); + + // Activer le thème sélectionné + const updatedTheme = await prisma.theme.update({ + where: { id: themeId }, + data: { isActive: true }, + }); + + // Mettre à jour les paramètres de l'application + await prisma.appSettings.upsert({ + where: { id: '1' }, + update: { currentThemeId: themeId }, + create: { id: '1', currentThemeId: themeId }, + }); + + return updatedTheme; + } + + // Mettre à jour un thème + static async updateTheme(themeId: string, data: Partial) { + return prisma.theme.update({ + where: { id: themeId }, + data, + }); + } + + // Supprimer un thème + static async deleteTheme(themeId: string) { + return prisma.theme.delete({ + where: { id: themeId }, + }); + } +} \ No newline at end of file diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..97175de --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "lib": ["es2018", "esnext.asynciterable"], + "skipLibCheck": true, + "sourceMap": true, + "outDir": "./dist", + "moduleResolution": "node", + "removeComments": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "resolveJsonModule": true, + "baseUrl": "." + }, + "exclude": ["node_modules"], + "include": ["./src/**/*.ts"] +} \ No newline at end of file diff --git a/docs/CHARTE_GRAPHIQUE.md b/docs/CHARTE_GRAPHIQUE.md new file mode 100644 index 0000000..623b3b6 --- /dev/null +++ b/docs/CHARTE_GRAPHIQUE.md @@ -0,0 +1,51 @@ +# Charte graphique · **P'titsPas** +*Version 1.0 – avril 2025* + +--- + +## 1. Essentiel de la marque +| Élément | Raison d'être | +|---------|---------------| +| **Nom** | *P'titsPas* · évoque le cheminement serein des 0-3 ans | +| **Signature** | « Grandir pas à pas, sereinement » | +| **Valeurs** | Bienveillance · Transparence · Simplicité · Modernité | + +--- + +## 2. Logos officiels + +| Variante | Aperçu | Usage | +|----------|--------|-------| +| **Principal** | ![logo principal](P'tisPas_logo_trans.png) | Headers, back-office, print A4+ | +| **Icône** | ![icône](P'titsPas_icone.png) | Favicon, PWA, app mobile | +| **Monochrome** | ![mono](mono_placeholder.svg) | Sérigraphie, tampon, textile sombre | + +> **Zone de protection** : laisser au minimum l'équivalent d'une pierre pastel autour du logotype. + +--- + +## 3. Palette de couleurs + +| Nom | Hex | Rôle | +|-----|-----|------| +| Violet Pastel | `#c6a3d8` | Accent / information | +| Turquoise | `#8ad0c8` | Actions primaires | +| Jaune Doux | `#f2d269` | Avertissements légers | +| Corail | `#f4a28c` | États d'erreur ou badges « conflit » | +| Encre | `#2f2f2f` | Texte principal | +| Ivoire BG | `#fffef9` | Fond d'application & documents | + +--- + +## 4. Typographies + +| Contexte | Fonte | Chargement | +|----------|-------|------------| +| Titres & accroches | **Merienda 600** | Google Fonts | +| Texte courant | **Merriweather 300/400** | Google Fonts | +| UI compact | **Inter** (fallback système) | CDN | + +```css +h1, h2 { font-family: "Merienda", cursive; } +body { font-family: "Merriweather", serif; } +``` \ No newline at end of file diff --git a/docs/EVOLUTIONS_CDC.md b/docs/EVOLUTIONS_CDC.md new file mode 100644 index 0000000..8b7a74d --- /dev/null +++ b/docs/EVOLUTIONS_CDC.md @@ -0,0 +1,257 @@ +# Évolutions du Cahier des Charges + +Ce document liste les modifications à apporter au cahier des charges original pour le rendre conforme à l'application développée. + +## 1. Gestion des Enfants + +### Modifications à apporter dans la section "Création de compte parent" + +#### Situation actuelle dans le CDC : +- Mentionne uniquement la collecte d'informations sur l'enfant +- Ne précise pas la possibilité d'ajouter plusieurs enfants +- Ne mentionne pas la gestion des naissances multiples +- Ne mentionne pas la gestion des enfants à naître + +#### Modifications proposées : + +Ajouter le paragraphe suivant après la description de la collecte d'informations sur l'enfant : + +``` +Les parents peuvent ajouter autant d'enfants que nécessaire. Pour chaque enfant, les informations suivantes sont collectées : +- Prénom +- Date de naissance (ou date prévue pour les enfants à naître) +- Photo (optionnelle) +- Consentement pour l'utilisation de la photo +- Indication si l'enfant fait partie d'une naissance multiple (jumeaux, triplés, etc.) + +Les parents peuvent : +- Ajouter un nouvel enfant à tout moment +- Supprimer un enfant ajouté +- Modifier les informations d'un enfant existant +- Indiquer si l'enfant est à naître +- Indiquer si l'enfant fait partie d'une naissance multiple +- Donner ou retirer leur consentement pour l'utilisation de la photo de l'enfant +``` + +### Modifications à apporter dans la section "Workflow de création de compte" + +#### Situation actuelle dans le CDC : +- Étape 3 : "Collecte des informations sur l'enfant" + +#### Modifications proposées : + +Remplacer l'étape 3 par : +``` +3. Collecte des informations sur les enfants + - Ajout d'un premier enfant + - Possibilité d'ajouter d'autres enfants + - Pour chaque enfant : + * Saisie du prénom + * Saisie de la date de naissance (ou date prévue) + * Option d'ajout d'une photo + * Option de consentement photo + * Indication si naissance multiple + * Indication si enfant à naître + - Possibilité de modifier ou supprimer un enfant +``` + +## 2. Workflow de Création de Compte + +### Modifications à apporter dans la section "Workflow de création de compte" + +#### Situation actuelle dans le CDC : +- Ne précise pas l'ordre exact des étapes +- Ne mentionne pas le statut du compte après création +- Ne détaille pas le processus de validation + +#### Modifications proposées : + +Ajouter les précisions suivantes au workflow : + +``` +Le processus de création de compte suit l'ordre suivant : +1. Collecte des informations du premier parent +2. Option d'ajout d'un second parent +3. Collecte des informations sur les enfants +4. Description de la situation familiale +5. Acceptation des conditions générales +6. Résumé et validation finale + +Après la validation : +- Le compte est créé avec le statut "en attente" +- Un gestionnaire doit valider le compte avant son activation +- Les parents reçoivent une notification de la création de leur compte +- Une notification est envoyée aux gestionnaires pour validation +``` + +## 3. Informations Supplémentaires + +### Modifications à apporter dans la section "Création de compte parent" + +#### Situation actuelle dans le CDC : +- Ne mentionne pas la possibilité de présentation personnelle +- Ne mentionne pas la gestion des photos +- Ne précise pas les statuts possibles du compte + +#### Modifications proposées : + +Ajouter les sections suivantes : + +``` +### Informations complémentaires +Le premier parent peut optionnellement ajouter une présentation personnelle pour décrire sa situation et ses attentes. + +### Gestion des photos +Pour chaque enfant, les parents peuvent : +- Ajouter une photo +- Donner ou retirer leur consentement pour l'utilisation de la photo +- La photo est stockée de manière sécurisée +- Le consentement est enregistré avec date et heure + +### Statut du compte +Les statuts possibles du compte sont : +- En attente : compte créé, en attente de validation +- Validé : compte activé par un gestionnaire +- Rejeté : compte refusé par un gestionnaire +- Suspendu : compte temporairement désactivé +``` + +## 4. Validation et Sécurité + +### Modifications à apporter dans la section "Validation" + +#### Situation actuelle dans le CDC : +- Mentionne la validation par un gestionnaire +- Ne précise pas le processus de validation +- Ne mentionne pas les notifications + +#### Modifications proposées : + +Ajouter la section suivante : + +``` +### Processus de validation +1. Création du compte avec statut "en attente" +2. Notification automatique aux gestionnaires +3. Revue des informations par un gestionnaire +4. Décision de validation ou rejet +5. Notification aux parents de la décision +6. Activation ou rejet du compte selon la décision + +### Notifications +- Les parents reçoivent une notification à chaque changement de statut +- Les gestionnaires reçoivent une notification pour chaque nouveau compte +- Un historique des validations est conservé +``` + +## 5. Initialisation de l'Application + +### Ajout de l'administrateur par défaut + +#### Situation actuelle dans le CDC : +- Ne mentionne pas l'existence d'un administrateur par défaut +- Ne précise pas les identifiants de connexion par défaut + +#### Modifications proposées : + +Ajouter la section suivante : + +``` +### Administrateur par défaut +Lors du premier démarrage de l'application, un compte administrateur est automatiquement créé avec les identifiants suivants : +- Email : administrateur@ptitspas.fr +- Mot de passe : password + +Ce compte permet d'accéder à toutes les fonctionnalités administratives de l'application. +Le changement de mot de passe est obligatoire lors de la première connexion. +L'application doit forcer ce changement avant d'autoriser l'accès aux fonctionnalités administratives. +``` + +## 6. Changement de Nom de l'Application + +### Situation actuelle dans le CDC : +- L'application est nommée "SuperNounou" dans tout le document +- Les références à l'application utilisent ce nom + +### Modifications proposées : + +Ajouter la section suivante : + +``` +### Changement de nom +L'application est renommée "P'titsPas" dans toute la documentation et l'interface utilisateur. +Ce changement implique : +- Mise à jour de toutes les références à "SuperNounou" dans le CDC +- Mise à jour des mentions légales +- Mise à jour de la documentation technique +- Mise à jour des interfaces utilisateur +- Mise à jour des messages système et notifications +- Mise à jour des adresses email (ex: support@ptitspas.fr) +``` + +### Impact sur l'application : +- Mise à jour de tous les textes statiques dans le code +- Mise à jour des templates d'email +- Mise à jour des messages de notification +- Mise à jour de la documentation utilisateur +- Mise à jour des mentions légales et CGU + +## Format de présentation + +Pour chaque évolution identifiée, ce document suivra la structure suivante : +1. Section concernée dans le CDC +2. Situation actuelle +3. Modifications proposées +4. Impact sur l'application + +## Prochaines évolutions à documenter + +- [x] Ajouter d'autres évolutions identifiées +- [ ] Mettre à jour le CDC original +- [ ] Valider les modifications avec les parties prenantes + +# Évolutions proposées au cahier des charges + +## 1. Workflow de création de compte + +### 1.1 Récupération de compte + +#### 1.1.1 Fonctionnalités +- Ajout d'un lien "Mot de passe oublié" sur la page de connexion +- Processus de récupération en 3 étapes : + 1. Saisie de l'adresse email + 2. Envoi d'un lien unique de réinitialisation (valide 24h) + 3. Création d'un nouveau mot de passe + +#### 1.1.2 Sécurité +- Le lien de réinitialisation doit être unique et à usage unique +- Le lien expire après 24 heures +- Le nouveau mot de passe doit respecter les mêmes critères que lors de la création de compte +- Notification par email lors de la réinitialisation du mot de passe + +#### 1.1.3 Interface +- Page dédiée pour la saisie de l'email +- Page de confirmation d'envoi du lien +- Formulaire de réinitialisation du mot de passe +- Messages d'erreur clairs en cas de : + - Email non trouvé + - Lien expiré + - Mot de passe non conforme + +## X. Amélioration de la Gestion des Photos Utilisateurs (Proposition) + +### X.1 Recadrage et Redimensionnement des Photos + +#### X.1.1 Fonctionnalités +- **Contexte :** Lors du téléchargement de photos par les utilisateurs (photos de profil, photos d'enfants). +- **Besoin :** Permettre à l'utilisateur de recadrer l'image (notamment en format carré pour les avatars) et potentiellement de la faire pivoter ou de zoomer avant son enregistrement final. +- **Objectif :** Améliorer l'expérience utilisateur, assurer une meilleure qualité et cohérence visuelle des images stockées et affichées dans l'application. + +#### X.1.2 Solution Technique Envisagée (pour discussion) +- L'intégration d'une librairie Flutter tierce dédiée au recadrage d'image (par exemple, `image_cropper` ou `crop_image`) sera nécessaire après la sélection initiale de l'image via `image_picker`. +- La tentative initiale avec `image_cropper` (version 5.0.1) a rencontré des difficultés techniques d'intégration (erreur "Too many positional arguments" persistante avec `AndroidUiSettings`) et a été mise en attente. Une investigation plus approfondie ou l'évaluation d'alternatives sera requise. + +#### X.1.3 Impact sur l'application +- Modification du flux de sélection d'image dans les écrans concernés (ex: `parent_register_step3_screen.dart`). +- Ajout potentiel de nouvelles dépendances et configurations spécifiques aux plateformes. +- Mise à jour de la documentation utilisateur si cette fonctionnalité est implémentée. \ No newline at end of file diff --git a/docs/SuperNounou_Cahier_Des_Charges_Complet_V1.1.md b/docs/SuperNounou_Cahier_Des_Charges_Complet_V1.1.md new file mode 100644 index 0000000..0b42440 --- /dev/null +++ b/docs/SuperNounou_Cahier_Des_Charges_Complet_V1.1.md @@ -0,0 +1,1212 @@ +title: "SuperNounou - Cahier des Charges Fonctionnel" +--- +author: "Julien MARTIN" +date: "Avril 2025" +version: "v1.1" +--- + +# SuperNounou – Cahier des Charges Fonctionnel + +> **Objet :** Définir le périmètre fonctionnel, les rôles utilisateurs, les processus métiers et les exigences techniques de la plateforme SuperNounou, destinée à accompagner les collectivités locales dans la gestion de la garde d’enfants. + +--- + +## Historique des versions + +| Version | Date | Auteur | Commentaires | +|---------|------------|----------------|--------------------------------------| +| 1.0 | 15/03/2025 | Julien MARTIN | Version initiale | +| 1.1 | 24/04/2025 | Julien MARTIN | Ajouts : gestion multi-enfants, fin de contrat, tableau de bord étendu | + +--- + + +# Table des matières – SuperNounou + +## 1. Introduction + +## 2. Typologie des utilisateurs +### 2.1 Rôles +#### 2.1.1 Parents +#### 2.1.2 Assistantes maternelles +#### 2.1.3 Gestionnaires +#### 2.1.4 Administrateurs +### 2.2 Périmètres et responsabilités + +## 3. Création de compte et fiches utilisateur +### 3.1 Création de compte parent +### 3.2 Création de compte assistante maternelle +### 3.3 Création d’un gestionnaire +### 3.4 Création d’un administrateur +### 3.5 Fiche enfant +### 3.6 Authentification et sécurité + +## 4. Tableaux de bord +### 4.1 Vue d’ensemble +### 4.2 Tableau de bord des parents +#### 4.2.1 En-tête +#### 4.2.2 Zone enfants +#### 4.2.3 Événements à venir +#### 4.2.4 Contrats +#### 4.2.5 Messagerie +#### 4.2.6 Notifications +### 4.3 Tableau de bord des assistantes maternelles +#### 4.3.1 En-tête +#### 4.3.2 Dossiers reçus +#### 4.3.3 Enfants accueillis +#### 4.3.4 Agenda +#### 4.3.5 Heures supplémentaires +#### 4.3.6 Messagerie +### 4.4 Tableau de bord des gestionnaires +#### 4.4.1 Comptes à valider +#### 4.4.2 Liste des utilisateurs +#### 4.4.3 Contrats +#### 4.4.4 Messagerie +#### 4.4.5 Événements RPE +#### 4.4.6 Alertes +### 4.5 Tableau de bord des administrateurs +#### 4.5.1 Menu Profil +#### 4.5.2 Gestion des utilisateurs +#### 4.5.3 Gestion des enfants +#### 4.5.4 Paramètres de la plateforme +#### 4.5.5 Statistiques et supervision +#### 4.5.6 Sécurité et conformité +### 4.6 Menus, alertes et raccourcis visuels + +## 5. Workflows +### 5.1 Recherche d’une assistante maternelle +### 5.2 Traitement du dossier +### 5.3 Création du contrat +### 5.4 Avenants +### 5.5 Fin de contrat + +## 6. Outils partagés +### 6.1 Messagerie +### 6.2 Agenda +### 6.3 Fiche enfant +### 6.4 Contrats et avenants + +## 7. Suivi administratif et financier +### 7.1 Fiche de paie +### 7.2 Aide au remplissage Pajemploi +### 7.3 Heures supplémentaires +### 7.4 Historique +### 7.5 Solde de tout compte + +## 8. Administration technique et conformité +### 8.1 Sécurité et accès +### 8.2 Gouvernance multi-admins +### 8.3 Conformité RGPD +### 8.4 Intégration API +### 8.5 Architecture technique + +## 9. Améliorations futures possibles +### 9.1 Modules activables +#### 9.1.1 Blog RPE +#### 9.1.2 IA de reformulation +#### 9.1.3 Connexion Pajemploi +#### 9.1.4 Garde d’enfants “grands” +#### 9.1.5 Pré-remplissage dossiers scolaires +#### 9.1.6 Dossier fratrie unique +### 9.2 Stratégie d’activation + +## 10. Synthèse finale +### 10.1 Objectifs atteints +### 10.2 Couverture fonctionnelle +### 10.3 Valeur ajoutée +### 10.4 Perspectives d’évolution + +# 1. Introduction + +SuperNounou est une plateforme numérique conçue pour accompagner les collectivités dans la gestion des relations entre les parents, les assistantes maternelles et les relais petite enfance (RPE). Elle vise à simplifier, structurer et sécuriser les processus de mise en relation, de contractualisation, de suivi et de communication autour de la garde d’enfants. + +Elle s’inscrit dans une démarche publique fondée sur les valeurs de la République : **égalité**, **neutralité**, **accessibilité** et **transparence**. Contrairement aux plateformes privées, SuperNounou ne fonctionne ni sur la logique de notation, ni de sélection algorithmique opaque. + +## 1.1 Objectifs + +- Faciliter la recherche d’assistantes maternelles par les parents. +- Offrir un cadre clair et sécurisé à la gestion des contrats. +- Fluidifier la communication entre les acteurs (parents, nounous, gestionnaires). +- Permettre aux gestionnaires de suivre l’ensemble des situations. +- Centraliser les démarches dans un outil unique, intuitif et structuré. +- Valoriser le rôle des collectivités locales dans l’accompagnement de la petite enfance. + +## 1.2 Principes fondateurs + +- **Neutralité** : pas de notation, pas de préférence algorithmique. +- **Équité** : chaque parent a les mêmes droits d’accès aux services. +- **Protection des données** : respect strict du RGPD et du droit à l’oubli. +- **Accessibilité** : conçu pour être lisible sur smartphone comme sur ordinateur. + +## 1.3 Usagers concernés + +- **Parents** d’enfants de 0 à 3 ans à la recherche d’un mode de garde. +- **Assistantes maternelles** souhaitant gérer facilement leurs contrats. +- **Responsables de RPE** supervisant les démarches et accompagnant les familles. +- **Administrateurs** municipaux assurant le paramétrage de l’outil. + +## 1.4 Contexte de déploiement + +SuperNounou est destiné à être déployé à l’échelle d’une ou plusieurs communes. Il s’adapte aux réalités locales en permettant une configuration personnalisée (noms des structures, couleurs, logo, etc.). + +Le modèle de gouvernance repose sur une articulation claire entre : + +- Les **parents et assistantes maternelles** (usagers finaux) +- Les **gestionnaires** (opérateurs terrain) +- Les **administrateurs** (services de la mairie ou de la collectivité) + +# 2. Typologie des utilisateurs + +SuperNounou repose sur une structure multi-rôles permettant une répartition claire des responsabilités et des droits d’accès à l’application. Chaque profil a une interface adaptée à ses besoins et à ses fonctions. + +## 2.1 Rôles + +### 2.1.1 Parents + +Les parents sont les usagers finaux de la plateforme. Ils accèdent à un tableau de bord personnel leur permettant de : + +- Rechercher une assistante maternelle +- Créer et suivre des dossiers +- Consulter les informations liées à leurs enfants +- Visualiser et gérer les contrats de garde +- Communiquer avec l'assistante maternelle et le gestionnaire +- Déclarer des absences ou des événements +- Suivre les documents administratifs liés à la garde +- Accéder à des outils d’aide à la paie + +### 2.1.2 Assistantes maternelles + +Les assistantes maternelles peuvent : + +- Recevoir des dossiers de demande de garde +- Accepter ou refuser une proposition +- Fournir leurs disponibilités pour organiser un rendez-vous +- Gérer leurs contrats et leurs avenants +- Suivre les absences des enfants accueillis +- Déclarer des heures supplémentaires +- Participer à des événements organisés par le RPE +- Utiliser la messagerie intégrée + +### 2.1.3 Gestionnaires + +Les gestionnaires (responsables de relais petite enfance) disposent d’un tableau de bord de supervision. Ils peuvent : + +- Valider ou rejeter les demandes de création de compte +- Suivre les mises en relation et les contrats +- Organiser des événements ou des rendez-vous +- Gérer les conflits ou les fins de contrat +- Lancer des sondages ou modérer un blog RPE (si activé) +- Voir les historiques et statistiques liés à leur périmètre + +### 2.1.4 Administrateurs + +Les administrateurs sont les représentants techniques et institutionnels de la collectivité (DSI ou agents désignés). Ils peuvent : + +- Créer ou supprimer des comptes (gestionnaires, parents, assistantes maternelles) +- Personnaliser l’interface (logo, couleurs, nom de la ville) +- Activer ou désactiver des modules complémentaires +- Consulter les statistiques d’usage +- Exporter les données +- Superviser les accès et assurer la conformité RGPD +- Intégrer la plateforme aux SI de la collectivité + +## 2.2 Périmètres et responsabilités + +Chaque rôle dispose d’un périmètre fonctionnel propre, sans empiétement sur celui des autres : + +- Les parents et assistantes maternelles **échangent directement** entre eux via la messagerie, mais ne peuvent modifier les données contractuelles qu’avec accord mutuel. +- Les gestionnaires interviennent en **appui et en arbitrage**, sans être partie prenante dans les contrats. +- Les administrateurs disposent d’un **pouvoir global de paramétrage et de supervision**. + +# 3. Création de compte et fiches utilisateur + +La création de compte dans SuperNounou est structurée par profil, avec des parcours adaptés à chaque rôle. Tous les champs d'identité sont obligatoires, y compris les photos pour les assistantes maternelles et les enfants déjà nés. Le processus d’approbation garantit la sécurité des données et l’implication des acteurs concernés. + +## 3.1 Création de compte parent + +Le parcours de création d’un compte parent s’effectue en plusieurs étapes successives. + +### 3.1.1 Informations parent 1 +- Nom, prénom +- Adresse postale +- Téléphone (obligatoire) +- Adresse e-mail +- Mot de passe +- Photo de profil + +### 3.1.2 Informations parent 2 (facultatif) +- Possibilité d’ajouter un second parent +- Même adresse que le parent 1 (case à cocher ou champs alternatifs) +- Les deux parents sont ensuite liés dans l’application + +### 3.1.3 Informations sur l’enfant +- Prénom (facultatif si enfant à naître) +- Nom (hérité des parents) +- Date de naissance ou **date prévisionnelle de naissance** (si l’enfant n’est pas encore né, un switch modifie le label) +- Photo obligatoire si l’enfant est né +- Rattachement automatique aux deux parents + +### 3.1.4 Présentation du dossier +- Zone de texte libre permettant aux parents de décrire leur situation +- Ce champ peut être rendu obligatoire ou non par l’administrateur + +### 3.1.5 – Acceptation des CGU +- Les utilisateurs doivent cocher la case + « J’ai lu et j’accepte les Conditions Générales d’Utilisation et la Politique de confidentialité ». +- Un lien direct ouvre la version PDF des CGU. +- Le refus bloque la création de compte. + +### 3.1.6 Récapitulatif et validation +- Résumé des données saisies +- Vérification, puis envoi de la demande +- Les comptes parent sont soumis à validation par un gestionnaire avant activation +- Une fois validé, les parents reçoivent un e-mail ou un SMS de confirmation + +## 3.2 Création de compte assistante maternelle + +Ce parcours est divisé en deux panneaux. + +### 3.2.1 Panneau 1 – Informations d’identité +- Nom, prénom +- Adresse postale +- Téléphone +- Adresse e-mail +- Mot de passe et confirmation +- Photo (obligatoire si l’option *Photo obligatoire* est activée) +- Consentement photo : case à cocher confirmant l’accord pour stocker et afficher la photo (RGPD) + +### 3.2.2 Panneau 2 – Informations professionnelles +- Date de naissance +- Ville et pays de naissance +- Numéro de Sécurité sociale (NIR) – obligatoire + - Saisi en clair (non masqué) + - Mention : « Utilisé uniquement pour la génération automatique du contrat » +- Numéro d’agrément +- Nombre d’enfants pouvant être accueillis + +### 3.2.3 Présentation +- Champ libre : message à destination du gestionnaire +- Permet de justifier une demande ou d’ajouter des précisions + +### 3.1.4 – Acceptation des CGU +- Les utilisateurs doivent cocher la case + « J’ai lu et j’accepte les Conditions Générales d’Utilisation et la Politique de confidentialité ». +- Un lien direct ouvre la version PDF des CGU. +- Le refus bloque la création de compte. + +### 3.2.5 Récapitulatif et validation +- Résumé des données saisies +- Vérification, puis envoi de la demande +- Validation par un gestionnaire requise avant activation +- Une fois validé, l'assistante maternelle reçois un e-mail ou un SMS de confirmation + +## 3.3 Création d’un gestionnaire + +- Réalisée par un administrateur +- Champs obligatoires : nom, prénom, adresse e-mail, mot de passe +- Affectation à un ou plusieurs relais petite enfance +- Le mot de passe doit être modifié lors de la première connexion + +## 3.4 Création d’un administrateur + +- Seuls les administrateurs existants peuvent créer de nouveaux comptes administrateurs +- Les droits sont équivalents (possibilité de restreindre par périmètre dans une version multi-mairies) +- Obligation de changer le mot de passe à la première connexion + +## 3.5 Fiche enfant + +Chaque enfant est représenté par une fiche : + +- Prénom (facultatif si enfant à naître) +- Nom +- Date de naissance ou date prévisionnelle +- Photo (obligatoire si l’enfant est né ET si l’option *Photo obligatoire* est activée) +- Consentement photo enregistré : valeur booléenne + horodatage liés à l’accord donné par le parent +- Statut : à naître / actif / scolarisé +- Indication possible : jumeaux, triplés, etc. +- Possibilité de rattacher un enfant à plusieurs parents (garde alternée) + +## 3.6 Authentification et sécurité + +- Tous les comptes utilisent une combinaison adresse e-mail + mot de passe +- Les gestionnaires et administrateurs doivent modifier leur mot de passe à la première connexion +- Des mécanismes de récupération sont disponibles en cas de perte + +Un lien direct vers les **Mentions légales** et la **Politique de confidentialité** est accessible en permanence depuis le pied de page, y compris avant la connexion. + +# 4. Tableaux de bord + +Chaque rôle utilisateur dispose d’un tableau de bord personnalisé, adapté à ses fonctions dans la plateforme. Ces interfaces sont pensées pour être lisibles, fonctionnelles et évolutives. + +## 4.1 Vue d’ensemble + +| Rôle | Accès au tableau de bord | +|------------------------|-------------------------------------------------------------------| +| Parents | Recherche d'assistante maternelle, gestion des enfants, contrat, agenda | +| Assistantes maternelles| Dossiers reçus, enfants accueillis, heures sup, agenda, messagerie| +| Gestionnaires | Validation de comptes, contrats, messagerie, événements RPE | +| Administrateurs | Paramètres globaux, gestion des utilisateurs, statistiques | + +Les tableaux de bord intègrent : +- Une **barre de navigation supérieure** (liens de navigation rapide) +- Une **zone centrale dynamique**, spécifique au rôle +- Une **zone de notifications ou de messagerie** +- Un **menu profil** propre à chaque type d'utilisateur (cf. section 4.6) + +Un système de **bulles d’aide contextuelle** (icône « ? ») doit être présent sur les champs ou boutons complexes ; un clic ouvre un court tooltip explicatif. Aucune base de connaissances complète n’est exigée en V1. + +#### Pied de page commun + +Chaque page de l’application affiche un **pied de page** fixe contenant : + +| Élément | Cible / comportement | +|---------|----------------------| +| **Contact support** | Ouvre une fenêtre de mailto :`support@supernounou.local` (adresse configurable dans l’admin). | +| **Signaler un bug** | Ouvre un formulaire modal minimal :
Champ texte, bouton « Envoyer » ⇒ crée une entrée dans la table `bug_report` (ou envoie un e-mail si aucune table n’est prévue). | +| **Mentions légales** | Lien vers la page statique `/legal` : informations éditeur, hébergeur, responsable du traitement. | +| **Politique de confidentialité** | Lien vers la page statique `/privacy` (le texte RGPD détaillé). | + +Le pied de page doit rester visible sur desktop et mobile ; sur écrans étroits, les liens peuvent être regroupés dans un menu « Informations ». + + +## 4.2 Tableau de bord des parents + +Le tableau de bord parent est conçu pour offrir une vue complète de la situation de la famille, avec des entrées rapides vers toutes les fonctionnalités utiles. + +### 4.2.1 En-tête + +- Logo de la ville +- Nom et prénom du parent connecté +- Accès au menu Profil +- Bouton “Rechercher une assistante maternelle” +- Notification si un contrat est en cours ou un dossier en traitement + +### 4.2.2 Zone enfants + +- Encadré pour chaque enfant : prénom, photo, statut +- Bouton “Ajouter un enfant” +- Accès à la fiche de l’enfant (dossiers, agenda, événements) + +### 4.2.3 Événements à venir + +Les événements sont affichés sous forme de **bulles colorées** : +- Vacances parents +- Absence enfant +- Activité RPE (confirmée ou suggérée) +- Congés assistante maternelle +- Pour un **arrêt maladie**, le parent destinataire dispose du bouton **« Arrêt bien reçu »** ; lorsqu’il le clique, l’événement est marqué **Validé** côté assistante maternelle. + +Chaque événement peut être : +- Confirmé ou refusé (si proposé par l’autre partie) +- Modifié par la partie créatrice +- Ajouté manuellement par le parent + +Un bouton “Accéder à l’agenda” ouvre une vue étendue : +- Liste complète des événements (passés et futurs) +- Calendrier annuel, mensuel ou hebdomadaire +- Ajout d’événements : absence, vacances, note personnelle + +### 4.2.4 Contrats + +- Encadré avec état du dossier (brouillon, validé, en cours, en fin) +- Consultation du contrat signé +- Bouton “Proposer un avenant” +- Accès à l’historique des modifications +- Lien vers l’outil d’aide Pajemploi (calculs et déclaration) + +### 4.2.5 Messagerie + +- Historique des conversations +- Possibilité de filtrer par enfant ou par assistante maternelle +- Les deux parents d’un enfant sont toujours intégrés ensemble dans une conversation +- Possibilité d’ajouter le gestionnaire à une conversation existante +- Icône spéciale si le message a été reformulé par IA (si module activé) + +### 4.2.6 Notifications + +- Liste visuelle avec icônes et codes couleur : + - Nouvel événement ajouté par l’assistante maternelle + - Dossier modifié + - Contrat en attente de validation + - Paiement en attente + - Message non lu + +## 4.3 Tableau de bord des assistantes maternelles + +L’assistante maternelle accède à une interface dédiée à la gestion de ses accueils, de ses disponibilités, et de ses échanges avec les familles et le RPE. + +### 4.3.1 En-tête + +- Nom et prénom +- Photo de profil +- Accès au menu Profil +- Message d’information (nouveau dossier reçu, contrat en cours, etc.) + +### 4.3.2 Dossiers reçus + +- Liste des demandes de garde +- Pour chaque dossier : nom de l’enfant, informations des parents, dates souhaitées +- Message personnalisé des parents affiché +- Bouton “Accepter le dossier” → propose ensuite des disponibilités de rendez-vous +- Historique des décisions prises + +### 4.3.3 Enfants accueillis + +- Liste des enfants avec fiche complète : + - Identité + - Contrat associé + - Agenda de garde + - Alertes importantes (allergies, horaires atypiques) + +### 4.3.4 Agenda + +- Vue des absences validées +- Ajout d’absences programmées (vacances, fermeture exceptionnelle) +- Ajout d’un **arrêt maladie** : + 1. L’assistante maternelle saisit la période et clique **« Arrêt transmis aux parents »** ; + 2. Elle sélectionne le parent à qui elle a remis le justificatif papier (Parent 1 ou Parent 2) ; + 3. L’événement passe au statut **« En attente de confirmation parent »** dans les deux agendas. +- Chaque événement peut contenir un commentaire +- Les absences doivent être validées par les parents + +### 4.3.5 Heures supplémentaires + +- Déclaration des jours concernés +- Saisie du nombre d’heures effectuées +- Commentaire facultatif +- Historique et régularisation automatique dans la fiche de paie + +### 4.3.6 Messagerie + +- Conversations avec les parents +- Conversations de groupe avec le gestionnaire (événements RPE) +- Notification lorsqu’un nouveau message est reçu +- Icône indiquant si le message a été reformulé par IA (si activée) + +--- + +## 4.4 Tableau de bord des gestionnaires + +Le gestionnaire RPE dispose d’une vision transversale sur les utilisateurs, les dossiers et les activités de la structure. Son rôle est d'accompagner, superviser, et arbitrer si nécessaire. + +### 4.4.1 Comptes à valider + +- File d’attente des demandes de création de compte +- Détails de chaque demande : parent, assistante maternelle +- Actions possibles : Valider / Refuser / Demander des précisions + +### 4.4.2 Liste des utilisateurs + +- Filtres par rôle, statut, date d’inscription +- Accès rapide aux informations et historiques +- Possibilité de contacter un utilisateur + +### 4.4.3 Contrats + +- Vue consolidée des contrats en cours, en validation, ou en fin de garde +- Historique des avenants +- Accès au détail d’un contrat en cas de besoin d’arbitrage + +> **Responsabilité du gestionnaire RPE** +> Le gestionnaire agit exclusivement comme **tiers de confiance** : +> - il vérifie la cohérence des informations fournies par les parents et l’assistante maternelle ; +> - il valide la conformité formelle du contrat (mentions légales, dates, taux). +> **Il n’est pas partie prenante au contrat de travail** et n’assume aucune responsabilité patrimoniale ou employeur ; son rôle se limite à un contrôle administratif et à la médiation en cas de désaccord. + +### 4.4.4 Messagerie + +- Possibilité d’entrer dans une conversation parent-assistante pour médiation +- Démarrer une nouvelle conversation avec un parent, une assistante maternelle, ou les deux +- Conversations de groupe avec plusieurs assistantes maternelles (organisation d’événements RPE) + +### 4.4.5 Événements RPE + +- Création et suivi des événements collectifs +- Sélection des assistantes concernées +- Confirmation de participation +- Ajout automatique dans les agendas parentaux si l’enfant est inscrit + +### 4.4.6 Alertes + +- Liste visuelle des événements nécessitant attention : + - Conflits signalés + - Absences non validées + - Fin de contrat en désaccord +- Code couleur selon niveau d’urgence +- Accès direct aux dossiers concernés + +##### Délais et relance – absences en attente +- Une absence saisie par la partie A doit être **validée sous 7 jours** par la partie B. +- Relance automatique : J+5 par notification (parent ou assistante maternelle). +- Si non validée au bout de 14 jours, alerte “Urgence” (rouge) dans le panneau Gestionnaire. +- Les délais sont configurables par l’administrateur (paramètre “ABSENCE_GRACE_PERIOD” en jours). + +## 4.5 Tableau de bord des administrateurs + +Le tableau de bord des administrateurs est dédié à la gestion technique et stratégique de la plateforme pour une collectivité. Il centralise tous les outils de supervision, d’administration des comptes et de paramétrage global. + +### 4.5.1 Menu Profil + +Le menu profil de l’administrateur comprend uniquement : +- Mon compte +- Déconnexion + +Toutes les fonctionnalités de gestion sont accessibles via des onglets distincts dans le tableau de bord. + +### 4.5.2 Gestion des utilisateurs + +L’administration des utilisateurs est organisée par panneaux distincts pour chaque type de profil. + +#### a. Gestion des gestionnaires +- Liste des gestionnaires existants +- Création (nom, prénom, e-mail, mot de passe) +- Attribution à un ou plusieurs RPE +- Réinitialisation du mot de passe +- Suppression du compte + +#### b. Gestion des parents +- Liste complète des parents enregistrés +- Recherche par nom, statut, enfants associés +- Modification des informations +- Suppression d’un compte +- Consultation du statut des dossiers liés + +#### c. Gestion des assistantes maternelles +- Liste des assistantes avec numéro d’agrément +- Modification ou suppression d’un compte +- Filtrage par zone géographique ou capacité + +#### d. Gestion des administrateurs +- Création de nouveaux comptes administrateurs +- Suivi des droits +- Obligation de modification du mot de passe à la première connexion + +### 4.5.3 Gestion des enfants + +Deux accès possibles à la gestion des enfants : + +#### a. Par fiche parent +- Consultation et édition des enfants associés à chaque parent + +#### b. Vue globale “Enfants” +- Liste complète avec : + - Nom, prénom, date de naissance ou prévisionnelle + - Statut (à naître, actif, scolarisé) + - Parents associés + - Contrat en cours (si applicable) +- Possibilité de modifier ou supprimer une fiche enfant + +### 4.5.4 Paramètres de la plateforme + +#### a. Modules activables +- Blog RPE +- IA de reformulation de messages +- Aide Pajemploi +- Dossiers scolaires en fin de garde +- Garde d’enfants “grands” +- Dossier fratrie unique + +#### b. Personnalisation visuelle +- Logo de la commune +- Thème couleurs +- Nom de la structure affichée + +#### c. Règles de consentement et médias +- Photo obligatoire (assistante maternelle & enfant) : *On / Off* + - *On* : le champ Photo devient requis pour l'assistante maternelle et l’enfant ; la case Consentement photo s’affiche. + - *Off* : la photo redevient facultative et le champ Consentement photo disparaît. +- Historique des changements avec horodatage et identifiant administrateur. + +### 4.5.5 Statistiques et supervision + +- Nombre de contrats actifs +- Nombre d’enfants enregistrés (par statut) +- Nombre de dossiers ouverts, validés, clôturés +- Utilisation des modules optionnels +- Export CSV disponibles : + - Activité par RPE + - Données sur les enfants + - Historique des absences + +### 4.5.6 Sécurité et conformité + +- Suivi des connexions et accès sensibles +- Politique de confidentialité consultable +- Gestion des consentements +- Interface de suppression/anonymisation des données +- Historique horodaté des actions d’administration + +## 4.6 Menus, alertes et raccourcis visuels + +Chaque type d’utilisateur dispose d’un menu “Profil” spécifique, d’un système d’alertes et de raccourcis permettant une navigation fluide et intuitive. + +### 4.6.1 Menus Profil par rôle + +#### a. Parents +- Mes informations +- Mon compte +- Déconnexion + +#### b. Assistantes maternelles +- Mon profil +- Mes informations professionnelles + - Permet de consulter et modifier : NIR, numéro d’agrément, capacité d’accueil, etc. + - Le NIR n’est accessible qu’à l’assistante maternelle elle-même ; aucun autre rôle ne le voit. +- Mon compte +- Déconnexion + +#### c. Gestionnaires +- Mon compte +- Déconnexion + +#### d. Administrateurs +- Mon compte +- Déconnexion + +### 4.6.2 Alertes visuelles + +Des alertes apparaissent sous forme de bulles ou encadrés colorés dans le tableau de bord : +- Nouvel événement ajouté par une autre partie +- Contrat en attente de validation +- Conflit signalé +- Absence à approuver +- Dossier refusé ou incomplet + +Les gestionnaires disposent d’un **panneau d’alertes consolidées**, codé par couleur : +- Rouge : urgence ou conflit +- Orange : action requise +- Vert : succès ou validation en attente + +### 4.6.3 Raccourcis fonctionnels + +Selon le rôle : +- **Parents** : bouton “Rechercher une assistante maternelle”, accès rapide à l’enfant principal +- **Assistantes maternelles** : “Déclarer une absence”, “Voir les enfants du jour” +- **Gestionnaires** : “Nouveaux comptes”, “Dossiers à traiter”, “Événements RPE” +- **Administrateurs** : accès direct aux sections de gestion des utilisateurs, enfants, et paramètres + +Les raccourcis sont affichés sous forme d’icônes dans la barre supérieure ou la colonne latérale, selon la taille d’écran. + +# 5. Workflows + +Ce chapitre décrit les différents processus métiers (workflows) au cœur de SuperNounou : de la recherche d’une assistante maternelle à la fin du contrat, en passant par le traitement des dossiers, la contractualisation, les avenants, et la séparation. + +## 5.1 Recherche d’une assistante maternelle + +### 5.1.1 Déclenchement + +- Un parent connecté accède à son tableau de bord. +- S’il n’a pas encore d'assistante maternelle, un panneau de recherche s’affiche automatiquement. +- Sinon, il peut cliquer sur le bouton “Rechercher une assistante maternelle”. + +### 5.1.2 Étape 1 – Informations du dossier + +Le parent complète un formulaire : +- Enfant concerné +- Type de contrat : année complète, incomplète, ou temporaire (remplacement) +- Fourniture des repas (oui/non) +- Budget maximum par poste (taux horaire, repas, indemnités) +- Plages horaires souhaitées pour chaque jour + +### 5.1.3 Étape 2 – Texte d’introduction + +Le parent rédige une courte présentation à destination des assistantes maternelles, expliquant sa situation. + +### 5.1.4 Étape 3 – Disponibilités pour un rendez-vous + +Le parent indique ses plages de disponibilité pour une rencontre. + +### 5.1.5 Étape 4 – Recherche et sélection + +- Une liste des assistantes maternelles est proposée, classée par **proximité géographique**. +- Chaque fiche contient des initiales (photo uniquement visible après rencontre). +- Une carte interactive affiche leur localisation. +- Le parent peut sélectionner une ou plusieurs nounous à qui envoyer un dossier. + +### 5.1.6 Étape 5 – Récapitulatif et envoi + +- Le parent valide le dossier complet +- Le dossier est transmis aux assistantes maternelles sélectionnées + +## 5.2 Traitement du dossier + +### 5.2.1 Réception par l'assistante maternelle + +- Le dossier s’affiche dans son tableau de bord +- Elle peut consulter : + - Fiche parent 1 et parent 2 (si existant) + - Fiche enfant + - Texte de présentation + - Informations pratiques et financières + - Plages de disponibilité proposées + +### 5.2.2 Réponse de l’assistante maternelle + +- Elle peut accepter, refuser, ou demander à échanger via messagerie. +- Si elle accepte, elle sélectionne un ou plusieurs créneaux compatibles avec ceux proposés. + +### 5.2.3 Proposition de rendez-vous + +- Le parent reçoit les créneaux suggérés. +- Il peut en choisir un, ou en proposer un autre à partir de l’agenda. +- L'assistante maternelle accepte, refuse ou modifie. +- Ce ping-pong se poursuit jusqu’à accord. + +### 5.2.4 Finalisation du rendez-vous + +- Une fois confirmé, les numéros de téléphone sont échangés. +- Le rendez-vous a lieu hors de la plateforme. + +## 5.3 Création du contrat + +### 5.3.1 Validation mutuelle + +- Après le rendez-vous, l'assistante maternelle et les parents sont invités à confirmer la poursuite du dossier. +- Chacun renseigne : + - Horaires de garde + - Taux horaire + - Indemnités repas et entretien + - Début du contrat + +### 5.3.2 Vérification croisée + +- L’outil compare les informations saisies par les deux parties. +- Si elles diffèrent : + - Nouvelle saisie demandée + - Le gestionnaire peut être notifié pour accompagner + +- Si elles concordent : + - Le contrat est généré automatiquement + +### 5.3.3 Validation finale + +Une fois le contrat généré et validé par le gestionnaire : + +1. **Téléchargement** : chaque partie récupère le PDF. +2. **Signature manuscrite** : les parents (employeurs) et l’assistante maternelle signent physiquement le document. +3. **Confirmation dans la plateforme** : + - Un bouton « J’ai signé » apparaît pour chaque partie. + - (Optionnel) Téléversement du contrat signé scanné ou photographié. +4. Dès que les deux confirmations sont saisies, **le contrat est considéré actif** ; l’application lie définitivement les comptes parent / assistante maternelle à ce contrat. + +> Lors de la génération du contrat, le NIR de l’assistante maternelle est injecté dans le PDF, mais **n’est affiché nulle part dans l’application**. + +## 5.4 Avenants + +### 5.4.1 Initiation + +- Un bouton “Proposer un avenant” est disponible dans le panneau Contrat +- Les deux parties peuvent initier un avenant + +### 5.4.2 Contenu + +- Modification des horaires +- Revalorisation du taux horaire +- Changement de type de contrat (année complète ↔ incomplète) +- Nombre de semaines de garde + +### 5.4.3 Validation + +- Même processus que pour le contrat initial : + - Chaque partie valide + - Vérification de cohérence + - Approbation finale du gestionnaire + +### 5.4.4 Amélioration future possible + +- Numérotation automatique des avenants +- Historique des modifications + +## 5.5 Fin de contrat + +### 5.5.1 Déclaration de fin + +- Initiée par l’une des parties +- Raison de la rupture (texte obligatoire) +- Option “Notifier le gestionnaire” (cas de désaccord) + +### 5.5.2 Intervention du gestionnaire + +- Peut proposer un rendez-vous de conciliation +- Possibilité de laisser des observations + +#### 5.5.2 bis Procédure en cas de désaccord persistant +- Si la médiation du gestionnaire n’aboutit pas : + 1. Création automatique d’un **dossier Litige** (statut “Ouvert”) visible du gestionnaire et des deux parties. + 2. Possibilité d’ajouter des commentaires horodatés et pièces justificatives (non obligatoires). + 3. Génération d’un **rapport PDF** exportable pour saisie d’un conciliateur ou des services juridiques de la mairie. +- Le dossier passe à “Clôturé” : + - par accord écrit des parties, + - ou après 90 jours sans activité (archivage automatique). + +### 5.5.3 Solde de tout compte + +- Calcul automatique du solde : + - Congés restants + - Indemnités + - Heures supplémentaires +- Génération d’un récapitulatif téléchargeable + + +# 6. Outils partagés + +Certains outils de la plateforme sont accessibles à plusieurs profils et favorisent la collaboration entre les utilisateurs tout en assurant la traçabilité des échanges. + +## 6.1 Messagerie + +- Intégrée dans tous les tableaux de bord (parents, assistantes maternelles, gestionnaires) +- Permet les échanges directs entre parents et assistantes maternelles +- Possibilité pour les parents ou l'assistante maternelle d’ajouter le gestionnaire à une conversation existante +- Le gestionnaire peut initier des conversations avec : + - Un parent + - Une assistante maternelle + - Les deux à la fois (en cas de médiation) +- Conversations de groupe possibles entre le gestionnaire et plusieurs assistantes maternelles (organisation d’événements) +- 📌 **Nota** : les parents ne participent jamais à ces groupes collectifs +- ✅ Si deux parents sont déclarés, ils sont **automatiquement intégrés** à toutes les conversations liées à leur enfant + +## 6.2 Agenda + +- Chaque rôle dispose d’un agenda intégré +- Permet d’ajouter, modifier ou valider : + - Absences enfants + - Congés des assistantes maternelles + - Vacances des parents + - Événements RPE +- Un résumé visuel est proposé (vue mois/semaine/année) +- Synchronisation automatique avec les événements validés par l’autre partie +- Récapitulatif hebdomadaire (ou quotidien) envoyé à l’assistante maternelle + +##### Workflow « Arrêt maladie » +- Fonctionne sans stockage de document médical : + 1. L’assistante maternelle clique **Arrêt transmis aux parents**. + 2. Le parent concerné valide via **Arrêt bien reçu**. +- Une fois les deux actions réalisées, l’arrêt maladie est confirmé dans les agendas des deux parties. + + +## 6.3 Fiche enfant + +- Visible et modifiable par les parents +- Consultable par : + - L’assistante maternelle (si liée à un contrat) + - Le gestionnaire (à titre d’information) +- Contient : + - Identité + - Photo (obligatoire si né) + - Statut (à naître / actif / scolarisé) + - Parents associés + - Mention s’il s’agit de jumeaux, triplés, etc. + +## 6.4 Contrats et avenants + +- Partagés entre les parents, l’assistante maternelle, et le gestionnaire +- Contrat actif consultable dans les tableaux de bord +- Avenants consultables dans l’historique +- Contrat généré uniquement après validation mutuelle et approbation du gestionnaire +- L’outil garantit que seules les versions validées sont actives et accessibles + + +# 7. Suivi administratif et financier + +La plateforme SuperNounou intègre plusieurs outils d’accompagnement pour simplifier la gestion administrative et financière des contrats entre parents et assistantes maternelles. + +## 7.1 Fiche de paie + +- Calcul automatique des éléments mensuels à reporter : + - Heures normales rémunérées + - Heures supplémentaires + - Indemnités repas et entretien + - Absences déduites +- Récapitulatif consultable dans l’espace Contrat du parent et de l’assistante maternelle +- Export possible sous format PDF pour usage personnel ou archivage +- Le NIR de l’assistante maternelle est utilisé pour pré-remplir les champs Pajemploi ; il n’est jamais affiché à l’écran. + + +## 7.2 Aide au remplissage Pajemploi + +- Affichage clair et guidé de la **feuille à remplir** sur le site Pajemploi +- SuperNounou préremplit chaque champ avec les valeurs calculées à partir du contrat +- Objectif : faciliter la saisie et éviter les erreurs de déclaration +- 📌 **Nota** : il ne s’agit pas d’une déclaration automatique + +## 7.3 Heures supplémentaires + +- L’assistante maternelle peut déclarer des heures supplémentaires via son tableau de bord : + - Sélection du jour concerné + - Nombre d’heures effectuées au-delà du contrat + - Commentaire facultatif +- Les heures apparaissent ensuite dans le récapitulatif mensuel +- Le tarif des heures supplémentaires est défini dans le contrat initial + +## 7.4 Historique + +- Chaque contrat conserve un historique : + - Avenants + - Déclarations mensuelles + - Modifications validées + - Absences notables +- Accessible aux parents, à l’assistante maternelle, et au gestionnaire + +## 7.5 Solde de tout compte + +- Lors de la fin de contrat, l’outil génère un récapitulatif : + - Jours de congés restants ou à indemniser + - Indemnités éventuelles + - Heures supplémentaires non réglées +- Permet d’établir un **solde de tout compte** clair, limitant les risques de conflit + + +# 8. Administration technique et conformité + +Ce chapitre regroupe les mécanismes garantissant la sécurité, la fiabilité et la conformité légale de la plateforme SuperNounou. Il s’adresse aux administrateurs techniques des collectivités. + +## 8.1 Sécurité et accès + +- Chaque utilisateur possède un identifiant personnel et un mot de passe +- Les administrateurs et gestionnaires doivent changer leur mot de passe à la première connexion +- Journalisation des accès sensibles (historique des connexions, changements de paramètres) +- Sécurisation HTTPS, stockage des mots de passe chiffré +- Limitation des tentatives de connexion frauduleuses (anti-brute-force) +- Chiffrement au repos : le NIR est stocké chiffré (AES-256, clé KMS). +- Accès restreint : seule l’assistante maternelle peut consulter ou modifier son NIR. +- Journalisation : toute modification du NIR est tracée (horodatage, ID utilisateur). +- Aucune pièce jointe médicale n’est conservée ; seules les métadonnées de validation des arrêts maladie (date, utilisateurs) sont journalisées. +- Les rapports créés via « Signaler un bug » sont stockés dans la table `bug_report` avec horodatage et identifiant utilisateur, accessibles uniquement aux administrateurs. + +> 🔗 **Sauvegarde & PRA** +> Les exigences détaillées sont décrites dans le SSS-SAU-001 « Sauvegarde & Plan de Reprise d’Activité ». +> Le présent CDC ne reprend que les objectifs RPO/RTO et la nécessité de tests semestriels. + + +## 8.2 Gouvernance multi-admins + +- Possibilité de créer plusieurs administrateurs par commune +- Droits identiques, sauf si future extension multi-mairies (non implémentée à ce stade) +- Suivi des actions d’administration horodatées pour audit interne + +## 8.3 Conformité RGPD + +- Affichage explicite des consentements lors de la création de compte +- Possibilité pour l’utilisateur de : + - Consulter les données stockées + - Demander la suppression de son compte +- Interface d’anonymisation des données sur demande +- Accès à la politique de confidentialité depuis chaque tableau de bord +- Archivage horodaté des modifications sensibles +- Stockage du consentement photo avec date, heure, utilisateur concerné. +- Retrait de consentement : masque immédiat de la photo, déclenchement d’une suppression définitive sous 30 jours. +- Registre des traitements mis à jour pour la finalité « affichage photo assistante maternelle / enfant ». + +#### 8.3.1 Fondement juridique +- Collecte du NIR fondée sur l’exécution du contrat de travail (art. 6-1-b RGPD). + +#### 8.3.2 Minimisation +- Le NIR est utilisé **exclusivement** pour générer le contrat PDF et la fiche de paie. + +#### 8.3.3 Conservation +- Conservation : 5 ans après la fin du dernier contrat, puis hachage irréversible ou suppression. + +#### 8.3.4 Consentement photo +- Stockage de l’accord avec date, heure, utilisateur. +- Retrait de consentement : masque immédiat, suppression définitive sous 30 jours. +- Registre des traitements mis à jour : finalité « Affichage photo assistante maternelle / enfant ». + +#### 8.3.5 Droits des personnes et suppression de compte +- Les parents et assistantes maternelles peuvent **supprimer** leur compte à tout moment depuis le menu Profil. +- La suppression entraîne : + - désactivation immédiate de l’accès, + - effacement des données personnelles hors obligations légales (contrats archivés 5 ans). +- **Anonymisation totale non applicable** : le lien employeur – salarié impose de conserver certaines informations nominatives dans les contrats et historiques de paie conformément au Code du Travail. + +## 8.4 Intégration API + +- Possibilité future d’intégration aux systèmes d’information municipaux : + - Statistiques + - Données anonymisées d’activité + - Synchronisation éventuelle avec les portails familles ou services petite enfance +- ⚠️ Préparation automatique des déclarations Pajemploi via API non disponible par défaut : + - Cette fonctionnalité nécessite une habilitation spécifique de l’URSSAF + - Elle est donc classée comme amélioration future possible + +### 8.5 Architecture technique + +L’équipe de développement est libre de choisir la pile technologique ; seules les règles suivantes sont impératives. + +> 🔗 Les aspects techniques (sauvegarde, PRA, CI/CD, API, déploiement) sont détaillés dans le document **SSS-001 – Spécification technique & opérationnelle unifiée**. + +#### 8.5.1 Principes directeurs +- **Conformité RGPD** : minimisation, chiffrement au repos et en transit. +- **Sécurité by-design** : respect OWASP Top 10, revue de code, tests de pénétration. +- **Modularité & maintenabilité** : séparation claire frontend / backend / base. +- **Portabilité** : conteneurisation (Docker ou équivalent) pour exécution locale et serveur communal. +- **Traçabilité** : journaux horodatés, corrélables (API, base, authentification). +- **Documentation** : README d’installation, diagrammes d’architecture, description API. + +#### 8.5.2 Contraintes non fonctionnelles _(valeurs indicatives)_ +- Disponibilité mensuelle ≥ **98 %**. +- Temps de réponse **P95 < 500 ms** sur les opérations courantes. +- Capacité de test : **≈ 50 sessions simultanées**. +- Compatibilité navigateurs : Chrome, Edge, Firefox, Safari (versions N, N-1). +- Accessibilité : niveau **RGAA AA**. +- Agenda utilisable hors-ligne ; synchronisation sous 24 h. + +#### 8.5.3 Déploiement (instance communale) +- L’équipe **propose** la méthode de packaging (ex. Docker Compose, VM image, .deb) permettant une installation sur un serveur Linux municipal **en < 1 h**. +- Un **script “update / rollback”** doit être livré (ou procédure équivalente). +- Un **pipeline CI/CD** minimal (tests + build image) est requis ; l’école fournit son dépôt Git / runner. +- Les journaux applicatifs et les métriques doivent pouvoir être branchés sur l’outillage de supervision de la commune (format libre, à documenter). + +#### 8.5.4 Mode faible connectivité +Service Worker : mise en cache des ressources critiques et file d’attente locale (≤ 24 h) pour l’agenda et la messagerie. + +#### 8.5.5 Sécurité technique +TLS 1.3, en-tête HSTS 12 mois, scan d’images conteneurs (CVE > 7 bloquantes), secrets via Vault ou fichier .env chiffré, rotation 90 j. + +# 9. Améliorations futures possibles + +Certaines fonctionnalités ne sont pas prioritaires dans la première version de SuperNounou, mais sont identifiées comme des évolutions possibles à forte valeur ajoutée pour les utilisateurs et les collectivités. + +## 9.1 Modules activables + +Ces modules peuvent être activés ou non par les administrateurs, selon la politique de la mairie ou les ressources disponibles. + +### 9.1.1 Blog RPE + +- Le gestionnaire peut publier des actualités dans un blog visible depuis le tableau de bord des parents et des assistantes maternelles. +- Il peut activer ou désactiver les commentaires. +- Il peut aussi publier des sondages ou cibler certaines publications (parents uniquement, assistantes maternelles uniquement, ou tous). +- La mairie (via l’administrateur) peut également publier dans ce flux. + +### 9.1.2 IA de reformulation + +- Un système d’intelligence artificielle peut proposer une reformulation des messages écrits avant envoi. +- Utile pour clarifier un message contractuel ou technique. +- Le message IA est signalé comme tel. +- Une API externe (ChatGPT, Claude, etc.) peut être configurée par l’administrateur. + +### 9.1.3 Connexion Pajemploi (API URSSAF) + +- Permettrait un envoi automatique des données vers Pajemploi. +- ⚠️ Cette API nécessite une habilitation spécifique par l’URSSAF. +- Requiert un partenariat de confiance entre la collectivité et l’institution. + +### 9.1.4 Garde d’enfants “grands” + +- Permet à une assistante maternelle d’accueillir des enfants scolarisés en périscolaire. +- Requiert un agrément spécifique, indiqué dans le profil. +- Les parents peuvent déclarer un enfant comme scolarisé. + +### 9.1.5 Pré-remplissage des dossiers scolaires + +- À la fin d’un contrat, un rappel automatique peut être envoyé aux parents sur les dates d’inscription à l’école. +- Pièces jointes : formulaire d’inscription, prérempli avec les données de la fiche enfant. + +### 9.1.6 Dossier fratrie unique + +- Un seul dossier pour plusieurs enfants (fratrie simultanée). +- Par défaut, chaque enfant dispose actuellement de son propre dossier. +- Cette fonctionnalité regrouperait les démarches dans un flux unique. +- Contrats séparés, workflow mutualisé + - Un seul **dossier de recherche** et de mise en relation couvre l’ensemble de la fratrie. + - Au moment de la génération, **un contrat distinct** est créé pour chaque enfant (obligations légales inchangées), mais les étapes du workflow (recherche, rendez-vous, validation) ne sont effectuées qu’une fois. + - Les parents et l’assistante maternelle n’ont donc qu’une seule demande à gérer, tandis que la plateforme conserve la granularité nécessaire pour la paie et les déclarations Pajemploi. + - Les écrans doivent afficher un badge « Fratrie » permettant de basculer d’un contrat à l’autre. + +#### 9.1.7 Signature électronique des contrats +- Intégration d’un prestataire de signature qualifiée (eIDAS) pour rendre le processus 100 % dématérialisé. +- Bénéfices : gain de temps, valeur probante renforcée, archivage automatisé. +- Complexité : coûts de licence, vérification d’identité, circuit API. + +#### 9.1.8 Intégration LDAP / SSO (agents municipaux) +- Connexion via annuaire LDAP ou fournisseur SAML 2. +- Les comptes parents / nounous restent sur le login interne. + +#### 9.1.9 Base de données externe PostgreSQL +- Possibilité de connecter l’application à un cluster PostgreSQL déjà géré par la DSI. +- Scripts d’initialisation et de migration fournis (Flyway/Liquibase). + +#### 9.1.10 Offre SaaS mutualisée +- Hébergement centralisé pour les communes sans infrastructure locale. +- Facturation à l’usage ; supervision et sauvegardes opérées par l’éditeur. + +#### 9.1.11 Mise en conformité RGAA (niveau AA) +- Audit complet : contrastes, navigation clavier, rôles ARIA. +- Correctifs design et tests utilisateurs. +- Priorité : **amélioration future**. + +#### 9.1.12 Centre d’aide / support utilisateur +- FAQ consultable + formulaire de ticket. +- Tableau de bord « Support » pour la mairie. +- Possibilité d’intégrer un chatbot d’assistance. +- Priorité : **amélioration future**. + +## 9.2 Stratégie d’activation + +- Chaque module peut être activé depuis le tableau de bord administrateur +- Une section “Modules complémentaires” permet : + - De consulter les modules disponibles + - D’activer/désactiver chaque fonctionnalité + - D’ajouter les API externes nécessaires (IA, URSSAF…) + +- Les modules activés sont ensuite visibles dans l’interface utilisateur concernée (parents, assistantes maternelles, gestionnaires). + +# 10. Synthèse finale + +SuperNounou est bien plus qu’un simple outil de gestion de la garde d’enfants. C’est une plateforme publique, inclusive et responsable, au service des familles, des assistantes maternelles et des collectivités locales. + +## 10.1 Valeurs portées + +SuperNounou s’inscrit dans une démarche de service public, avec des fondements clairs : + +- **Neutralité** : pas de système de notation, pas de mise en concurrence biaisée. +- **Équité** : tous les utilisateurs bénéficient des mêmes droits d’usage. +- **Transparence** : chaque processus est lisible, traçable, et validé par les deux parties. +- **Accessibilité** : l’interface est pensée pour les téléphones comme pour les ordinateurs. + +## 10.2 Bénéfices pour les utilisateurs + +| Acteur | Bénéfices directs | +|-------------------------|---------------------------------------------------------------------------| +| **Parents** | Simplicité de recherche, centralisation des échanges, aide Pajemploi | +| **Assistantes maternelles** | Gain de temps, traçabilité des contrats, autonomie de gestion | +| **Gestionnaires** | Suivi global des situations, outils de médiation, organisation d’événements | +| **Administrateurs** | Supervision complète, gouvernance multi-rôle, contrôle des données | + +## 10.3 Périmètre fonctionnel de la V1 + +Fonctionnalités incluses dès la première version : +- Création de comptes +- Recherche et sélection de nounous +- Suivi des dossiers +- Génération de contrat et avenants +- Messagerie +- Gestion des événements et agenda +- Aide à la paie Pajemploi +- Gestion des heures supplémentaires +- Suivi RGPD et administration + +## 10.4 Perspectives + +La structure du produit permet une montée en charge progressive : +- Activation des modules complémentaires (blog, IA, API Pajemploi) +- Adaptation aux nouvelles attentes des mairies +- Déploiement intercommunal possible +- Intégration avec les portails citoyens des collectivités + +SuperNounou incarne une nouvelle manière de soutenir les parcours de garde dans les territoires. C’est une application qui place **l’humain**, **la clarté** et **le lien de confiance** au cœur du service. + +## 10.5 Propriété intellectuelle et licence de l’application + +- Tout le code source, la marque « SuperNounou » et la documentation associée demeurent la **propriété exclusive de l’éditeur** (Julien). +- Les communes clientes reçoivent une **licence d’utilisation non exclusive, non transférable** : + - droit d’installer et d’exécuter l’application dans leur infrastructure, + - droit d’effectuer les sauvegardes nécessaires à l’exploitation, + - interdiction de distribuer, vendre ou mettre le code à disposition de tiers sans accord écrit. +- Le code source n’est pas publié sous une licence open-source ; il peut être remis **sous clause d’escrow** (tiers séquestre) pour garantir la maintenance en cas de défaillance de l’éditeur. +- Les étudiants ou prestataires contribuant au projet cèdent leurs droits patrimoniaux à l’éditeur via un accord de cession signé avant livraison. +- Toute personnalisation réalisée pour une commune fait partie intégrante du produit et est couverte par la licence ci-dessus, sauf stipulation contraire. + +--- + +*Fin du document* + diff --git a/docs/SuperNounou_SSS-001.md b/docs/SuperNounou_SSS-001.md new file mode 100644 index 0000000..68553e3 --- /dev/null +++ b/docs/SuperNounou_SSS-001.md @@ -0,0 +1,108 @@ +# SuperNounou – SSS-001 +## Spécification technique & opérationnelle unifiée +_Version 0.2 – 24 avril 2025_ + +--- + +## 1. Objet +Centraliser tous les aspects **techniques et opérationnels** de la plateforme SuperNounou : +- Sauvegarde & Plan de Reprise d’Activité (PRA) +- Spécifications des API & intégrations +- Directives de déploiement, d’observabilité, de CI/CD + +## 2. Portée +Instances de production, pré-production et recette (Frontend, Backend, PostgreSQL, stockage objets), scripts d’installation, pipelines CI/CD, journaux et métriques. + +## 3. Références +- CDC SuperNounou V1.1 +- ISO 27001 / ISO 22301 bonnes pratiques +- Politique sécurité DSI Enedis #SEC-POL-2024 +- RGPD (2016/679) + +--- + +# A – Sauvegarde & Plan de Reprise d’Activité + +### A.1 Architecture de sauvegarde +Schéma bloc, chiffrement AES-256 (KMS), réplication hors-site « Object Storage B ». + +### A.2 Stratégie de sauvegarde + +| Type | Fréquence | Rétention | Support | +|-------------------|-------------|-----------|--------------------| +| Incrémentale | Quotidienne | 30 j | Object Storage A | +| Complète | Hebdomadaire| 6 mois | Object Storage B | +| Export logique DB | Mensuelle | 5 ans | Stockage Glacier | + +### A.3 PRA +RPO 24 h / RTO 4 h – scénarios : panne VM, corruption DB, sinistre DC – procédure détaillée + escalade. + +### A.4 Tests de restauration +Intégrale semestrielle, partielle trimestrielle – rapport d’audit et actions correctives. + +### A.5 Monitoring & alertes +Endpoint Prometheus `/metrics`, tableau Grafana « Backup status », alerte > 26 h sans backup. + +### A.6 Rôles +DevOps Lead (implémentation), DBA (tests restore), RSSI (audit). + +--- + +# B – API & Intégrations + +### B.1 Conventions +OpenAPI 3 livré (`openapi.yaml`), version URL `/api/v1`, ISO 8601 dates. + +### B.2 Sécurité API +JWT Bearer (ou OAuth 2), TLS 1.3, rate-limit 100 req/min/IP, signature HMAC pour webhooks. + +### B.3 Exemples +Collection Postman, scripts cURL, guide « Appeler l’API ». + +### B.4 Intégrations futures +SSO LDAP/SAML, webhook `contract.validated`, export statistiques CSV. + +--- + +# C – Déploiement, CI/CD et Observabilité *(nouveau)* + +### C.1 Déploiement communal (on-premise) +- **Objectif** : installation complète sur un serveur Linux ou VM en moins d’1 h. +- **Livrable** : solution de packaging **au choix** (Docker Compose, image VM, paquet .deb/.rpm). +- **Script update / rollback** : `update.sh` ou équivalent (backup ➜ pull ➜ migrate ➜ vérif ; rollback ≤ 5 min). +- **Config** : fichier `.env.sample` décrivant toutes les variables. + +### C.2 Environnements +- Developpement local (Docker Compose). +- Recette & Production (serveur communal). +- Les étudiants doivent décrire la procédure de bascule Recette → Prod. + +### C.3 Pipeline CI/CD +- Pipeline automatisé (tests unitaires + build image + scan CVE) déclenché à chaque merge. +- L’école fournit son propre dépôt Git/runner. +- Artifacts : images taguées, notes de version (`CHANGELOG.md`). + +### C.4 Observabilité & logs +- Journaux applicatifs JSON (timestamp UTC, level, traceId). +- Rotation/retention : 7 jours sur disque, 30 jours sur archive compressée. +- Export métriques Prometheus (`/metrics`) : latence API, nombre de sessions, files d’attente hors-ligne. +- Tableaux Grafana d’exemple inclus (`grafana_dashboard.json`). + +### C.5 SLA et performances (indicatifs) +- Disponibilité mensuelle cible : **≥ 98 %**. +- Temps de réponse P95 des opérations courantes : **< 500 ms**. +- Capacité test : **≈ 50 sessions simultanées** sans dégradation (> 1 s). + +--- + +# D – Glossaire +AES-256, JWT, KMS, OpenAPI, RPO, RTO, rate-limit, HMAC, Compose, CI/CD… + +--- + +# E – Historique des versions + +| Version | Date | Auteur | Commentaire | +|---------|------------|------------------|---------------------------------| +| 0.1-draft | 2025-04-24 | Équipe projet | Création du SSS unifié | +| 0.2 | 2025-04-24 | ChatGPT & Julien | Ajout déploiement / CI/CD / logs | diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..79c113f --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/frontend/.metadata b/frontend/.metadata new file mode 100644 index 0000000..6fbbc45 --- /dev/null +++ b/frontend/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ea121f8859e4b13e47a8f845e4586164519588bc" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: web + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: windows + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..9e6f97c --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,16 @@ +# petitspas + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/frontend/analysis_options.yaml b/frontend/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/frontend/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9ce3b78 --- /dev/null +++ b/frontend/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + json) { + return AppUser( + id: json['id'] as String, + email: json['email'] as String, + role: json['role'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + updatedAt: DateTime.parse(json['updatedAt'] as String), + ); + } + + Map toJson() { + return { + 'id': id, + 'email': email, + 'role': role, + 'createdAt': createdAt.toIso8601String(), + 'updatedAt': updatedAt.toIso8601String(), + }; + } +} \ No newline at end of file diff --git a/frontend/lib/models/user_registration_data.dart b/frontend/lib/models/user_registration_data.dart new file mode 100644 index 0000000..d2c6954 --- /dev/null +++ b/frontend/lib/models/user_registration_data.dart @@ -0,0 +1,97 @@ +import 'dart:io'; // Pour File +import '../models/card_assets.dart'; // Import de l'enum CardColorVertical + +class ParentData { + String firstName; + String lastName; + String address; // Rue et numéro + String postalCode; // Ajout + String city; // Ajout + String phone; + String email; + String password; // Peut-être pas nécessaire pour le récap, mais pour la création initiale si + File? profilePicture; // Chemin ou objet File + + ParentData({ + this.firstName = '', + this.lastName = '', + this.address = '', // Rue + this.postalCode = '', // Ajout + this.city = '', // Ajout + this.phone = '', + this.email = '', + this.password = '', + this.profilePicture, + }); +} + +class ChildData { + String firstName; + String lastName; + String dob; // Date de naissance ou prévisionnelle + bool photoConsent; + bool multipleBirth; + bool isUnbornChild; + File? imageFile; + CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte + + ChildData({ + this.firstName = '', + this.lastName = '', + this.dob = '', + this.photoConsent = false, + this.multipleBirth = false, + this.isUnbornChild = false, + this.imageFile, + required this.cardColor, // Rendre requis dans le constructeur + }); +} + +class UserRegistrationData { + ParentData parent1; + ParentData? parent2; // Optionnel + List children; + String motivationText; + bool cguAccepted; + + UserRegistrationData({ + ParentData? parent1Data, + this.parent2, + List? childrenData, + this.motivationText = '', + this.cguAccepted = false, + }) : parent1 = parent1Data ?? ParentData(), + children = childrenData ?? []; + + // Méthode pour ajouter/mettre à jour le parent 1 + void updateParent1(ParentData data) { + parent1 = data; + } + + // Méthode pour ajouter/mettre à jour le parent 2 + void updateParent2(ParentData? data) { + parent2 = data; + } + + // Méthode pour ajouter un enfant + void addChild(ChildData child) { + children.add(child); + } + + // Méthode pour mettre à jour un enfant (si nécessaire plus tard) + void updateChild(int index, ChildData child) { + if (index >= 0 && index < children.length) { + children[index] = child; + } + } + + // Mettre à jour la motivation + void updateMotivation(String text) { + motivationText = text; + } + + // Accepter les CGU + void acceptCGU() { + cguAccepted = true; + } +} \ No newline at end of file diff --git a/frontend/lib/navigation/app_router.dart b/frontend/lib/navigation/app_router.dart new file mode 100644 index 0000000..70b55a2 --- /dev/null +++ b/frontend/lib/navigation/app_router.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../screens/auth/login_screen.dart'; +import '../screens/auth/register_choice_screen.dart'; +import '../screens/auth/parent_register_step1_screen.dart'; +import '../screens/auth/parent_register_step2_screen.dart'; +import '../screens/auth/parent_register_step3_screen.dart'; +import '../screens/auth/parent_register_step4_screen.dart'; +import '../screens/auth/parent_register_step5_screen.dart'; +import '../screens/home/home_screen.dart'; +import '../models/user_registration_data.dart'; + +class AppRouter { + static const String login = '/login'; + static const String registerChoice = '/register-choice'; + static const String parentRegisterStep1 = '/parent-register/step1'; + static const String parentRegisterStep2 = '/parent-register/step2'; + static const String parentRegisterStep3 = '/parent-register/step3'; + static const String parentRegisterStep4 = '/parent-register/step4'; + static const String parentRegisterStep5 = '/parent-register/step5'; + static const String home = '/home'; + + static Route generateRoute(RouteSettings settings) { + Widget screen; + bool slideTransition = false; + Object? args = settings.arguments; + + Widget buildErrorScreen(String step) { + print("Erreur: Données UserRegistrationData manquantes ou de mauvais type pour l'étape $step"); + return const ParentRegisterStep1Screen(); + } + + switch (settings.name) { + case login: + screen = const LoginPage(); + break; + case registerChoice: + screen = const RegisterChoiceScreen(); + slideTransition = true; + break; + case parentRegisterStep1: + screen = const ParentRegisterStep1Screen(); + slideTransition = true; + break; + case parentRegisterStep2: + if (args is UserRegistrationData) { + screen = ParentRegisterStep2Screen(registrationData: args); + } else { + screen = buildErrorScreen('2'); + } + slideTransition = true; + break; + case parentRegisterStep3: + if (args is UserRegistrationData) { + screen = ParentRegisterStep3Screen(registrationData: args); + } else { + screen = buildErrorScreen('3'); + } + slideTransition = true; + break; + case parentRegisterStep4: + if (args is UserRegistrationData) { + screen = ParentRegisterStep4Screen(registrationData: args); + } else { + screen = buildErrorScreen('4'); + } + slideTransition = true; + break; + case parentRegisterStep5: + if (args is UserRegistrationData) { + screen = ParentRegisterStep5Screen(registrationData: args); + } else { + screen = buildErrorScreen('5'); + } + slideTransition = true; + break; + case home: + screen = const HomeScreen(); + break; + default: + screen = Scaffold( + body: Center( + child: Text('Route non définie : ${settings.name}'), + ), + ); + } + + if (slideTransition) { + return PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => screen, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + const begin = Offset(1.0, 0.0); + const end = Offset.zero; + const curve = Curves.easeInOut; + var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve)); + var offsetAnimation = animation.drive(tween); + return SlideTransition(position: offsetAnimation, child: child); + }, + transitionDuration: const Duration(milliseconds: 400), + ); + } else { + return MaterialPageRoute(builder: (_) => screen); + } + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/login_screen.dart b/frontend/lib/screens/auth/login_screen.dart new file mode 100644 index 0000000..f6c8a8f --- /dev/null +++ b/frontend/lib/screens/auth/login_screen.dart @@ -0,0 +1,399 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:url_launcher/url_launcher.dart'; +import 'package:p_tits_pas/services/bug_report_service.dart'; +import 'package:go_router/go_router.dart'; +import '../../widgets/image_button.dart'; +import '../../widgets/custom_app_text_field.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + String? _validateEmail(String? value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer votre email'; + } + if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { + return 'Veuillez entrer un email valide'; + } + return null; + } + + String? _validatePassword(String? value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer votre mot de passe'; + } + if (value.length < 6) { + return 'Le mot de passe doit contenir au moins 6 caractères'; + } + return null; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.transparent, + body: LayoutBuilder( + builder: (context, constraints) { + // Version desktop (web) + if (kIsWeb) { + final w = constraints.maxWidth; + final h = constraints.maxHeight; + + return FutureBuilder( + future: _getImageDimensions(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator()); + } + + final imageDimensions = snapshot.data!; + final imageHeight = h; + final imageWidth = imageHeight * (imageDimensions.width / imageDimensions.height); + final remainingWidth = w - imageWidth; + final leftMargin = remainingWidth / 4; + + return Stack( + children: [ + // Fond en papier + Positioned.fill( + child: Image.asset( + 'assets/images/paper2.png', + fit: BoxFit.cover, + repeat: ImageRepeat.repeat, + ), + ), + // Image principale + Positioned( + left: leftMargin, + top: 0, + height: imageHeight, + width: imageWidth, + child: Image.asset( + 'assets/images/river_logo_desktop.png', + fit: BoxFit.contain, + ), + ), + // Formulaire dans le cadran en bas à droite + Positioned( + right: 0, + bottom: 0, + width: w * 0.6, // 60% de la largeur de l'écran + height: h * 0.5, // 50% de la hauteur de l'écran + child: Padding( + padding: EdgeInsets.all(w * 0.02), // 2% de padding + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Champs côte à côte + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: CustomAppTextField( + controller: _emailController, + labelText: 'Email', + hintText: 'Votre adresse email', + validator: _validateEmail, + style: CustomAppTextFieldStyle.lavande, + fieldHeight: 53, + fieldWidth: double.infinity, + ), + ), + const SizedBox(width: 20), + Expanded( + child: CustomAppTextField( + controller: _passwordController, + labelText: 'Mot de passe', + hintText: 'Votre mot de passe', + obscureText: true, + validator: _validatePassword, + style: CustomAppTextFieldStyle.jaune, + fieldHeight: 53, + fieldWidth: double.infinity, + ), + ), + ], + ), + const SizedBox(height: 20), + // Bouton centré + Center( + child: ImageButton( + bg: 'assets/images/btn_green.png', + width: 300, + height: 40, + text: 'Se connecter', + textColor: const Color(0xFF2D6A4F), + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + // TODO: Implémenter la logique de connexion + } + }, + ), + ), + const SizedBox(height: 10), + // Lien mot de passe oublié + Center( + child: TextButton( + onPressed: () { + // TODO: Implémenter la logique de récupération de mot de passe + }, + child: Text( + 'Mot de passe oublié ?', + style: GoogleFonts.merienda( + fontSize: 14, + color: const Color(0xFF2D6A4F), + decoration: TextDecoration.underline, + ), + ), + ), + ), + const SizedBox(height: 10), + // Lien de création de compte + Center( + child: TextButton( + onPressed: () { + Navigator.pushNamed(context, '/register-choice'); + }, + child: Text( + 'Créer un compte', + style: GoogleFonts.merienda( + fontSize: 16, + color: const Color(0xFF2D6A4F), + decoration: TextDecoration.underline, + ), + ), + ), + ), + const SizedBox(height: 20), // Réduit l'espacement en bas + ], + ), + ), + ), + ), + // Pied de page + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + decoration: BoxDecoration( + color: Colors.transparent, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _FooterLink( + text: 'Contact support', + onTap: () async { + final Uri emailLaunchUri = Uri( + scheme: 'mailto', + path: 'support@supernounou.local', + ); + if (await canLaunchUrl(emailLaunchUri)) { + await launchUrl(emailLaunchUri); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Impossible d\'ouvrir le client mail', + style: GoogleFonts.merienda(), + ), + ), + ); + } + }, + ), + _FooterLink( + text: 'Signaler un bug', + onTap: () { + _showBugReportDialog(context); + }, + ), + _FooterLink( + text: 'Mentions légales', + onTap: () { + Navigator.pushNamed(context, '/legal'); + }, + ), + _FooterLink( + text: 'Politique de confidentialité', + onTap: () { + Navigator.pushNamed(context, '/privacy'); + }, + ), + ], + ), + ), + ), + ], + ); + }, + ); + } + + // Version mobile (à implémenter) + return const Center( + child: Text('Version mobile à implémenter'), + ); + }, + ), + ); + } + + void _showBugReportDialog(BuildContext context) { + final TextEditingController controller = TextEditingController(); + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text( + 'Signaler un bug', + style: GoogleFonts.merienda(), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: controller, + maxLines: 5, + decoration: InputDecoration( + hintText: 'Décrivez le problème rencontré...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Annuler', + style: GoogleFonts.merienda(), + ), + ), + TextButton( + onPressed: () async { + if (controller.text.trim().isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Veuillez décrire le problème', + style: GoogleFonts.merienda(), + ), + ), + ); + return; + } + + try { + await BugReportService.sendReport(controller.text); + if (context.mounted) { + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Rapport envoyé avec succès', + style: GoogleFonts.merienda(), + ), + ), + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Erreur lors de l\'envoi du rapport', + style: GoogleFonts.merienda(), + ), + ), + ); + } + } + }, + child: Text( + 'Envoyer', + style: GoogleFonts.merienda(), + ), + ), + ], + ), + ); + } + + Future _getImageDimensions() async { + final image = Image.asset('assets/images/river_logo_desktop.png'); + final completer = Completer(); + image.image.resolve(const ImageConfiguration()).addListener( + ImageStreamListener((info, _) { + completer.complete(ImageDimensions( + width: info.image.width.toDouble(), + height: info.image.height.toDouble(), + )); + }), + ); + return completer.future; + } +} + +class ImageDimensions { + final double width; + final double height; + + ImageDimensions({required this.width, required this.height}); +} + +// ─────────────────────────────────────────────────────────────── +// Lien du pied de page +// ─────────────────────────────────────────────────────────────── +class _FooterLink extends StatelessWidget { + final String text; + final VoidCallback onTap; + + const _FooterLink({ + required this.text, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + text, + style: GoogleFonts.merienda( + fontSize: 14, + color: Colors.black87, + decoration: TextDecoration.underline, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/parent_register_step1_screen.dart b/frontend/lib/screens/auth/parent_register_step1_screen.dart new file mode 100644 index 0000000..0463967 --- /dev/null +++ b/frontend/lib/screens/auth/parent_register_step1_screen.dart @@ -0,0 +1,226 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:math' as math; // Pour la rotation du chevron +import '../../models/user_registration_data.dart'; // Import du modèle de données +import '../../utils/data_generator.dart'; // Import du générateur de données +import '../../widgets/custom_app_text_field.dart'; // Import du widget CustomAppTextField +import '../../models/card_assets.dart'; // Import des enums de cartes + +class ParentRegisterStep1Screen extends StatefulWidget { + const ParentRegisterStep1Screen({super.key}); + + @override + State createState() => _ParentRegisterStep1ScreenState(); +} + +class _ParentRegisterStep1ScreenState extends State { + final _formKey = GlobalKey(); + late UserRegistrationData _registrationData; + + // Contrôleurs pour les champs (restauration CP et Ville) + final _lastNameController = TextEditingController(); + final _firstNameController = TextEditingController(); + final _phoneController = TextEditingController(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + final _addressController = TextEditingController(); // Rue seule + final _postalCodeController = TextEditingController(); // Restauré + final _cityController = TextEditingController(); // Restauré + + @override + void initState() { + super.initState(); + _registrationData = UserRegistrationData(); + _generateAndFillData(); + } + + void _generateAndFillData() { + final String genFirstName = DataGenerator.firstName(); + final String genLastName = DataGenerator.lastName(); + + // Utilisation des méthodes publiques de DataGenerator + _addressController.text = DataGenerator.address(); + _postalCodeController.text = DataGenerator.postalCode(); + _cityController.text = DataGenerator.city(); + + _firstNameController.text = genFirstName; + _lastNameController.text = genLastName; + _phoneController.text = DataGenerator.phone(); + _emailController.text = DataGenerator.email(genFirstName, genLastName); + _passwordController.text = DataGenerator.password(); + _confirmPasswordController.text = _passwordController.text; + } + + @override + void dispose() { + _lastNameController.dispose(); + _firstNameController.dispose(); + _phoneController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + _addressController.dispose(); + _postalCodeController.dispose(); + _cityController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + children: [ + // Fond papier + Positioned.fill( + child: Image.asset( + 'assets/images/paper2.png', + fit: BoxFit.cover, + repeat: ImageRepeat.repeat, + ), + ), + + // Contenu centré + Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Indicateur d'étape (à rendre dynamique) + Text( + 'Étape 1/5', + style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), + ), + const SizedBox(height: 10), + // Texte d'instruction + Text( + 'Informations du Parent Principal', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + + // Carte jaune contenant le formulaire + Container( + width: screenSize.width * 0.6, + padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50), + constraints: const BoxConstraints(minHeight: 570), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(CardColorHorizontal.peach.path), + fit: BoxFit.fill, + ), + ), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), + Expanded(flex: 1, child: const SizedBox()), // Espace de 4% + Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), + Expanded(flex: 1, child: const SizedBox()), // Espace de 4% + Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Créez votre mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) { + if (value == null || value.isEmpty) return 'Mot de passe requis'; + if (value.length < 6) return '6 caractères minimum'; + return null; + })), + Expanded(flex: 1, child: const SizedBox()), // Espace de 4% + Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmez le mot de passe', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: (value) { + if (value == null || value.isEmpty) return 'Confirmation requise'; + if (value != _passwordController.text) return 'Ne correspond pas'; + return null; + })), + ], + ), + const SizedBox(height: 20), + CustomAppTextField( + controller: _addressController, + labelText: 'Adresse (N° et Rue)', + hintText: 'Numéro et nom de votre rue', + style: CustomAppTextFieldStyle.beige, + fieldWidth: double.infinity, + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), + const SizedBox(width: 20), + Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + + // Chevron de navigation gauche (Retour) + Positioned( + top: screenSize.height / 2 - 20, // Centré verticalement + left: 40, + child: IconButton( + icon: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(math.pi), // Inverse horizontalement + child: Image.asset('assets/images/chevron_right.png', height: 40), + ), + onPressed: () => Navigator.pop(context), // Retour à l'écran de choix + tooltip: 'Retour', + ), + ), + + // Chevron de navigation droit (Suivant) + Positioned( + top: screenSize.height / 2 - 20, // Centré verticalement + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + _registrationData.updateParent1( + ParentData( + firstName: _firstNameController.text, + lastName: _lastNameController.text, + address: _addressController.text, // Rue + postalCode: _postalCodeController.text, // Ajout + city: _cityController.text, // Ajout + phone: _phoneController.text, + email: _emailController.text, + password: _passwordController.text, + ) + ); + Navigator.pushNamed(context, '/parent-register/step2', arguments: _registrationData); + } + }, + tooltip: 'Suivant', + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/parent_register_step2_screen.dart b/frontend/lib/screens/auth/parent_register_step2_screen.dart new file mode 100644 index 0000000..a7ecaf2 --- /dev/null +++ b/frontend/lib/screens/auth/parent_register_step2_screen.dart @@ -0,0 +1,255 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:math' as math; // Pour la rotation du chevron +import '../../models/user_registration_data.dart'; // Import du modèle +import '../../utils/data_generator.dart'; // Import du générateur +import '../../widgets/custom_app_text_field.dart'; // Import du widget +import '../../models/card_assets.dart'; // Import des enums de cartes + +class ParentRegisterStep2Screen extends StatefulWidget { + final UserRegistrationData registrationData; // Accepte les données de l'étape 1 + + const ParentRegisterStep2Screen({super.key, required this.registrationData}); + + @override + State createState() => _ParentRegisterStep2ScreenState(); +} + +class _ParentRegisterStep2ScreenState extends State { + final _formKey = GlobalKey(); + late UserRegistrationData _registrationData; // Copie locale pour modification + + bool _addParent2 = true; // Pour le test, on ajoute toujours le parent 2 + bool _sameAddressAsParent1 = false; // Peut être généré aléatoirement aussi + + // Contrôleurs pour les champs du parent 2 (restauration CP et Ville) + final _lastNameController = TextEditingController(); + final _firstNameController = TextEditingController(); + final _phoneController = TextEditingController(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + final _addressController = TextEditingController(); // Rue seule + final _postalCodeController = TextEditingController(); // Restauré + final _cityController = TextEditingController(); // Restauré + + @override + void initState() { + super.initState(); + _registrationData = widget.registrationData; // Récupère les données de l'étape 1 + if (_addParent2) { + _generateAndFillParent2Data(); + } + } + + void _generateAndFillParent2Data() { + final String genFirstName = DataGenerator.firstName(); + final String genLastName = DataGenerator.lastName(); + _firstNameController.text = genFirstName; + _lastNameController.text = genLastName; + _phoneController.text = DataGenerator.phone(); + _emailController.text = DataGenerator.email(genFirstName, genLastName); + _passwordController.text = DataGenerator.password(); + _confirmPasswordController.text = _passwordController.text; + + _sameAddressAsParent1 = DataGenerator.boolean(); + if (!_sameAddressAsParent1) { + // Générer adresse, CP, Ville séparément + _addressController.text = DataGenerator.address(); + _postalCodeController.text = DataGenerator.postalCode(); + _cityController.text = DataGenerator.city(); + } else { + // Vider les champs si même adresse (seront désactivés) + _addressController.clear(); + _postalCodeController.clear(); + _cityController.clear(); + } + } + + @override + void dispose() { + _lastNameController.dispose(); + _firstNameController.dispose(); + _phoneController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + _addressController.dispose(); + _postalCodeController.dispose(); + _cityController.dispose(); + super.dispose(); + } + + bool get _parent2FieldsEnabled => _addParent2; + bool get _addressFieldsEnabled => _addParent2 && !_sameAddressAsParent1; + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat), + ), + Center( + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Étape 2/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), + const SizedBox(height: 10), + Text( + 'Informations du Deuxième Parent (Optionnel)', + style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Container( + width: screenSize.width * 0.6, + padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50), + decoration: BoxDecoration( + image: DecorationImage(image: AssetImage(CardColorHorizontal.blue.path), fit: BoxFit.fill), + ), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + flex: 12, + child: Row(children: [ + const Icon(Icons.person_add_alt_1, size: 20), const SizedBox(width: 8), + Flexible(child: Text('Ajouter Parent 2 ?', style: GoogleFonts.merienda(fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis)), + const Spacer(), + Switch(value: _addParent2, onChanged: (val) => setState(() { + _addParent2 = val ?? false; + if (_addParent2) _generateAndFillParent2Data(); else _clearParent2Fields(); + }), activeColor: Theme.of(context).primaryColor), + ]), + ), + Expanded(flex: 1, child: const SizedBox()), + Expanded( + flex: 12, + child: Row(children: [ + Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey), + const SizedBox(width: 8), + Flexible(child: Text('Même Adresse ?', style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), overflow: TextOverflow.ellipsis)), + const Spacer(), + Switch(value: _sameAddressAsParent1, onChanged: _addParent2 ? (val) => setState(() { + _sameAddressAsParent1 = val ?? false; + if (_sameAddressAsParent1) { + _addressController.text = _registrationData.parent1.address; + _postalCodeController.text = _registrationData.parent1.postalCode; + _cityController.text = _registrationData.parent1.city; + } else { + _addressController.text = DataGenerator.address(); + _postalCodeController.text = DataGenerator.postalCode(); + _cityController.text = DataGenerator.city(); + } + }) : null, activeColor: Theme.of(context).primaryColor), + ]), + ), + ]), + const SizedBox(height: 25), + Row( + children: [ + Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Nom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), + Expanded(flex: 1, child: const SizedBox()), // Espace de 4% + Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Prénom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son téléphone', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), + Expanded(flex: 1, child: const SizedBox()), // Espace de 4% + Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son email', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), + ], + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded(flex: 12, child: CustomAppTextField(controller: _passwordController, labelText: 'Mot de passe', obscureText: true, hintText: 'Son mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v.length < 6 ? '6 car. min' : null)) : null)), + Expanded(flex: 1, child: const SizedBox()), // Espace de 4% + Expanded(flex: 12, child: CustomAppTextField(controller: _confirmPasswordController, labelText: 'Confirmation', obscureText: true, hintText: 'Confirmer mot de passe', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, validator: _addParent2 ? (v) => (v == null || v.isEmpty ? 'Requis' : (v != _passwordController.text ? 'Différent' : null)) : null)), + ], + ), + const SizedBox(height: 20), + CustomAppTextField(controller: _addressController, labelText: 'Adresse (N° et Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity), + const SizedBox(height: 20), + Row( + children: [ + Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), + const SizedBox(width: 20), + Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity)), + ], + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)), + onPressed: () => Navigator.pop(context), + tooltip: 'Retour', + ), + ), + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: () { + if (!_addParent2 || (_formKey.currentState?.validate() ?? false)) { + if (_addParent2) { + _registrationData.updateParent2( + ParentData( + firstName: _firstNameController.text, + lastName: _lastNameController.text, + address: _sameAddressAsParent1 ? _registrationData.parent1.address : _addressController.text, + postalCode: _sameAddressAsParent1 ? _registrationData.parent1.postalCode : _postalCodeController.text, + city: _sameAddressAsParent1 ? _registrationData.parent1.city : _cityController.text, + phone: _phoneController.text, + email: _emailController.text, + password: _passwordController.text, + ) + ); + } else { + _registrationData.updateParent2(null); + } + Navigator.pushNamed(context, '/parent-register/step3', arguments: _registrationData); + } + }, + tooltip: 'Suivant', + ), + ), + ], + ), + ); + } + + void _clearParent2Fields() { + _formKey.currentState?.reset(); + _lastNameController.clear(); _firstNameController.clear(); _phoneController.clear(); + _emailController.clear(); _passwordController.clear(); _confirmPasswordController.clear(); + _addressController.clear(); + _postalCodeController.clear(); + _cityController.clear(); + _sameAddressAsParent1 = false; + setState(() {}); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/parent_register_step3_screen.dart b/frontend/lib/screens/auth/parent_register_step3_screen.dart new file mode 100644 index 0000000..ac9daff --- /dev/null +++ b/frontend/lib/screens/auth/parent_register_step3_screen.dart @@ -0,0 +1,487 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:math' as math; // Pour la rotation du chevron +import 'package:flutter/gestures.dart'; // Pour PointerDeviceKind +import '../../widgets/hover_relief_widget.dart'; // Import du nouveau widget +import 'package:image_picker/image_picker.dart'; +// import 'package:image_cropper/image_cropper.dart'; // Supprimé +import 'dart:io' show File, Platform; // Ajout de Platform +import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb +import '../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField +import '../../widgets/app_custom_checkbox.dart'; // Import du nouveau widget Checkbox +import '../../models/user_registration_data.dart'; // Import du modèle de données +import '../../utils/data_generator.dart'; // Import du générateur +import '../../models/card_assets.dart'; // Import des enums de cartes + +// La classe _ChildFormData est supprimée car on utilise ChildData du modèle + +class ParentRegisterStep3Screen extends StatefulWidget { + final UserRegistrationData registrationData; // Accepte les données + + const ParentRegisterStep3Screen({super.key, required this.registrationData}); + + @override + State createState() => _ParentRegisterStep3ScreenState(); +} + +class _ParentRegisterStep3ScreenState extends State { + late UserRegistrationData _registrationData; // Stocke l'état complet + final ScrollController _scrollController = ScrollController(); // Pour le défilement horizontal + bool _isScrollable = false; + bool _showLeftFade = false; + bool _showRightFade = false; + static const double _fadeExtent = 0.05; // Pourcentage de fondu + + // Liste ordonnée des couleurs de cartes pour les enfants + static const List _childCardColors = [ + CardColorVertical.lavender, // Premier enfant toujours lavande + CardColorVertical.pink, + CardColorVertical.peach, + CardColorVertical.lime, + CardColorVertical.red, + CardColorVertical.green, + CardColorVertical.blue, + ]; + + // Garder une trace des couleurs déjà utilisées + final Set _usedColors = {}; + + // Utilisation de GlobalKey pour les cartes enfants si validation complexe future + // Map> _childFormKeys = {}; + + @override + void initState() { + super.initState(); + _registrationData = widget.registrationData; + // Initialiser les couleurs utilisées avec les enfants existants + for (var child in _registrationData.children) { + _usedColors.add(child.cardColor); + } + // S'il n'y a pas d'enfant, en ajouter un automatiquement avec des données générées + if (_registrationData.children.isEmpty) { + _addChild(); + } + _scrollController.addListener(_scrollListener); + WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener()); + } + + @override + void dispose() { + _scrollController.removeListener(_scrollListener); + _scrollController.dispose(); + super.dispose(); + } + + void _scrollListener() { + if (!_scrollController.hasClients) return; + final position = _scrollController.position; + final newIsScrollable = position.maxScrollExtent > 0.0; + final newShowLeftFade = newIsScrollable && position.pixels > (position.viewportDimension * _fadeExtent / 2); + final newShowRightFade = newIsScrollable && position.pixels < (position.maxScrollExtent - (position.viewportDimension * _fadeExtent / 2)); + if (newIsScrollable != _isScrollable || newShowLeftFade != _showLeftFade || newShowRightFade != _showRightFade) { + setState(() { + _isScrollable = newIsScrollable; + _showLeftFade = newShowLeftFade; + _showRightFade = newShowRightFade; + }); + } + } + + void _addChild() { + setState(() { + bool isUnborn = DataGenerator.boolean(); + + // Trouver la première couleur non utilisée + CardColorVertical cardColor = _childCardColors.firstWhere( + (color) => !_usedColors.contains(color), + orElse: () => _childCardColors[0], // Fallback sur la première couleur si toutes sont utilisées + ); + + final newChild = ChildData( + lastName: _registrationData.parent1.lastName, + firstName: DataGenerator.firstName(), + dob: DataGenerator.dob(isUnborn: isUnborn), + isUnbornChild: isUnborn, + photoConsent: DataGenerator.boolean(), + multipleBirth: DataGenerator.boolean(), + cardColor: cardColor, + ); + _registrationData.addChild(newChild); + _usedColors.add(cardColor); + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + _scrollListener(); + if (_scrollController.hasClients && _scrollController.position.maxScrollExtent > 0.0) { + _scrollController.animateTo(_scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut); + } + }); + } + + void _removeChild(int index) { + if (_registrationData.children.length > 1 && index >= 0 && index < _registrationData.children.length) { + setState(() { + // Ne pas retirer la couleur de _usedColors pour éviter sa réutilisation + _registrationData.children.removeAt(index); + }); + WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener()); + } + } + + Future _pickImage(int childIndex) async { + final ImagePicker picker = ImagePicker(); + try { + final XFile? pickedFile = await picker.pickImage( + source: ImageSource.gallery, imageQuality: 70, maxWidth: 1024, maxHeight: 1024); + if (pickedFile != null) { + setState(() { + if (childIndex < _registrationData.children.length) { + _registrationData.children[childIndex].imageFile = File(pickedFile.path); + } + }); + } + } catch (e) { print("Erreur image: $e"); } + } + + Future _selectDate(BuildContext context, int childIndex) async { + final ChildData currentChild = _registrationData.children[childIndex]; + final DateTime now = DateTime.now(); + DateTime initialDatePickerDate = now; + DateTime firstDatePickerDate = DateTime(1980); DateTime lastDatePickerDate = now; + + if (currentChild.isUnbornChild) { + firstDatePickerDate = now; lastDatePickerDate = now.add(const Duration(days: 300)); + if (currentChild.dob.isNotEmpty) { + try { + List parts = currentChild.dob.split('/'); + DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}"); + if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) { + initialDatePickerDate = parsedDate; + } + } catch (e) {} + } + } else { + if (currentChild.dob.isNotEmpty) { + try { + List parts = currentChild.dob.split('/'); + DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}"); + if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) { + initialDatePickerDate = parsedDate; + } + } catch (e) {} + } + } + final DateTime? picked = await showDatePicker( + context: context, initialDate: initialDatePickerDate, firstDate: firstDatePickerDate, + lastDate: lastDatePickerDate, locale: const Locale('fr', 'FR'), + ); + if (picked != null) { + setState(() { + currentChild.dob = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}"; + }); + } + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + return Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat), + ), + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Étape 3/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), + const SizedBox(height: 10), + Text( + 'Informations Enfants', + style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 150.0), + child: SizedBox( + height: 684.0, + child: ShaderMask( + shaderCallback: (Rect bounds) { + final Color leftFade = (_isScrollable && _showLeftFade) ? Colors.transparent : Colors.black; + final Color rightFade = (_isScrollable && _showRightFade) ? Colors.transparent : Colors.black; + if (!_isScrollable) { return LinearGradient(colors: const [Colors.black, Colors.black, Colors.black, Colors.black], stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0],).createShader(bounds); } + return LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [ leftFade, Colors.black, Colors.black, rightFade ], stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0], ).createShader(bounds); + }, + blendMode: BlendMode.dstIn, + child: Scrollbar( + controller: _scrollController, + thumbVisibility: true, + child: ListView.builder( + controller: _scrollController, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 20.0), + itemCount: _registrationData.children.length + 1, + itemBuilder: (context, index) { + if (index < _registrationData.children.length) { + // Carte Enfant + return Padding( + padding: const EdgeInsets.only(right: 20.0), + child: _ChildCardWidget( + key: ValueKey(_registrationData.children[index].hashCode), // Utiliser une clé basée sur les données + childData: _registrationData.children[index], + childIndex: index, + onPickImage: () => _pickImage(index), + onDateSelect: () => _selectDate(context, index), + onFirstNameChanged: (value) => setState(() => _registrationData.children[index].firstName = value), + onLastNameChanged: (value) => setState(() => _registrationData.children[index].lastName = value), + onTogglePhotoConsent: (newValue) => setState(() => _registrationData.children[index].photoConsent = newValue), + onToggleMultipleBirth: (newValue) => setState(() => _registrationData.children[index].multipleBirth = newValue), + onToggleIsUnborn: (newValue) => setState(() { + _registrationData.children[index].isUnbornChild = newValue; + // Générer une nouvelle date si on change le statut + _registrationData.children[index].dob = DataGenerator.dob(isUnborn: newValue); + }), + onRemove: () => _removeChild(index), + canBeRemoved: _registrationData.children.length > 1, + ), + ); + } else { + // Bouton Ajouter + return Center( + child: HoverReliefWidget( + onPressed: _addChild, + borderRadius: BorderRadius.circular(15), + child: Image.asset('assets/images/plus.png', height: 80, width: 80), + ), + ); + } + }, + ), + ), + ), + ), + ), + const SizedBox(height: 20), + ], + ), + ), + // Chevrons de navigation + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)), + onPressed: () => Navigator.pop(context), + tooltip: 'Retour', + ), + ), + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: () { + // TODO: Validation (si nécessaire) + Navigator.pushNamed(context, '/parent-register/step4', arguments: _registrationData); + }, + tooltip: 'Suivant', + ), + ), + ], + ), + ); + } +} + +// Widget pour la carte enfant (adapté pour prendre ChildData et des callbacks) +class _ChildCardWidget extends StatefulWidget { // Transformé en StatefulWidget pour gérer les contrôleurs internes + final ChildData childData; + final int childIndex; + final VoidCallback onPickImage; + final VoidCallback onDateSelect; + final ValueChanged onFirstNameChanged; + final ValueChanged onLastNameChanged; + final ValueChanged onTogglePhotoConsent; + final ValueChanged onToggleMultipleBirth; + final ValueChanged onToggleIsUnborn; + final VoidCallback onRemove; + final bool canBeRemoved; + + const _ChildCardWidget({ + required Key key, + required this.childData, + required this.childIndex, + required this.onPickImage, + required this.onDateSelect, + required this.onFirstNameChanged, + required this.onLastNameChanged, + required this.onTogglePhotoConsent, + required this.onToggleMultipleBirth, + required this.onToggleIsUnborn, + required this.onRemove, + required this.canBeRemoved, + }) : super(key: key); + + @override + State<_ChildCardWidget> createState() => _ChildCardWidgetState(); +} + +class _ChildCardWidgetState extends State<_ChildCardWidget> { + late TextEditingController _firstNameController; + late TextEditingController _lastNameController; + late TextEditingController _dobController; + + @override + void initState() { + super.initState(); + // Initialiser les contrôleurs avec les données du widget + _firstNameController = TextEditingController(text: widget.childData.firstName); + _lastNameController = TextEditingController(text: widget.childData.lastName); + _dobController = TextEditingController(text: widget.childData.dob); + + // Ajouter des listeners pour mettre à jour les données sources via les callbacks + _firstNameController.addListener(() => widget.onFirstNameChanged(_firstNameController.text)); + _lastNameController.addListener(() => widget.onLastNameChanged(_lastNameController.text)); + // Pour dob, la mise à jour se fait via _selectDate, pas besoin de listener ici + } + + @override + void didUpdateWidget(covariant _ChildCardWidget oldWidget) { + super.didUpdateWidget(oldWidget); + // Mettre à jour les contrôleurs si les données externes changent + // (peut arriver si on recharge l'état global) + if (widget.childData.firstName != _firstNameController.text) { + _firstNameController.text = widget.childData.firstName; + } + if (widget.childData.lastName != _lastNameController.text) { + _lastNameController.text = widget.childData.lastName; + } + if (widget.childData.dob != _dobController.text) { + _dobController.text = widget.childData.dob; + } + } + + @override + void dispose() { + _firstNameController.dispose(); + _lastNameController.dispose(); + _dobController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final File? currentChildImage = widget.childData.imageFile; + // Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond + final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender + ? Colors.purple.shade200 + : (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200); // Placeholder pour autres couleurs + final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90); + final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130); + + return Container( + width: 345.0 * 1.1, // 379.5 + height: 570.0 * 1.2, // 684.0 + padding: const EdgeInsets.all(22.0 * 1.1), // 24.2 + decoration: BoxDecoration( + image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.cover), + borderRadius: BorderRadius.circular(20 * 1.1), // 22 + ), + child: Stack( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + HoverReliefWidget( + onPressed: widget.onPickImage, + borderRadius: BorderRadius.circular(10), + initialShadowColor: initialPhotoShadow, + hoverShadowColor: hoverPhotoShadow, + child: SizedBox( + height: 200.0, + width: 200.0, + child: Center( + child: Padding( + padding: const EdgeInsets.all(5.0 * 1.1), // 5.5 + child: currentChildImage != null + ? ClipRRect(borderRadius: BorderRadius.circular(10 * 1.1), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover)) + : Image.asset('assets/images/photo.png', fit: BoxFit.contain), + ), + ), + ), + ), + const SizedBox(height: 12.0 * 1.1), // Augmenté pour plus d'espace après la photo + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)), + Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor), + ], + ), + const SizedBox(height: 9.0 * 1.1), // 9.9 + CustomAppTextField( + controller: _firstNameController, + labelText: 'Prénom', + hintText: 'Facultatif si à naître', + isRequired: !widget.childData.isUnbornChild, + fieldHeight: 55.0 * 1.1, // 60.5 + ), + const SizedBox(height: 6.0 * 1.1), // 6.6 + CustomAppTextField( + controller: _lastNameController, + labelText: 'Nom', + hintText: 'Nom de l\'enfant', + enabled: true, + fieldHeight: 55.0 * 1.1, // 60.5 + ), + const SizedBox(height: 9.0 * 1.1), // 9.9 + CustomAppTextField( + controller: _dobController, + labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance', + hintText: 'JJ/MM/AAAA', + readOnly: true, + onTap: widget.onDateSelect, + suffixIcon: Icons.calendar_today, + fieldHeight: 55.0 * 1.1, // 60.5 + ), + const SizedBox(height: 11.0 * 1.1), // 12.1 + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppCustomCheckbox( + label: 'Consentement photo', + value: widget.childData.photoConsent, + onChanged: widget.onTogglePhotoConsent, + checkboxSize: 22.0 * 1.1, // 24.2 + ), + const SizedBox(height: 6.0 * 1.1), // 6.6 + AppCustomCheckbox( + label: 'Naissance multiple', + value: widget.childData.multipleBirth, + onChanged: widget.onToggleMultipleBirth, + checkboxSize: 22.0 * 1.1, // 24.2 + ), + ], + ), + ], + ), + if (widget.canBeRemoved) + Positioned( + top: -5, right: -5, + child: InkWell( + onTap: widget.onRemove, + customBorder: const CircleBorder(), + child: Image.asset( + 'images/red_cross2.png', + width: 36, + height: 36, + fit: BoxFit.contain, + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/parent_register_step4_screen.dart b/frontend/lib/screens/auth/parent_register_step4_screen.dart new file mode 100644 index 0000000..62ae003 --- /dev/null +++ b/frontend/lib/screens/auth/parent_register_step4_screen.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:p_tits_pas/widgets/custom_decorated_text_field.dart'; // Import du nouveau widget +import 'dart:math' as math; // Pour la rotation du chevron +import 'package:p_tits_pas/widgets/app_custom_checkbox.dart'; // Import de la checkbox personnalisée +// import 'package:p_tits_pas/models/placeholder_registration_data.dart'; // Remplacé +import '../../models/user_registration_data.dart'; // Import du vrai modèle +import '../../utils/data_generator.dart'; // Import du générateur +import '../../models/card_assets.dart'; // Import des enums de cartes + +class ParentRegisterStep4Screen extends StatefulWidget { + final UserRegistrationData registrationData; // Accepte les données + + const ParentRegisterStep4Screen({super.key, required this.registrationData}); + + @override + State createState() => _ParentRegisterStep4ScreenState(); +} + +class _ParentRegisterStep4ScreenState extends State { + late UserRegistrationData _registrationData; // État local + final _motivationController = TextEditingController(); + bool _cguAccepted = true; // Pour le test, CGU acceptées par défaut + + @override + void initState() { + super.initState(); + _registrationData = widget.registrationData; + _motivationController.text = DataGenerator.motivation(); // Générer la motivation + } + + @override + void dispose() { + _motivationController.dispose(); + super.dispose(); + } + + void _showCGUModal() { + // Un long texte Lorem Ipsum pour simuler les CGU + const String loremIpsumText = ''' +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit. + +Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna. + +Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet. + +Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit. + +Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna. Etiam et felis dolor. + +Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. + +Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. +'''; + + showDialog( + context: context, + barrierDismissible: false, // L'utilisateur doit utiliser le bouton + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text( + 'Conditions Générales d\'Utilisation', + style: GoogleFonts.merienda(fontWeight: FontWeight.bold), + ), + content: SizedBox( + width: MediaQuery.of(dialogContext).size.width * 0.7, // 70% de la largeur de l'écran + height: MediaQuery.of(dialogContext).size.height * 0.6, // 60% de la hauteur de l'écran + child: SingleChildScrollView( + child: Text( + loremIpsumText, + style: GoogleFonts.merienda(fontSize: 13), + textAlign: TextAlign.justify, + ), + ), + ), + actionsPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), + actionsAlignment: MainAxisAlignment.center, + actions: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(dialogContext).primaryColor, + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15), + ), + child: Text( + 'Valider et Accepter', + style: GoogleFonts.merienda(fontSize: 15, color: Colors.white, fontWeight: FontWeight.bold), + ), + onPressed: () { + Navigator.of(dialogContext).pop(); // Ferme la modale + setState(() { + _cguAccepted = true; // Met à jour l'état + }); + }, + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final cardWidth = screenSize.width * 0.6; // Largeur de la carte (60% de l'écran) + final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0) + final cardHeight = cardWidth / imageAspectRatio; + + return Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat), + ), + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Étape 4/5', + style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54), + ), + const SizedBox(height: 20), + Text( + 'Motivation de votre demande', + style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), + textAlign: TextAlign.center, + ), + const SizedBox(height: 30), + Container( + width: cardWidth, + height: cardHeight, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(CardColorHorizontal.green.path), + fit: BoxFit.fill, + ), + ), + child: Padding( + padding: const EdgeInsets.all(40.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: CustomDecoratedTextField( + controller: _motivationController, + hintText: 'Écrivez ici pour motiver votre demande...', + fieldHeight: cardHeight * 0.6, + maxLines: 10, + expandDynamically: true, + fontSize: 18.0, + ), + ), + const SizedBox(height: 20), + GestureDetector( + onTap: () { + if (!_cguAccepted) { + _showCGUModal(); + } + }, + child: AppCustomCheckbox( + label: 'J\'accepte les conditions générales d\'utilisation', + value: _cguAccepted, + onChanged: (newValue) { + if (!_cguAccepted) { + _showCGUModal(); + } else { + setState(() => _cguAccepted = false); + } + }, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + // Chevrons de navigation + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)), + onPressed: () => Navigator.pop(context), + tooltip: 'Retour', + ), + ), + Positioned( + top: screenSize.height / 2 - 20, + right: 40, + child: IconButton( + icon: Image.asset('assets/images/chevron_right.png', height: 40), + onPressed: _cguAccepted + ? () { + _registrationData.updateMotivation(_motivationController.text); + _registrationData.acceptCGU(); + + Navigator.pushNamed( + context, + '/parent-register/step5', + arguments: _registrationData + ); + } + : null, + tooltip: 'Suivant', + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/parent_register_step5_screen.dart b/frontend/lib/screens/auth/parent_register_step5_screen.dart new file mode 100644 index 0000000..24a914c --- /dev/null +++ b/frontend/lib/screens/auth/parent_register_step5_screen.dart @@ -0,0 +1,464 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import '../../models/user_registration_data.dart'; // Utilisation du vrai modèle +import '../../widgets/image_button.dart'; // Import du ImageButton +import '../../models/card_assets.dart'; // Import des enums de cartes +import 'package:flutter/foundation.dart' show kIsWeb; +import '../../widgets/custom_decorated_text_field.dart'; // Import du CustomDecoratedTextField + +// Nouvelle méthode helper pour afficher un champ de type "lecture seule" stylisé +Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) { + const FontWeight labelFontWeight = FontWeight.w600; + + // Ne pas afficher le label si labelFontSize est 0 ou si label est vide + bool showLabel = label.isNotEmpty && labelFontSize > 0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showLabel) + Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)), + if (showLabel) + const SizedBox(height: 4), + // Utiliser Expanded si multiLine et pas de hauteur fixe, sinon Container + multiLine && fieldHeight == null + ? Expanded( + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), + decoration: BoxDecoration( + image: const DecorationImage( + image: AssetImage('assets/images/input_field_bg.png'), + fit: BoxFit.fill, + ), + ), + child: SingleChildScrollView( // Pour le défilement si le texte dépasse + child: Text( + value.isNotEmpty ? value : '-', + style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0), // Garder une taille de texte par défaut si label caché + maxLines: null, // Permettre un nombre illimité de lignes + ), + ), + ), + ) + : Container( + width: double.infinity, + height: multiLine ? null : fieldHeight, + constraints: multiLine ? BoxConstraints(minHeight: fieldHeight) : null, + padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0), + decoration: BoxDecoration( + image: const DecorationImage( + image: AssetImage('assets/images/input_field_bg.png'), + fit: BoxFit.fill, + ), + ), + child: Text( + value.isNotEmpty ? value : '-', + style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0), + maxLines: multiLine ? null : 1, + overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis, + ), + ), + ], + ); +} + +class ParentRegisterStep5Screen extends StatelessWidget { + final UserRegistrationData registrationData; + + const ParentRegisterStep5Screen({super.key, required this.registrationData}); + + // Méthode pour construire la carte Parent 1 + Widget _buildParent1Card(BuildContext context, ParentData data) { + const double verticalSpacing = 28.0; // Espacement vertical augmenté + const double labelFontSize = 22.0; // Taille de label augmentée + + List details = [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)), + const SizedBox(width: 20), + Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)), + ], + ), + const SizedBox(height: verticalSpacing), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)), + const SizedBox(width: 20), + Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)), + ], + ), + const SizedBox(height: verticalSpacing), + _buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize), + ]; + return _SummaryCard( + backgroundImagePath: CardColorHorizontal.peach.path, + title: 'Parent Principal', + content: details, + onEdit: () => Navigator.of(context).pushNamed('/parent-register/step1', arguments: registrationData), + ); + } + + // Méthode pour construire la carte Parent 2 + Widget _buildParent2Card(BuildContext context, ParentData data) { + const double verticalSpacing = 28.0; + const double labelFontSize = 22.0; + List details = [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)), + const SizedBox(width: 20), + Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)), + ], + ), + const SizedBox(height: verticalSpacing), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)), + const SizedBox(width: 20), + Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)), + ], + ), + const SizedBox(height: verticalSpacing), + _buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize), + ]; + return _SummaryCard( + backgroundImagePath: CardColorHorizontal.blue.path, + title: 'Deuxième Parent', + content: details, + onEdit: () => Navigator.of(context).pushNamed('/parent-register/step2', arguments: registrationData), + ); + } + + // Méthode pour construire les cartes Enfants + List _buildChildrenCards(BuildContext context, List children) { + return children.asMap().entries.map((entry) { + int index = entry.key; + ChildData child = entry.value; + + CardColorHorizontal cardColorHorizontal = CardColorHorizontal.values.firstWhere( + (e) => e.name == child.cardColor.name, + orElse: () => CardColorHorizontal.lavender, + ); + + return Padding( + padding: const EdgeInsets.only(bottom: 20.0), + child: Stack( + children: [ + AspectRatio( + aspectRatio: 2.0, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(cardColorHorizontal.path), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(15), + ), + child: Column( + children: [ + // Titre centré dans la carte + Row( + children: [ + Expanded( + child: Text( + 'Enfant ${index + 1}' + (child.isUnbornChild ? ' (à naître)' : ''), + style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600), + textAlign: TextAlign.center, + ), + ), + IconButton( + icon: const Icon(Icons.edit, color: Colors.black54, size: 28), + onPressed: () { + Navigator.of(context).pushNamed( + '/parent-register/step3', + arguments: registrationData, + ); + }, + tooltip: 'Modifier', + ), + ], + ), + const SizedBox(height: 18), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // IMAGE SANS CADRE BLANC, PREND LA HAUTEUR + Expanded( + flex: 1, + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(18), + child: AspectRatio( + aspectRatio: 1, + child: (child.imageFile != null) + ? (kIsWeb + ? Image.network(child.imageFile!.path, fit: BoxFit.cover) + : Image.file(child.imageFile!, fit: BoxFit.cover)) + : Image.asset('assets/images/photo.png', fit: BoxFit.contain), + ), + ), + ), + ), + const SizedBox(width: 32), + // INFOS À DROITE (2/3) + Expanded( + flex: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildDisplayFieldValue(context, 'Prénom :', child.firstName, labelFontSize: 22.0), + const SizedBox(height: 12), + _buildDisplayFieldValue(context, 'Nom :', child.lastName, labelFontSize: 22.0), + const SizedBox(height: 12), + _buildDisplayFieldValue(context, child.isUnbornChild ? 'Date de naissance :' : 'Date de naissance :', child.dob, labelFontSize: 22.0), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 18), + // Ligne des consentements + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + Checkbox( + value: child.photoConsent, + onChanged: null, + ), + Text('Consentement photo', style: GoogleFonts.merienda(fontSize: 16)), + ], + ), + const SizedBox(width: 32), + Row( + children: [ + Checkbox( + value: child.multipleBirth, + onChanged: null, + ), + Text('Naissance multiple', style: GoogleFonts.merienda(fontSize: 16)), + ], + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + }).toList(); + } + + // Méthode pour construire la carte Motivation + Widget _buildMotivationCard(BuildContext context, String motivation) { + return _SummaryCard( + backgroundImagePath: CardColorHorizontal.green.path, + title: 'Votre Motivation', + content: [ + Expanded( + child: CustomDecoratedTextField( + controller: TextEditingController(text: motivation), + hintText: 'Aucune motivation renseignée.', + fieldHeight: 200, + maxLines: 10, + expandDynamically: true, + readOnly: true, + fontSize: 18.0, + ), + ), + ], + onEdit: () => Navigator.of(context).pushNamed('/parent-register/step4', arguments: registrationData), + ); + } + + // Helper pour afficher une ligne de détail (police et agencement amélioré) + Widget _buildDetailRow(String label, String value) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "$label: ", + style: GoogleFonts.merienda(fontSize: 18, fontWeight: FontWeight.w600), + ), + Expanded( + child: Text( + value.isNotEmpty ? value : '-', + style: GoogleFonts.merienda(fontSize: 18), + softWrap: true, + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran) + final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0) + final cardHeight = cardWidth / imageAspectRatio; + + return Scaffold( + body: Stack( + children: [ + Positioned.fill( + child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY), + ), + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 40.0), // Padding horizontal supprimé ici + child: Padding( // Ajout du Padding horizontal externe + padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('Étape 5/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)), + const SizedBox(height: 20), + Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center), + const SizedBox(height: 30), + + _buildParent1Card(context, registrationData.parent1), + const SizedBox(height: 20), + if (registrationData.parent2 != null) ...[ + _buildParent2Card(context, registrationData.parent2!), + const SizedBox(height: 20), + ], + ..._buildChildrenCards(context, registrationData.children), + _buildMotivationCard(context, registrationData.motivationText), + const SizedBox(height: 40), + ImageButton( + bg: 'assets/images/btn_green.png', + text: 'Soumettre ma demande', + textColor: const Color(0xFF2D6A4F), + width: 350, + height: 50, + fontSize: 18, + onPressed: () { + print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}"); + _showConfirmationModal(context); + }, + ), + ], + ), + ), + ), + ), + Positioned( + top: screenSize.height / 2 - 20, + left: 40, + child: IconButton( + icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)), + onPressed: () => Navigator.pop(context), // Retour à l'étape 4 + tooltip: 'Retour', + ), + ), + ], + ), + ); + } + + void _showConfirmationModal(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text( + 'Demande enregistrée', + style: GoogleFonts.merienda(fontWeight: FontWeight.bold), + ), + content: Text( + 'Votre dossier a bien été pris en compte. Un gestionnaire le validera bientôt.', + style: GoogleFonts.merienda(fontSize: 14), + ), + actions: [ + TextButton( + child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)), + onPressed: () { + Navigator.of(dialogContext).pop(); // Ferme la modale + // TODO: Naviguer vers l'écran de connexion ou tableau de bord + Navigator.of(context).pushNamedAndRemoveUntil('/login', (Route route) => false); + }, + ), + ], + ); + }, + ); + } +} + +// Widget générique _SummaryCard (ajusté) +class _SummaryCard extends StatelessWidget { + final String backgroundImagePath; + final String title; + final List content; + final VoidCallback onEdit; + + const _SummaryCard({ + super.key, + required this.backgroundImagePath, + required this.title, + required this.content, + required this.onEdit, + }); + + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: 2.0, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(backgroundImagePath), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(15), + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + title, + style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600), + textAlign: TextAlign.center, + ), + ), + IconButton( + icon: const Icon(Icons.edit, color: Colors.black54, size: 28), + onPressed: onEdit, + tooltip: 'Modifier', + ), + ], + ), + const SizedBox(height: 18), + Expanded( + child: Column( + children: content, + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/auth/register_choice_screen.dart b/frontend/lib/screens/auth/register_choice_screen.dart new file mode 100644 index 0000000..0b6bd8a --- /dev/null +++ b/frontend/lib/screens/auth/register_choice_screen.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'dart:math' as math; // Pour la rotation du chevron +import '../../widgets/hover_relief_widget.dart'; // Import du widget générique +import '../../models/card_assets.dart'; // Import des enums de cartes + +class RegisterChoiceScreen extends StatelessWidget { + const RegisterChoiceScreen({super.key}); + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + + return Scaffold( + body: Stack( + children: [ + // Fond papier + Positioned.fill( + child: Image.asset( + 'assets/images/paper2.png', + fit: BoxFit.cover, + repeat: ImageRepeat.repeat, + ), + ), + + // Bouton Retour (chevron gauche) + Positioned( + top: 40, + left: 40, + child: IconButton( + icon: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(math.pi), + child: Image.asset('assets/images/chevron_right.png', height: 40), + ), + onPressed: () => Navigator.pop(context), + tooltip: 'Retour', + ), + ), + + // Contenu principal en Row (Gauche / Droite) + Padding( + padding: EdgeInsets.symmetric(horizontal: screenSize.width * 0.05), + child: Row( + children: [ + // Partie Gauche: Texte d'instruction centré + Expanded( + flex: 1, + child: Center( + child: Text( + 'Veuillez choisir votre\ntype de compte :', + style: GoogleFonts.merienda( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Colors.black87, + height: 1.5, + ), + textAlign: TextAlign.center, + ), + ), + ), + // Espace entre les deux parties + SizedBox(width: screenSize.width * 0.05), + + // Partie Droite: Carte rose avec les boutons + Expanded( + flex: 1, + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: screenSize.height * 0.78, // Augmenté pour éviter l'overflow + ), + child: AspectRatio( + aspectRatio: 2 / 3, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(CardColorVertical.pink.path), + fit: BoxFit.fill, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // Bouton "Parents" avec HoverReliefWidget appliqué uniquement à l'image + _buildChoiceButton( + context: context, + iconPath: 'assets/images/icon_parents.png', + label: 'Parents', + onPressed: () { + Navigator.pushNamed(context, '/parent-register/step1'); + }, + ), + // Bouton "Assistante Maternelle" avec HoverReliefWidget appliqué uniquement à l'image + _buildChoiceButton( + context: context, + iconPath: 'assets/images/icon_assmat.png', + label: 'Assistante Maternelle', + onPressed: () { + // TODO: Naviguer vers l'écran d'inscription assmat + print('Choix: Assistante Maternelle'); + }, + ), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +// Nouvelle méthode helper pour construire les boutons de choix +Widget _buildChoiceButton({ + required BuildContext context, + required String iconPath, + required String label, + required VoidCallback onPressed, +}) { + // TODO: Déterminer la couleur de base de card_rose.png et ajuster ces couleurs d'ombre + final Color baseRoseColor = Colors.pink.shade300; // Placeholder + final Color initialShadow = baseRoseColor.withAlpha(90); // Rose plus foncé et transparent pour l'ombre initiale + final Color hoverShadow = baseRoseColor.withAlpha(130); // Rose encore plus foncé pour l'ombre au survol + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + HoverReliefWidget( + onPressed: onPressed, + borderRadius: BorderRadius.circular(15.0), + initialShadowColor: initialShadow, // Ombre rose initiale + hoverShadowColor: hoverShadow, // Ombre rose au survol + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Image.asset(iconPath, height: 140), + ), + ), + const SizedBox(height: 15), + Text( + label, + style: GoogleFonts.merienda( + fontSize: 26, + fontWeight: FontWeight.w600, + color: Colors.black.withOpacity(0.85), + ), + textAlign: TextAlign.center, + ), + ], + ); +} + +// --- La classe HoverChoiceButton peut maintenant être supprimée si elle n'est plus utilisée ailleurs --- +// class HoverChoiceButton extends StatefulWidget { ... } +// class _HoverChoiceButtonState extends State { ... } \ No newline at end of file diff --git a/frontend/lib/screens/home/home_screen.dart b/frontend/lib/screens/home/home_screen.dart new file mode 100644 index 0000000..9a0c6ba --- /dev/null +++ b/frontend/lib/screens/home/home_screen.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Accueil'), + ), + body: const Center( + child: Text('Bienvenue sur P\'titsPas !'), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/legal/legal_page.dart b/frontend/lib/screens/legal/legal_page.dart new file mode 100644 index 0000000..fd3b5a6 --- /dev/null +++ b/frontend/lib/screens/legal/legal_page.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class LegalPage extends StatelessWidget { + const LegalPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + 'Mentions légales', + style: GoogleFonts.merienda(), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Éditeur', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + 'P\'titsPas est une application développée pour les collectivités locales.', + style: GoogleFonts.merienda(), + ), + const SizedBox(height: 32), + Text( + 'Hébergeur', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + 'Les données sont hébergées sur des serveurs sécurisés en France.', + style: GoogleFonts.merienda(), + ), + const SizedBox(height: 32), + Text( + 'Responsable du traitement', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + 'Le responsable du traitement des données est la collectivité locale utilisatrice de l\'application.', + style: GoogleFonts.merienda(), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/legal/privacy_page.dart b/frontend/lib/screens/legal/privacy_page.dart new file mode 100644 index 0000000..377b91a --- /dev/null +++ b/frontend/lib/screens/legal/privacy_page.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class PrivacyPage extends StatelessWidget { + const PrivacyPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + 'Politique de confidentialité', + style: GoogleFonts.merienda(), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Protection des données personnelles', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + 'P\'titsPas s\'engage à protéger vos données personnelles conformément au RGPD.', + style: GoogleFonts.merienda(), + ), + const SizedBox(height: 32), + Text( + 'Données collectées', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + 'Les données collectées sont nécessaires au bon fonctionnement de l\'application et à la gestion des contrats de garde d\'enfants.', + style: GoogleFonts.merienda(), + ), + const SizedBox(height: 32), + Text( + 'Vos droits', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + 'Vous disposez d\'un droit d\'accès, de rectification, d\'effacement et de portabilité de vos données.', + style: GoogleFonts.merienda(), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/services/auth_service.dart b/frontend/lib/services/auth_service.dart new file mode 100644 index 0000000..5234c8e --- /dev/null +++ b/frontend/lib/services/auth_service.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/user.dart'; + +class AuthService { + static const String _usersKey = 'users'; + static const String _parentsKey = 'parents'; + static const String _childrenKey = 'children'; + + // Méthode pour se connecter (mode démonstration) + static Future login(String email, String password) async { + await Future.delayed(const Duration(seconds: 1)); // Simule un délai de traitement + throw Exception('Mode démonstration - Connexion désactivée'); + } + + // Méthode pour s'inscrire (mode démonstration) + static Future register({ + required String email, + required String password, + required String firstName, + required String lastName, + required String role, + }) async { + await Future.delayed(const Duration(seconds: 1)); // Simule un délai de traitement + throw Exception('Mode démonstration - Inscription désactivée'); + } + + // Méthode pour se déconnecter (mode démonstration) + static Future logout() async { + // Ne fait rien en mode démonstration + } + + // Méthode pour vérifier si l'utilisateur est connecté (mode démonstration) + static Future isLoggedIn() async { + return false; // Toujours non connecté en mode démonstration + } + + // Méthode pour récupérer l'utilisateur connecté (mode démonstration) + static Future getCurrentUser() async { + return null; // Aucun utilisateur en mode démonstration + } +} \ No newline at end of file diff --git a/frontend/lib/services/bug_report_service.dart b/frontend/lib/services/bug_report_service.dart new file mode 100644 index 0000000..7dc9af4 --- /dev/null +++ b/frontend/lib/services/bug_report_service.dart @@ -0,0 +1,28 @@ +import 'package:http/http.dart' as http; +import 'dart:convert'; + +class BugReportService { + static const String _apiUrl = 'https://api.supernounou.local/bug-reports'; + + static Future sendReport(String description) async { + try { + final response = await http.post( + Uri.parse(_apiUrl), + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + 'description': description, + 'timestamp': DateTime.now().toIso8601String(), + 'platform': 'web', // TODO: Ajouter la détection de la plateforme + }), + ); + + if (response.statusCode != 200) { + throw Exception('Erreur lors de l\'envoi du rapport'); + } + } catch (e) { + rethrow; + } + } +} \ No newline at end of file diff --git a/frontend/lib/utils/data_generator.dart b/frontend/lib/utils/data_generator.dart new file mode 100644 index 0000000..a6f9077 --- /dev/null +++ b/frontend/lib/utils/data_generator.dart @@ -0,0 +1,65 @@ +import 'dart:math'; + +class DataGenerator { + static final Random _random = Random(); + + static final List _firstNames = [ + 'Alice', 'Bob', 'Charlie', 'David', 'Eva', 'Félix', 'Gabrielle', 'Hugo', 'Inès', 'Jules', + 'Léa', 'Manon', 'Nathan', 'Oscar', 'Pauline', 'Quentin', 'Raphaël', 'Sophie', 'Théo', 'Victoire' + ]; + + static final List _lastNames = [ + 'Martin', 'Bernard', 'Dubois', 'Thomas', 'Robert', 'Richard', 'Petit', 'Durand', 'Leroy', 'Moreau', + 'Simon', 'Laurent', 'Lefebvre', 'Michel', 'Garcia', 'David', 'Bertrand', 'Roux', 'Vincent', 'Fournier' + ]; + + static final List _addressSuffixes = [ + 'Rue de la Paix', 'Boulevard des Rêves', 'Avenue du Soleil', 'Place des Étoiles', 'Chemin des Champs' + ]; + + static final List _motivationSnippets = [ + 'Nous cherchons une personne de confiance.', + 'Nos horaires sont atypiques.', + 'Notre enfant est plein de vie.', + 'Nous souhaitons une garde à temps plein.', + 'Une adaptation en douceur est primordiale pour nous.', + 'Nous avons hâte de vous rencontrer.', + 'La pédagogie Montessori nous intéresse.' + ]; + + static String firstName() => _firstNames[_random.nextInt(_firstNames.length)]; + static String lastName() => _lastNames[_random.nextInt(_lastNames.length)]; + static String address() => "${_random.nextInt(100) + 1} ${_addressSuffixes[_random.nextInt(_addressSuffixes.length)]}"; + static String postalCode() => "750${_random.nextInt(10)}${_random.nextInt(10)}"; + static String city() => "Paris"; + static String phone() => "06${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}${_random.nextInt(10)}"; + static String email(String firstName, String lastName) => "${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com"; + static String password() => "password123"; // Simple pour le test + + static String dob({bool isUnborn = false}) { + final now = DateTime.now(); + if (isUnborn) { + final provisionalDate = now.add(Duration(days: _random.nextInt(180) + 30)); // Entre 1 et 7 mois dans le futur + return "${provisionalDate.day.toString().padLeft(2, '0')}/${provisionalDate.month.toString().padLeft(2, '0')}/${provisionalDate.year}"; + } else { + final birthYear = now.year - _random.nextInt(3); // Enfants de 0 à 2 ans + final birthMonth = _random.nextInt(12) + 1; + final birthDay = _random.nextInt(28) + 1; // Simple, évite les pbs de jours/mois + return "${birthDay.toString().padLeft(2, '0')}/${birthMonth.toString().padLeft(2, '0')}/${birthYear}"; + } + } + + static bool boolean() => _random.nextBool(); + + static String motivation() { + int count = _random.nextInt(3) + 2; // 2 à 4 phrases + List chosenSnippets = []; + while(chosenSnippets.length < count) { + String snippet = _motivationSnippets[_random.nextInt(_motivationSnippets.length)]; + if (!chosenSnippets.contains(snippet)) { + chosenSnippets.add(snippet); + } + } + return chosenSnippets.join(' '); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/app_custom_checkbox.dart b/frontend/lib/widgets/app_custom_checkbox.dart new file mode 100644 index 0000000..f5e0276 --- /dev/null +++ b/frontend/lib/widgets/app_custom_checkbox.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class AppCustomCheckbox extends StatelessWidget { + final String label; + final bool value; + final ValueChanged onChanged; + final double checkboxSize; + final double checkmarkSizeFactor; + + const AppCustomCheckbox({ + super.key, + required this.label, + required this.value, + required this.onChanged, + this.checkboxSize = 20.0, + this.checkmarkSizeFactor = 1.4, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onChanged(!value), // Inverse la valeur au clic + behavior: HitTestBehavior.opaque, // Pour s'assurer que toute la zone du Row est cliquable + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: checkboxSize, + height: checkboxSize, + child: Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + Image.asset( + 'assets/images/square.png', + height: checkboxSize, + width: checkboxSize, + ), + if (value) + Image.asset( + 'assets/images/coche.png', + height: checkboxSize * checkmarkSizeFactor, + width: checkboxSize * checkmarkSizeFactor, + ), + ], + ), + ), + const SizedBox(width: 10), + // Utiliser Flexible pour que le texte ne cause pas d'overflow si trop long + Flexible( + child: Text( + label, + style: GoogleFonts.merienda(fontSize: 16), + overflow: TextOverflow.ellipsis, // Gérer le texte long + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/custom_app_text_field.dart b/frontend/lib/widgets/custom_app_text_field.dart new file mode 100644 index 0000000..5f700da --- /dev/null +++ b/frontend/lib/widgets/custom_app_text_field.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +// Définition de l'enum pour les styles de couleur/fond +enum CustomAppTextFieldStyle { + beige, + lavande, + jaune, +} + +class CustomAppTextField extends StatefulWidget { + final TextEditingController controller; + final String labelText; + final String hintText; + final double fieldWidth; + final double fieldHeight; + final bool obscureText; + final TextInputType keyboardType; + final String? Function(String?)? validator; + final CustomAppTextFieldStyle style; + final bool isRequired; + final bool enabled; + final bool readOnly; + final VoidCallback? onTap; + final IconData? suffixIcon; + final double labelFontSize; + final double inputFontSize; + + const CustomAppTextField({ + super.key, + required this.controller, + required this.labelText, + this.hintText = '', + this.fieldWidth = 300.0, + this.fieldHeight = 53.0, + this.obscureText = false, + this.keyboardType = TextInputType.text, + this.validator, + this.style = CustomAppTextFieldStyle.beige, + this.isRequired = false, + this.enabled = true, + this.readOnly = false, + this.onTap, + this.suffixIcon, + this.labelFontSize = 18.0, + this.inputFontSize = 18.0, + }); + + @override + State createState() => _CustomAppTextFieldState(); +} + +class _CustomAppTextFieldState extends State { + String getBackgroundImagePath() { + switch (widget.style) { + case CustomAppTextFieldStyle.lavande: + return 'assets/images/input_field_lavande.png'; + case CustomAppTextFieldStyle.jaune: + return 'assets/images/input_field_jaune.png'; + case CustomAppTextFieldStyle.beige: + default: + return 'assets/images/input_field_bg.png'; + } + } + + @override + Widget build(BuildContext context) { + const double fontHeightMultiplier = 1.2; + const double internalVerticalPadding = 16.0; + final double dynamicFieldHeight = widget.fieldHeight; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.labelText, + style: GoogleFonts.merienda( + fontSize: widget.labelFontSize, + color: Colors.black87, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 6), + SizedBox( + width: widget.fieldWidth, + height: dynamicFieldHeight, + child: Stack( + alignment: Alignment.centerLeft, + children: [ + Positioned.fill( + child: Image.asset( + getBackgroundImagePath(), + fit: BoxFit.fill, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 8.0), + child: TextFormField( + controller: widget.controller, + obscureText: widget.obscureText, + keyboardType: widget.keyboardType, + enabled: widget.enabled, + readOnly: widget.readOnly, + onTap: widget.onTap, + style: GoogleFonts.merienda( + fontSize: widget.inputFontSize, + color: widget.enabled ? Colors.black87 : Colors.grey + ), + validator: widget.validator ?? + (value) { + if (!widget.enabled || widget.readOnly) return null; + if (widget.isRequired && (value == null || value.isEmpty)) { + return 'Ce champ est obligatoire'; + } + return null; + }, + decoration: InputDecoration( + hintText: widget.hintText, + hintStyle: GoogleFonts.merienda(fontSize: widget.inputFontSize, color: Colors.black54.withOpacity(0.7)), + border: InputBorder.none, + contentPadding: EdgeInsets.zero, + suffixIcon: widget.suffixIcon != null + ? Padding( + padding: const EdgeInsets.only(right: 0.0), + child: Icon(widget.suffixIcon, color: Colors.black54, size: widget.inputFontSize * 1.1), + ) + : null, + isDense: true, + ), + textAlignVertical: TextAlignVertical.center, + ), + ), + ], + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/custom_decorated_text_field.dart b/frontend/lib/widgets/custom_decorated_text_field.dart new file mode 100644 index 0000000..c4a2d1f --- /dev/null +++ b/frontend/lib/widgets/custom_decorated_text_field.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class CustomDecoratedTextField extends StatelessWidget { + final TextEditingController controller; + final String hintText; + final int maxLines; + final double? fieldHeight; // Hauteur optionnelle pour le champ + final bool expandDynamically; // Nouvelle propriété + final bool readOnly; + final double fontSize; + + const CustomDecoratedTextField({ + super.key, + required this.controller, + this.hintText = 'Écrire votre texte ici...', + this.maxLines = 10, // Un nombre raisonnable de lignes par défaut si non dynamique + this.fieldHeight, // Si non fourni, la hauteur sera intrinsèque ou définie par l'image + this.expandDynamically = false, // Par défaut, non dynamique + this.readOnly = false, + this.fontSize = 15.0, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: fieldHeight, // Permet de forcer une hauteur si besoin + child: Stack( + alignment: Alignment.topLeft, + children: [ + Image.asset( + 'assets/images/square.png', // L'image de fond + fit: BoxFit.fill, // Pour remplir l'espace du Stack/SizedBox + width: double.infinity, // S'assurer qu'elle prend toute la largeur disponible + height: fieldHeight != null ? double.infinity : null, // Et toute la hauteur si fieldHeight est spécifié + ), + Padding( + // Ajouter un padding interne pour que le texte ne colle pas aux bords de l'image + padding: const EdgeInsets.only(top: 25.0, bottom: 15.0, left: 20.0, right: 20.0), // Augmentation de la marge supérieure + child: TextFormField( + controller: controller, + keyboardType: TextInputType.multiline, + maxLines: expandDynamically ? null : maxLines, // S'étend dynamiquement si expandDynamically est true + style: GoogleFonts.merienda(fontSize: fontSize, color: Colors.black87), + textAlignVertical: TextAlignVertical.top, + readOnly: readOnly, + decoration: InputDecoration( + hintText: hintText, + hintStyle: GoogleFonts.merienda(fontSize: fontSize, color: Colors.black54.withOpacity(0.7)), + border: InputBorder.none, // Pas de bordure pour le TextFormField lui-même + contentPadding: EdgeInsets.zero, // Le padding est géré par le widget Padding externe + // Pour aligner le hintText en haut à gauche + alignLabelWithHint: true, + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/hover_relief_widget.dart b/frontend/lib/widgets/hover_relief_widget.dart new file mode 100644 index 0000000..cee1f79 --- /dev/null +++ b/frontend/lib/widgets/hover_relief_widget.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +class HoverReliefWidget extends StatefulWidget { + final Widget child; + final VoidCallback? onPressed; + final BorderRadius borderRadius; + final double initialElevation; + final double hoverElevation; + final double scaleFactor; + final bool enableHoverEffect; // Pour activer/désactiver l'effet de survol + final Color initialShadowColor; // Nouveau paramètre + final Color hoverShadowColor; // Nouveau paramètre + + const HoverReliefWidget({ + required this.child, + this.onPressed, + this.borderRadius = const BorderRadius.all(Radius.circular(15.0)), + this.initialElevation = 4.0, + this.hoverElevation = 8.0, + this.scaleFactor = 1.03, // Légèrement réduit par rapport à l'exemple précédent + this.enableHoverEffect = true, // Par défaut, l'effet est activé + this.initialShadowColor = const Color(0x26000000), // Default: Colors.black.withOpacity(0.15) + this.hoverShadowColor = const Color(0x4D000000), // Default: Colors.black.withOpacity(0.3) + super.key, + }); + + @override + State createState() => _HoverReliefWidgetState(); +} + +class _HoverReliefWidgetState extends State { + bool _isHovering = false; + + @override + Widget build(BuildContext context) { + final bool canHover = widget.enableHoverEffect && widget.onPressed != null; + + final hoverTransform = Matrix4.identity()..scale(widget.scaleFactor); + final transform = _isHovering && canHover ? hoverTransform : Matrix4.identity(); + final shadowColor = _isHovering && canHover ? widget.hoverShadowColor : widget.initialShadowColor; + final elevation = _isHovering && canHover ? widget.hoverElevation : widget.initialElevation; + + Widget content = AnimatedContainer( + duration: const Duration(milliseconds: 200), + transform: transform, + transformAlignment: Alignment.center, + child: Material( + color: Colors.transparent, + elevation: elevation, + shadowColor: shadowColor, + borderRadius: widget.borderRadius, + clipBehavior: Clip.antiAlias, + child: widget.child, + ), + ); + + if (widget.onPressed == null) { + // Si non cliquable, on retourne juste le contenu avec l'élévation initiale (pas de survol) + // Ajustement: pour toujours avoir un Material de base même si non cliquable et sans hover. + return Material( + color: Colors.transparent, + elevation: widget.initialElevation, // Utilise l'élévation initiale + shadowColor: widget.initialShadowColor, // Appliqué ici pour l'état non cliquable + borderRadius: widget.borderRadius, + clipBehavior: Clip.antiAlias, + child: widget.child, + ); + } + + return MouseRegion( + onEnter: (_) { + if (widget.enableHoverEffect) setState(() => _isHovering = true); + }, + onExit: (_) { + if (widget.enableHoverEffect) setState(() => _isHovering = false); + }, + cursor: SystemMouseCursors.click, + child: InkWell( + onTap: widget.onPressed, + borderRadius: widget.borderRadius, + hoverColor: Colors.transparent, + splashColor: Colors.grey.withOpacity(0.2), + highlightColor: Colors.grey.withOpacity(0.1), + child: content, + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/image_button.dart b/frontend/lib/widgets/image_button.dart new file mode 100644 index 0000000..2d81362 --- /dev/null +++ b/frontend/lib/widgets/image_button.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class ImageButton extends StatelessWidget { + final String bg; + final double width; + final double height; + final String text; + final Color textColor; + final VoidCallback onPressed; + final double fontSize; // Ajout pour la flexibilité + + const ImageButton({ + super.key, + required this.bg, + required this.width, + required this.height, + required this.text, + required this.textColor, + required this.onPressed, + this.fontSize = 16, // Valeur par défaut + }); + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: onPressed, + child: Container( + width: width, + height: height, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(bg), + fit: BoxFit.fill, + ), + ), + child: Center( + child: Text( + text, + style: GoogleFonts.merienda( + color: textColor, + fontSize: fontSize, // Utilisation du paramètre + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/public/ptitspas-login/login.html b/frontend/public/ptitspas-login/login.html new file mode 100644 index 0000000..430055e --- /dev/null +++ b/frontend/public/ptitspas-login/login.html @@ -0,0 +1,40 @@ + + + + + + P'titsPas - Connexion + + + + + + +
+ +

Grandir pas à pas, sereinement

+ + +
+ + \ No newline at end of file diff --git a/frontend/public/ptitspas-login/styles.css b/frontend/public/ptitspas-login/styles.css new file mode 100644 index 0000000..584aa53 --- /dev/null +++ b/frontend/public/ptitspas-login/styles.css @@ -0,0 +1,133 @@ +/* Reset et styles de base */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Merienda', cursive; + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + background-color: #ffffff; +} + +/* Décor de fond */ +.river { + position: fixed; + left: 0; + top: 0; + width: 60vw; + opacity: 0.15; + pointer-events: none; + z-index: -1; +} + +/* Container principal */ +.login-container { + width: 100%; + max-width: 480px; + padding: 2rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 2rem; +} + +/* Logo */ +.logo { + max-width: 220px; + height: auto; +} + +/* Slogan */ +.slogan { + font-family: 'Merienda', cursive; + text-align: center; + color: #3a3a3a; + font-size: 1.5rem; + margin-bottom: 2rem; +} + +/* Formulaire */ +.login-form { + width: 100%; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +/* Labels */ +label { + color: #3a3a3a; + font-weight: 600; + font-size: 1rem; +} + +/* Champs de saisie */ +input { + height: 80px; + border: none; + border-radius: 30px; + padding: 0 1.2rem; + font-size: 1rem; + font-family: inherit; + background-size: cover; +} + +input[type="email"] { + background-image: url('assets/field_email.png'); + color: #fff; +} + +input[type="password"] { + background-image: url('assets/field_password.png'); + color: #000; +} + +/* Bouton de connexion */ +.login-button { + height: 80px; + background-image: url('assets/btn_green.png'); + background-size: cover; + border: none; + border-radius: 40px; + color: #ffffff; + font-family: "Merienda", cursive; + font-size: 1.1rem; + cursor: pointer; + transition: filter 0.2s ease; +} + +.login-button:hover { + filter: brightness(1.08); +} + +/* Media queries pour mobile */ +@media (max-width: 480px) { + .river { + width: 40vw; + opacity: 0.10; + clip-path: inset(0 0 20% 0); + } + + .logo { + max-width: 120px; + } + + .login-container { + padding: 1rem; + } + + .slogan { + font-size: 1.2rem; + } +} \ No newline at end of file diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock new file mode 100644 index 0000000..9d0bbd5 --- /dev/null +++ b/frontend/pubspec.lock @@ -0,0 +1,639 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + 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: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + 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: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + 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" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e + url: "https://pub.dev" + source: hosted + version: "2.0.28" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: b465e99ce64ba75e61c8c0ce3d87b66d8ac07f0b35d0a7e0263fcfc10f99e836 + url: "https://pub.dev" + source: hosted + version: "13.2.5" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" + http: + dependency: "direct main" + description: + name: http + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + url: "https://pub.dev" + source: hosted + version: "1.3.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + 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: "317a5d961cec5b34e777b9252393f2afbd23084aa6e60fcf601dcf6341b9ebeb" + url: "https://pub.dev" + source: hosted + version: "0.8.12+23" + 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: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + js: + dependency: "direct main" + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://pub.dev" + source: hosted + version: "10.0.8" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + provider: + dependency: "direct main" + description: + name: provider + sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.dev" + source: hosted + version: "14.3.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" +sdks: + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml new file mode 100644 index 0000000..1a73bb5 --- /dev/null +++ b/frontend/pubspec.yaml @@ -0,0 +1,39 @@ +name: p_tits_pas +description: Application de gestion de la garde d'enfants pour les collectivités locales. +publish_to: 'none' +version: 0.1.0 + +environment: + sdk: '>=3.2.6 <4.0.0' + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + provider: ^6.1.1 + go_router: ^13.2.5 + google_fonts: ^6.1.0 + shared_preferences: ^2.2.2 + image_picker: ^1.0.7 + js: ^0.6.7 + url_launcher: ^6.2.4 + http: ^1.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + uses-material-design: true + + assets: + - assets/images/ # Déclarer le dossier entier + - assets/cards/ # Nouveau dossier de cartes + + fonts: + - family: Merienda + fonts: + - asset: assets/fonts/Merienda-VariableFont_wght.ttf + style: normal \ No newline at end of file diff --git a/frontend/test/widget_test.dart b/frontend/test/widget_test.dart new file mode 100644 index 0000000..a6ee807 --- /dev/null +++ b/frontend/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:petitspas/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/frontend/web/favicon.png b/frontend/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/frontend/web/favicon.png differ diff --git a/frontend/web/icons/Icon-192.png b/frontend/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/frontend/web/icons/Icon-192.png differ diff --git a/frontend/web/icons/Icon-512.png b/frontend/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/frontend/web/icons/Icon-512.png differ diff --git a/frontend/web/icons/Icon-maskable-192.png b/frontend/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/frontend/web/icons/Icon-maskable-192.png differ diff --git a/frontend/web/icons/Icon-maskable-512.png b/frontend/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/frontend/web/icons/Icon-maskable-512.png differ diff --git a/frontend/web/index.html b/frontend/web/index.html new file mode 100644 index 0000000..9b2a41d --- /dev/null +++ b/frontend/web/index.html @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + P'titsPas + + + + + + + + + + + + + diff --git a/frontend/web/manifest.json b/frontend/web/manifest.json new file mode 100644 index 0000000..9a2dd4e --- /dev/null +++ b/frontend/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "P'titsPas", + "short_name": "P'titsPas", + "start_url": ".", + "display": "standalone", + "background_color": "#FFFEF9", + "theme_color": "#8AD0C8", + "description": "P'titsPas - Grandir pas à pas, sereinement", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "assets/images/icon.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "assets/images/icon.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/frontend/windows/.gitignore b/frontend/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/frontend/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/frontend/windows/CMakeLists.txt b/frontend/windows/CMakeLists.txt new file mode 100644 index 0000000..8b12466 --- /dev/null +++ b/frontend/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(petitspas LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "petitspas") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/frontend/windows/flutter/CMakeLists.txt b/frontend/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/frontend/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/frontend/windows/flutter/generated_plugin_registrant.cc b/frontend/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..043a96f --- /dev/null +++ b/frontend/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,17 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/frontend/windows/flutter/generated_plugin_registrant.h b/frontend/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/frontend/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/frontend/windows/flutter/generated_plugins.cmake b/frontend/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..a95e267 --- /dev/null +++ b/frontend/windows/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/frontend/windows/runner/CMakeLists.txt b/frontend/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/frontend/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/frontend/windows/runner/Runner.rc b/frontend/windows/runner/Runner.rc new file mode 100644 index 0000000..0bd6754 --- /dev/null +++ b/frontend/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "petitspas" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "petitspas" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "petitspas.exe" "\0" + VALUE "ProductName", "petitspas" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/frontend/windows/runner/flutter_window.cpp b/frontend/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/frontend/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/frontend/windows/runner/flutter_window.h b/frontend/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/frontend/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/frontend/windows/runner/main.cpp b/frontend/windows/runner/main.cpp new file mode 100644 index 0000000..81deb37 --- /dev/null +++ b/frontend/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"petitspas", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/frontend/windows/runner/resource.h b/frontend/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/frontend/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/frontend/windows/runner/resources/app_icon.ico b/frontend/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/frontend/windows/runner/resources/app_icon.ico differ diff --git a/frontend/windows/runner/runner.exe.manifest b/frontend/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/frontend/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/frontend/windows/runner/utils.cpp b/frontend/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/frontend/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/frontend/windows/runner/utils.h b/frontend/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/frontend/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/frontend/windows/runner/win32_window.cpp b/frontend/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/frontend/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/frontend/windows/runner/win32_window.h b/frontend/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/frontend/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..cc1262b --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:p_tits_pas/screens/auth/login_screen.dart'; +import 'package:p_tits_pas/screens/auth/register_choice_screen.dart'; +import 'package:p_tits_pas/screens/auth/parent_register_step1_screen.dart'; +import 'package:p_tits_pas/screens/auth/parent_register_step2_screen.dart'; +import 'package:p_tits_pas/screens/auth/parent_register_step3_screen.dart'; + +void main() { + // TODO: Initialiser SharedPreferences, Provider, etc. + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'P\'titsPas', + theme: ThemeData( + primarySwatch: Colors.blue, // TODO: Utiliser la palette de la charte graphique + textTheme: GoogleFonts.merriweatherTextTheme( + Theme.of(context).textTheme, + ), + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + // Gestionnaire de routes initial (simple pour l'instant) + initialRoute: '/', // Ou '/login' selon le point d'entrée désiré + routes: { + '/': (context) => const LoginScreen(), // Exemple, pourrait être RegisterChoiceScreen aussi + '/login': (context) => const LoginScreen(), + '/register-choice': (context) => const RegisterChoiceScreen(), + '/parent-register/step1': (context) => const ParentRegisterStep1Screen(), + '/parent-register/step2': (context) => const ParentRegisterStep2Screen(), + '/parent-register/step3': (context) => const ParentRegisterStep3Screen(), + // TODO: Ajouter les autres routes (step 4, etc., dashboard...) + }, + // Gestion des routes inconnues + onUnknownRoute: (settings) { + return MaterialPageRoute( + builder: (context) => Scaffold( + body: Center( + child: Text( + 'Route inconnue :\n${settings.name}', + style: GoogleFonts.merriweather(fontSize: 20, color: Colors.red), + textAlign: TextAlign.center, + ), + ), + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/screens/auth/login_screen.dart b/lib/screens/auth/login_screen.dart new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/lib/screens/auth/login_screen.dart @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/screens/auth/parent_register_step3_screen.dart b/lib/screens/auth/parent_register_step3_screen.dart new file mode 100644 index 0000000..9857813 --- /dev/null +++ b/lib/screens/auth/parent_register_step3_screen.dart @@ -0,0 +1,22 @@ + CustomAppTextField( + controller: _firstNameController, + labelText: 'Prénom', + hintText: 'Facultatif si à naître', + isRequired: !widget.childData.isUnbornChild, + ), + const SizedBox(height: 6.0), + CustomAppTextField( + controller: _lastNameController, + labelText: 'Nom', + hintText: 'Nom de l\'enfant', + enabled: true, + ), + const SizedBox(height: 9.0), + CustomAppTextField( + controller: _dobController, + labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance', + hintText: 'JJ/MM/AAAA', + readOnly: true, + onTap: widget.onDateSelect, + suffixIcon: Icons.calendar_today, + ), \ No newline at end of file