feat(init): mise en place initiale de P'titsPas - Documentation: CDC complet, sécifications techniques SSS-001, charte graphique, évolutions - Backend: structure NestJS avec controllers/services/routes, config Prisma - Frontend: app Flutter avec structure MVC, thème et Firebase - Changement de nom: SuperNounou devient P'titsPas

This commit is contained in:
Julien Martin 2025-04-30 10:38:47 +02:00
parent d5015b9c42
commit 9321430818
61 changed files with 9251 additions and 125 deletions

3320
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +1,36 @@
{
"name": "supernounou-backend",
"name": "petitspas-backend",
"version": "1.0.0",
"description": "Backend API pour SuperNounou",
"main": "src/index.ts",
"description": "Backend pour l'application P'titsPas",
"main": "dist/index.js",
"scripts": {
"dev": "nodemon src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "jest"
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"test": "jest",
"init-admin": "ts-node src/scripts/initAdmin.ts"
},
"dependencies": {
"@prisma/client": "^5.0.0",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"bcrypt": "^5.1.0",
"@nestjs/common": "^11.1.0",
"@prisma/client": "^6.7.0",
"@types/jsonwebtoken": "^9.0.9",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"helmet": "^7.0.0",
"express": "^4.18.2",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/node": "^18.15.11",
"@types/bcrypt": "^5.0.0",
"@types/cors": "^2.8.13",
"@types/jsonwebtoken": "^9.0.1",
"@types/morgan": "^1.9.4",
"nodemon": "^2.0.22",
"ts-node": "^10.9.1",
"typescript": "^5.0.4",
"prisma": "^5.0.0",
"jest": "^29.5.0",
"@types/jest": "^29.5.0",
"ts-jest": "^29.1.0"
"@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"
}
}

View File

@ -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
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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' };
}
}

17
backend/src/app.module.ts Normal file
View File

@ -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 {}

View File

@ -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<ThemeData> = 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' });
}
}
}

View File

@ -2,10 +2,10 @@ import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import { PrismaClient } from '@prisma/client';
import themeRoutes from './routes/theme.routes';
const app = express();
const prisma = new PrismaClient();
const port = process.env.PORT || 3000;
// Middleware
app.use(cors());
@ -13,19 +13,16 @@ app.use(helmet());
app.use(morgan('dev'));
app.use(express.json());
// Routes de base
app.get('/', (req, res) => {
res.json({ message: 'Bienvenue sur l\'API SuperNounou' });
});
// 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({ message: 'Une erreur est survenue' });
res.status(500).json({ error: 'Une erreur est survenue' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Serveur démarré sur le port ${PORT}`);
// Démarrage du serveur
app.listen(port, () => {
console.log(`Serveur démarré sur le port ${port}`);
});

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -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<ThemeData>) {
return prisma.theme.update({
where: { id: themeId },
data,
});
}
// Supprimer un thème
static async deleteTheme(themeId: string) {
return prisma.theme.delete({
where: { id: themeId },
});
}
}

51
docs/CHARTE_GRAPHIQUE.md Normal file
View File

@ -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; }
```

211
docs/EVOLUTIONS_CDC.md Normal file
View File

@ -0,0 +1,211 @@
# É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

File diff suppressed because it is too large Load Diff

108
docs/SuperNounou_SSS-001.md Normal file
View File

@ -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 dActivité (PRA)
- Spécifications des API & intégrations
- Directives de déploiement, dobservabilité, de CI/CD
## 2. Portée
Instances de production, pré-production et recette (Frontend, Backend, PostgreSQL, stockage objets), scripts dinstallation, 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 dActivité
### 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 daudit 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 lAPI ».
### 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 d1 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 dattente hors-ligne.
- Tableaux Grafana dexemple 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 |

45
frontend/.gitignore vendored Normal file
View File

@ -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

33
frontend/.metadata Normal file
View File

@ -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'

16
frontend/README.md Normal file
View File

@ -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.

View File

@ -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

View File

@ -0,0 +1,22 @@
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
// Handle other platforms if needed
return web;
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyDXVQr3rlBPXhk-dYB6DyQF_JYrHKxXwrk',
appId: '1:654650461516:web:7a9e7c84c26c5a3a7c0e2c',
messagingSenderId: '654650461516',
projectId: 'petitspas-dev',
authDomain: 'petitspas-dev.firebaseapp.com',
storageBucket: 'petitspas-dev.appspot.com',
);
}

View File

@ -3,62 +3,52 @@ import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart';
import 'theme/app_theme.dart';
import 'screens/auth/login_screen.dart';
import 'screens/auth/register_screen.dart';
import 'screens/auth/parent_register_screen.dart';
import 'screens/home/home_screen.dart';
void main() {
runApp(const MyApp());
runApp(
ChangeNotifierProvider(
create: (_) => AppTheme(),
child: const MyApp(),
),
);
}
final _router = GoRouter(
initialLocation: '/login',
routes: [
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: '/register',
builder: (context, state) => const RegisterScreen(),
),
GoRoute(
path: '/parent-register',
builder: (context, state) => const ParentRegisterScreen(),
),
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
],
);
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
return Consumer<AppTheme>(
builder: (context, appTheme, _) => MaterialApp.router(
title: 'P\'titsPas',
theme: AppTheme.lightTheme,
routerConfig: GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
],
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('P\'titsPas'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/logo.png',
width: 200,
height: 200,
),
const SizedBox(height: 24),
Text(
'Bienvenue sur P\'titsPas',
style: Theme.of(context).textTheme.displayMedium,
),
const SizedBox(height: 16),
Text(
'La plateforme de gestion de la garde d\'enfants',
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
theme: appTheme.lightTheme,
routerConfig: _router,
),
);
}

View File

@ -0,0 +1,73 @@
class Child {
final String id;
final String firstName;
final String lastName;
final DateTime? birthDate;
final DateTime? expectedBirthDate;
final String? photoUrl;
final bool hasPhotoConsent;
final DateTime? photoConsentDate;
final String status; // 'unborn', 'active', 'schooled'
final List<String> parentIds;
final bool isMultipleBirth; // true pour jumeaux, triplés, etc.
final DateTime createdAt;
final DateTime updatedAt;
Child({
required this.id,
required this.firstName,
required this.lastName,
this.birthDate,
this.expectedBirthDate,
this.photoUrl,
required this.hasPhotoConsent,
this.photoConsentDate,
required this.status,
required this.parentIds,
required this.isMultipleBirth,
required this.createdAt,
required this.updatedAt,
});
factory Child.fromJson(Map<String, dynamic> json) {
return Child(
id: json['id'],
firstName: json['firstName'],
lastName: json['lastName'],
birthDate: json['birthDate'] != null
? DateTime.parse(json['birthDate'])
: null,
expectedBirthDate: json['expectedBirthDate'] != null
? DateTime.parse(json['expectedBirthDate'])
: null,
photoUrl: json['photoUrl'],
hasPhotoConsent: json['hasPhotoConsent'] ?? false,
photoConsentDate: json['photoConsentDate'] != null
? DateTime.parse(json['photoConsentDate'])
: null,
status: json['status'] ?? 'unborn',
parentIds: List<String>.from(json['parentIds'] ?? []),
isMultipleBirth: json['isMultipleBirth'] ?? false,
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'firstName': firstName,
'lastName': lastName,
'birthDate': birthDate?.toIso8601String(),
'expectedBirthDate': expectedBirthDate?.toIso8601String(),
'photoUrl': photoUrl,
'hasPhotoConsent': hasPhotoConsent,
'photoConsentDate': photoConsentDate?.toIso8601String(),
'status': status,
'parentIds': parentIds,
'isMultipleBirth': isMultipleBirth,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
}

View File

@ -0,0 +1,63 @@
class Parent {
final String id;
final String userId;
final String firstName;
final String lastName;
final String email;
final String phoneNumber;
final String address;
final String city;
final String postalCode;
final List<String> childrenIds;
final DateTime createdAt;
final DateTime updatedAt;
Parent({
required this.id,
required this.userId,
required this.firstName,
required this.lastName,
required this.email,
required this.phoneNumber,
required this.address,
required this.city,
required this.postalCode,
required this.childrenIds,
required this.createdAt,
required this.updatedAt,
});
factory Parent.fromJson(Map<String, dynamic> json) {
return Parent(
id: json['id'],
userId: json['userId'],
firstName: json['firstName'],
lastName: json['lastName'],
email: json['email'],
phoneNumber: json['phoneNumber'],
address: json['address'],
city: json['city'],
postalCode: json['postalCode'],
childrenIds: List<String>.from(json['childrenIds']),
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'userId': userId,
'firstName': firstName,
'lastName': lastName,
'email': email,
'phoneNumber': phoneNumber,
'address': address,
'city': city,
'postalCode': postalCode,
'childrenIds': childrenIds,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
}

View File

@ -0,0 +1,35 @@
class AppUser {
final String id;
final String email;
final String role;
final DateTime createdAt;
final DateTime updatedAt;
AppUser({
required this.id,
required this.email,
required this.role,
required this.createdAt,
required this.updatedAt,
});
factory AppUser.fromJson(Map<String, dynamic> 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<String, dynamic> toJson() {
return {
'id': id,
'email': email,
'role': role,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
}

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import '../screens/auth/login_screen.dart';
import '../screens/auth/register_screen.dart';
import '../screens/auth/parent_register_screen.dart';
import '../screens/home/home_screen.dart';
class AppRouter {
static const String login = '/login';
static const String register = '/register';
static const String parentRegister = '/parent-register';
static const String home = '/home';
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case login:
return MaterialPageRoute(builder: (_) => const LoginScreen());
case register:
return MaterialPageRoute(builder: (_) => const RegisterScreen());
case parentRegister:
return MaterialPageRoute(builder: (_) => const ParentRegisterScreen());
case home:
return MaterialPageRoute(builder: (_) => const HomeScreen());
default:
return MaterialPageRoute(
builder: (_) => Scaffold(
body: Center(
child: Text('Route non définie: ${settings.name}'),
),
),
);
}
}
}

View File

@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../services/auth_service.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _login() async {
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
try {
await AuthService.login(
_emailController.text,
_passwordController.text,
);
if (mounted) {
context.go('/home');
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de la connexion: $e'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Connexion'),
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
if (!value.contains('@')) {
return 'Veuillez entrer un email valide';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Mot de passe',
border: OutlineInputBorder(),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre mot de passe';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _login,
child: _isLoading
? const CircularProgressIndicator()
: const Text('Se connecter'),
),
const SizedBox(height: 16),
TextButton(
onPressed: () => context.go('/register'),
child: const Text('Créer un compte'),
),
const SizedBox(height: 8),
TextButton(
onPressed: () => context.go('/parent-register'),
child: const Text('Créer un compte parent'),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,756 @@
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:go_router/go_router.dart';
import '../../services/auth_service.dart';
import '../../theme/app_theme.dart';
class ChildData {
final TextEditingController firstNameController = TextEditingController();
final TextEditingController lastNameController = TextEditingController();
DateTime? birthDate;
DateTime? expectedBirthDate;
XFile? photo;
bool hasPhotoConsent = false;
bool isMultipleBirth = false;
bool isUnborn = false;
}
class ParentRegisterScreen extends StatefulWidget {
const ParentRegisterScreen({super.key});
@override
State<ParentRegisterScreen> createState() => _ParentRegisterScreenState();
}
class _ParentRegisterScreenState extends State<ParentRegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _authService = AuthService();
int _currentStep = 0;
bool _isLoading = false;
bool _hasPartner = false;
bool _hasAcceptedCGU = false;
bool _partnerSameAddress = false;
// Contrôleurs pour le parent 1
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _phoneController = TextEditingController();
final _addressController = TextEditingController();
final _cityController = TextEditingController();
final _postalCodeController = TextEditingController();
final _presentationController = TextEditingController();
// Contrôleurs pour le parent 2
final _partnerFirstNameController = TextEditingController();
final _partnerLastNameController = TextEditingController();
final _partnerEmailController = TextEditingController();
final _partnerPhoneController = TextEditingController();
final _partnerAddressController = TextEditingController();
final _partnerCityController = TextEditingController();
final _partnerPostalCodeController = TextEditingController();
// Liste des enfants
final List<ChildData> _children = [ChildData()];
final _motivationController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_firstNameController.dispose();
_lastNameController.dispose();
_phoneController.dispose();
_addressController.dispose();
_cityController.dispose();
_postalCodeController.dispose();
_presentationController.dispose();
_partnerFirstNameController.dispose();
_partnerLastNameController.dispose();
_partnerEmailController.dispose();
_partnerPhoneController.dispose();
_partnerAddressController.dispose();
_partnerCityController.dispose();
_partnerPostalCodeController.dispose();
for (var child in _children) {
child.firstNameController.dispose();
child.lastNameController.dispose();
}
_motivationController.dispose();
super.dispose();
}
Future<void> _pickImage(ChildData child) async {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() {
child.photo = image;
});
}
}
Future<String?> _uploadImage(XFile image, String userId) async {
// En mode démonstration, on retourne juste un chemin local
return image.path;
}
Future<void> _register() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _isLoading = true);
try {
final List<Map<String, dynamic>> childrenData = [];
for (var child in _children) {
String? childPhotoUrl;
if (child.photo != null) {
childPhotoUrl = await _uploadImage(child.photo!, _emailController.text);
}
childrenData.add({
'firstName': child.firstNameController.text,
'lastName': child.lastNameController.text,
'birthDate': child.isUnborn ? null : child.birthDate,
'expectedBirthDate': child.isUnborn ? child.expectedBirthDate : null,
'photoUrl': childPhotoUrl,
'hasPhotoConsent': child.hasPhotoConsent,
'isMultipleBirth': child.isMultipleBirth,
});
}
await _authService.registerParent(
email: _emailController.text,
password: _passwordController.text,
firstName: _firstNameController.text,
lastName: _lastNameController.text,
phoneNumber: _phoneController.text,
address: _addressController.text,
city: _cityController.text,
postalCode: _postalCodeController.text,
presentation: _presentationController.text,
hasAcceptedCGU: _hasAcceptedCGU,
partnerFirstName: _hasPartner ? _partnerFirstNameController.text : null,
partnerLastName: _hasPartner ? _partnerLastNameController.text : null,
partnerEmail: _hasPartner ? _partnerEmailController.text : null,
partnerPhoneNumber: _hasPartner ? _partnerPhoneController.text : null,
partnerAddress: _hasPartner
? (_partnerSameAddress
? _addressController.text
: _partnerAddressController.text)
: null,
partnerCity: _hasPartner
? (_partnerSameAddress ? _cityController.text : _partnerCityController.text)
: null,
partnerPostalCode: _hasPartner
? (_partnerSameAddress ? _postalCodeController.text : _partnerPostalCodeController.text)
: null,
children: childrenData,
motivation: _motivationController.text,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Inscription réussie ! Votre compte est en attente de validation.'),
backgroundColor: Colors.green,
),
);
context.pop();
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de l\'inscription: $e'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
Widget _buildChildForm(ChildData child, int index) {
return Card(
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Enfant ${index + 1}',
style: Theme.of(context).textTheme.titleMedium,
),
if (index > 0)
IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
setState(() {
_children.removeAt(index);
});
},
),
],
),
if (child.photo != null)
CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(child.photo!.path),
),
TextButton(
onPressed: () => _pickImage(child),
child: const Text('Ajouter une photo'),
),
SwitchListTile(
title: const Text('Enfant à naître'),
value: child.isUnborn,
onChanged: (value) => setState(() => child.isUnborn = value),
),
TextFormField(
controller: child.firstNameController,
decoration: const InputDecoration(labelText: 'Prénom de l\'enfant'),
),
TextFormField(
controller: child.lastNameController,
decoration: const InputDecoration(labelText: 'Nom de l\'enfant'),
),
if (!child.isUnborn)
ListTile(
title: const Text('Date de naissance'),
subtitle: Text(child.birthDate != null
? '${child.birthDate!.day}/${child.birthDate!.month}/${child.birthDate!.year}'
: 'Non définie'),
trailing: IconButton(
icon: const Icon(Icons.calendar_today),
onPressed: () async {
final date = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime.now(),
);
if (date != null) {
setState(() => child.birthDate = date);
}
},
),
),
if (child.isUnborn)
ListTile(
title: const Text('Date prévue'),
subtitle: Text(child.expectedBirthDate != null
? '${child.expectedBirthDate!.day}/${child.expectedBirthDate!.month}/${child.expectedBirthDate!.year}'
: 'Non définie'),
trailing: IconButton(
icon: const Icon(Icons.calendar_today),
onPressed: () async {
final date = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (date != null) {
setState(() => child.expectedBirthDate = date);
}
},
),
),
SwitchListTile(
title: const Text('Naissance multiple'),
subtitle: const Text('Jumeaux, triplés, etc.'),
value: child.isMultipleBirth,
onChanged: (value) => setState(() => child.isMultipleBirth = value),
),
if (child.photo != null)
SwitchListTile(
title: const Text('Consentement photo'),
subtitle: const Text('J\'autorise l\'utilisation de la photo de mon enfant'),
value: child.hasPhotoConsent,
onChanged: (value) => setState(() => child.hasPhotoConsent = value),
),
],
),
),
);
}
List<Step> _getSteps() {
return [
// Étape 1 : Parent 1
Step(
title: const Text('Informations parent 1'),
content: Column(
children: [
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
return null;
},
),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Mot de passe'),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
},
),
TextFormField(
controller: _firstNameController,
decoration: const InputDecoration(labelText: 'Prénom'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre prénom';
}
return null;
},
),
TextFormField(
controller: _lastNameController,
decoration: const InputDecoration(labelText: 'Nom'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre nom';
}
return null;
},
),
TextFormField(
controller: _phoneController,
decoration: const InputDecoration(labelText: 'Téléphone'),
keyboardType: TextInputType.phone,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre numéro de téléphone';
}
return null;
},
),
TextFormField(
controller: _addressController,
decoration: const InputDecoration(labelText: 'Adresse'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre adresse';
}
return null;
},
),
TextFormField(
controller: _cityController,
decoration: const InputDecoration(labelText: 'Ville'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre ville';
}
return null;
},
),
TextFormField(
controller: _postalCodeController,
decoration: const InputDecoration(labelText: 'Code postal'),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre code postal';
}
return null;
},
),
TextFormField(
controller: _presentationController,
decoration: const InputDecoration(labelText: 'Présentation'),
maxLines: 3,
),
],
),
isActive: _currentStep >= 0,
),
// Étape 2 : Parent 2
Step(
title: const Text('Parent 2'),
content: Column(
children: [
SwitchListTile(
title: const Text('Ajouter un deuxième parent'),
value: _hasPartner,
onChanged: (value) => setState(() => _hasPartner = value),
),
if (_hasPartner) ...[
SwitchListTile(
title: const Text('Adresse identique au parent 1'),
value: _partnerSameAddress,
onChanged: (value) {
setState(() {
_partnerSameAddress = value;
if (value) {
_partnerAddressController.text = _addressController.text;
_partnerCityController.text = _cityController.text;
_partnerPostalCodeController.text = _postalCodeController.text;
} else {
_partnerAddressController.clear();
_partnerCityController.clear();
_partnerPostalCodeController.clear();
}
});
},
),
TextFormField(
controller: _partnerFirstNameController,
decoration: const InputDecoration(labelText: 'Prénom du deuxième parent'),
validator: (value) {
if (_hasPartner && (value == null || value.isEmpty)) {
return 'Veuillez entrer le prénom';
}
return null;
},
),
TextFormField(
controller: _partnerLastNameController,
decoration: const InputDecoration(labelText: 'Nom du deuxième parent'),
validator: (value) {
if (_hasPartner && (value == null || value.isEmpty)) {
return 'Veuillez entrer le nom';
}
return null;
},
),
TextFormField(
controller: _partnerEmailController,
decoration: const InputDecoration(labelText: 'Email du deuxième parent'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (_hasPartner && (value == null || value.isEmpty)) {
return 'Veuillez entrer l\'email';
}
return null;
},
),
TextFormField(
controller: _partnerPhoneController,
decoration: const InputDecoration(labelText: 'Téléphone du deuxième parent'),
keyboardType: TextInputType.phone,
),
if (!_partnerSameAddress) ...[
TextFormField(
controller: _partnerAddressController,
decoration: const InputDecoration(labelText: 'Adresse du deuxième parent'),
validator: (value) {
if (_hasPartner && !_partnerSameAddress && (value == null || value.isEmpty)) {
return 'Veuillez entrer l\'adresse';
}
return null;
},
),
TextFormField(
controller: _partnerCityController,
decoration: const InputDecoration(labelText: 'Ville du deuxième parent'),
validator: (value) {
if (_hasPartner && !_partnerSameAddress && (value == null || value.isEmpty)) {
return 'Veuillez entrer la ville';
}
return null;
},
),
TextFormField(
controller: _partnerPostalCodeController,
decoration: const InputDecoration(labelText: 'Code postal du deuxième parent'),
keyboardType: TextInputType.number,
validator: (value) {
if (_hasPartner && !_partnerSameAddress && (value == null || value.isEmpty)) {
return 'Veuillez entrer le code postal';
}
return null;
},
),
],
],
],
),
isActive: _currentStep >= 1,
),
// Étape 3 : Enfants
Step(
title: const Text('Enfants'),
content: Column(
children: [
..._children.asMap().entries.map((entry) => _buildChildForm(entry.value, entry.key)),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () {
setState(() {
_children.add(ChildData());
});
},
icon: const Icon(Icons.add),
label: const Text('Ajouter un autre enfant'),
),
],
),
isActive: _currentStep >= 2,
),
// Étape 4 : Description de la situation
Step(
title: const Text('Description de votre situation'),
content: Column(
children: [
TextFormField(
controller: _motivationController,
decoration: const InputDecoration(
labelText: 'Décrivez votre situation',
hintText: 'Expliquez-nous votre situation familiale et vos besoins...',
),
maxLines: 5,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez nous décrire votre situation';
}
return null;
},
),
],
),
isActive: _currentStep >= 3,
),
// Étape 5 : CGU
Step(
title: const Text('Conditions générales'),
content: Column(
children: [
SwitchListTile(
title: const Text('Conditions générales'),
subtitle: const Text('J\'accepte les conditions générales d\'utilisation'),
value: _hasAcceptedCGU,
onChanged: (value) => setState(() => _hasAcceptedCGU = value),
),
if (!_hasAcceptedCGU)
const Text(
'Vous devez accepter les conditions générales pour continuer',
style: TextStyle(color: Colors.red),
),
],
),
isActive: _currentStep >= 4,
),
// Étape 6 : Résumé
Step(
title: const Text('Résumé'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Veuillez vérifier vos informations avant validation :',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 16),
// Parent 1
const Text('Parent 1', style: TextStyle(fontWeight: FontWeight.bold)),
ListTile(
title: const Text('Email'),
subtitle: Text(_emailController.text),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 0),
),
),
ListTile(
title: const Text('Nom complet'),
subtitle: Text('${_firstNameController.text} ${_lastNameController.text}'),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 0),
),
),
ListTile(
title: const Text('Adresse'),
subtitle: Text('${_addressController.text}\n${_postalCodeController.text} ${_cityController.text}'),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 0),
),
),
ListTile(
title: const Text('Téléphone'),
subtitle: Text(_phoneController.text),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 0),
),
),
if (_presentationController.text.isNotEmpty)
ListTile(
title: const Text('Présentation'),
subtitle: Text(_presentationController.text),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 0),
),
),
// Parent 2
if (_hasPartner) ...[
const SizedBox(height: 16),
const Text('Parent 2', style: TextStyle(fontWeight: FontWeight.bold)),
ListTile(
title: const Text('Email'),
subtitle: Text(_partnerEmailController.text),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 1),
),
),
ListTile(
title: const Text('Nom complet'),
subtitle: Text('${_partnerFirstNameController.text} ${_partnerLastNameController.text}'),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 1),
),
),
ListTile(
title: const Text('Téléphone'),
subtitle: Text(_partnerPhoneController.text),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 1),
),
),
ListTile(
title: const Text('Adresse'),
subtitle: _partnerSameAddress
? const Text('Identique au parent 1')
: Text('${_partnerAddressController.text}\n${_partnerPostalCodeController.text} ${_partnerCityController.text}'),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 1),
),
),
],
// Enfants
const SizedBox(height: 16),
const Text('Enfants', style: TextStyle(fontWeight: FontWeight.bold)),
..._children.asMap().entries.map((entry) {
final child = entry.value;
return ListTile(
title: Text('Enfant ${entry.key + 1}'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Prénom : ${child.firstNameController.text} ${child.lastNameController.text}'),
if (child.isUnborn)
Text('Date prévue : ${child.expectedBirthDate?.day}/${child.expectedBirthDate?.month}/${child.expectedBirthDate?.year}')
else
Text('Date de naissance : ${child.birthDate?.day}/${child.birthDate?.month}/${child.birthDate?.year}'),
if (child.isMultipleBirth)
const Text('Naissance multiple'),
],
),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 2),
),
);
}),
// Motivation
const SizedBox(height: 16),
const Text('Motivation', style: TextStyle(fontWeight: FontWeight.bold)),
ListTile(
title: const Text('Votre message'),
subtitle: Text(_motivationController.text),
trailing: IconButton(
icon: const Icon(Icons.edit),
onPressed: () => setState(() => _currentStep = 3),
),
),
],
),
),
isActive: _currentStep >= 5,
),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Inscription Parent'),
),
body: Form(
key: _formKey,
child: Stepper(
currentStep: _currentStep,
onStepContinue: () {
if (_currentStep < _getSteps().length - 1) {
setState(() => _currentStep++);
} else if (_hasAcceptedCGU) {
_register();
}
},
onStepCancel: () {
if (_currentStep > 0) {
setState(() => _currentStep--);
} else {
Navigator.pop(context);
}
},
controlsBuilder: (context, details) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Row(
children: [
if (_currentStep > 0)
OutlinedButton(
onPressed: details.onStepCancel,
child: const Text('Retour'),
),
const SizedBox(width: 16),
if (_currentStep < _getSteps().length - 1)
ElevatedButton(
onPressed: details.onStepContinue,
child: const Text('Suivant'),
)
else
ElevatedButton(
onPressed: _hasAcceptedCGU ? details.onStepContinue : null,
child: _isLoading
? const CircularProgressIndicator()
: const Text('S\'inscrire'),
),
],
),
);
},
steps: _getSteps(),
),
),
);
}
}

View File

@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../services/auth_service.dart';
class RegisterScreen extends StatefulWidget {
const RegisterScreen({super.key});
@override
State<RegisterScreen> createState() => _RegisterScreenState();
}
class _RegisterScreenState extends State<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
_firstNameController.dispose();
_lastNameController.dispose();
super.dispose();
}
Future<void> _register() async {
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
try {
await AuthService.register(
email: _emailController.text,
password: _passwordController.text,
firstName: _firstNameController.text,
lastName: _lastNameController.text,
role: 'user',
);
if (mounted) {
context.go('/login');
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur lors de l\'inscription: $e'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Inscription'),
),
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: _firstNameController,
decoration: const InputDecoration(
labelText: 'Prénom',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre prénom';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _lastNameController,
decoration: const InputDecoration(
labelText: 'Nom',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre nom';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer votre email';
}
if (!value.contains('@')) {
return 'Veuillez entrer un email valide';
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Mot de passe',
border: OutlineInputBorder(),
),
obscureText: true,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un mot de passe';
}
if (value.length < 6) {
return 'Le mot de passe doit contenir au moins 6 caractères';
}
return null;
},
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _register,
child: _isLoading
? const CircularProgressIndicator()
: const Text('S\'inscrire'),
),
const SizedBox(height: 16),
TextButton(
onPressed: () => context.go('/login'),
child: const Text('Déjà un compte ? Se connecter'),
),
],
),
),
),
),
);
}
}

View File

@ -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('P\'titsPas'),
),
body: const Center(
child: Text('Bienvenue sur P\'titsPas'),
),
);
}
}

View File

@ -0,0 +1,70 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/user.dart';
import '../models/parent.dart';
import '../models/child.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<AppUser> 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<AppUser> 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 s'inscrire en tant que parent (mode démonstration)
Future<void> registerParent({
required String email,
required String password,
required String firstName,
required String lastName,
required String phoneNumber,
required String address,
required String city,
required String postalCode,
String? presentation,
required bool hasAcceptedCGU,
String? partnerFirstName,
String? partnerLastName,
String? partnerEmail,
String? partnerPhoneNumber,
String? partnerAddress,
String? partnerCity,
String? partnerPostalCode,
required List<Map<String, dynamic>> children,
required String motivation,
}) async {
// En mode démonstration, on ne fait rien
await Future.delayed(const Duration(seconds: 2)); // Simule un délai de traitement
}
// Méthode pour se déconnecter (mode démonstration)
static Future<void> logout() async {
// Ne fait rien en mode démonstration
}
// Méthode pour vérifier si l'utilisateur est connecté (mode démonstration)
static Future<bool> 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<AppUser?> getCurrentUser() async {
return null; // Aucun utilisateur en mode démonstration
}
}

View File

@ -1,49 +1,86 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class AppTheme {
static const Color primaryColor = Color(0xFF2B6CB0);
static const Color secondaryColor = Color(0xFFF7FAFC);
static const Color backgroundColor = Colors.white;
static const Color textColor = Color(0xFF2D3748);
enum ThemeType {
defaultTheme,
pastelTheme,
// Ajouter d'autres thèmes ici
}
static ThemeData get lightTheme {
class AppTheme extends ChangeNotifier {
static final AppTheme _instance = AppTheme._internal();
factory AppTheme() => _instance;
AppTheme._internal();
// Thème par défaut (P'titsPas)
static const Color _defaultPrimaryColor = Color(0xFF2B6CB0);
static const Color _defaultSecondaryColor = Color(0xFFF7FAFC);
static const Color _defaultBackgroundColor = Color(0xFFFFFFFF);
static const Color _defaultTextColor = Color(0xFF000000);
static const Color _defaultErrorColor = Color(0xFFF4A28C);
static const Color _defaultWarningColor = Color(0xFFF2D269);
static const Color _defaultSuccessColor = Color(0xFF4CAF50);
// Thème Pastel
static const Color _pastelPrimaryColor = Color(0xFF8AD0C8); // Turquoise
static const Color _pastelSecondaryColor = Color(0xFFC6A3D8); // Violet Pastel
static const Color _pastelBackgroundColor = Color(0xFFFFFEF9); // Ivoire BG
static const Color _pastelTextColor = Color(0xFF2F2F2F); // Encre
static const Color _pastelErrorColor = Color(0xFFFFB4AB); // Rose Pastel
static const Color _pastelWarningColor = Color(0xFFFFE4B5); // Jaune Pastel
static const Color _pastelSuccessColor = Color(0xFFA5D6A7); // Vert Pastel
// Configuration du thème actuel
ThemeType _currentTheme = ThemeType.defaultTheme;
ThemeType get currentTheme => _currentTheme;
// Getters pour les couleurs du thème actuel
Color get primaryColor => _currentTheme == ThemeType.defaultTheme ? _defaultPrimaryColor : _pastelPrimaryColor;
Color get secondaryColor => _currentTheme == ThemeType.defaultTheme ? _defaultSecondaryColor : _pastelSecondaryColor;
Color get backgroundColor => _currentTheme == ThemeType.defaultTheme ? _defaultBackgroundColor : _pastelBackgroundColor;
Color get textColor => _currentTheme == ThemeType.defaultTheme ? _defaultTextColor : _pastelTextColor;
Color get errorColor => _currentTheme == ThemeType.defaultTheme ? _defaultErrorColor : _pastelErrorColor;
Color get warningColor => _currentTheme == ThemeType.defaultTheme ? _defaultWarningColor : _pastelWarningColor;
Color get successColor => _currentTheme == ThemeType.defaultTheme ? _defaultSuccessColor : _pastelSuccessColor;
// Méthode pour changer de thème
void setTheme(ThemeType theme) {
_currentTheme = theme;
notifyListeners();
}
// Thème Material 3
ThemeData get lightTheme {
return ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.light(
primary: primaryColor,
secondary: secondaryColor,
background: backgroundColor,
error: errorColor,
tertiary: warningColor,
onPrimary: Colors.white,
onSecondary: Colors.white,
onBackground: textColor,
onError: Colors.white,
onTertiary: textColor,
),
scaffoldBackgroundColor: backgroundColor,
textTheme: TextTheme(
displayLarge: GoogleFonts.comfortaa(
fontSize: 32,
fontWeight: FontWeight.bold,
color: textColor,
),
displayMedium: GoogleFonts.comfortaa(
fontSize: 24,
fontWeight: FontWeight.bold,
color: textColor,
),
bodyLarge: GoogleFonts.roboto(
fontSize: 16,
color: textColor,
),
bodyMedium: GoogleFonts.roboto(
fontSize: 14,
color: textColor,
),
displayLarge: _getTitleStyle(32),
displayMedium: _getTitleStyle(28),
displaySmall: _getTitleStyle(24),
headlineMedium: _getTitleStyle(20),
headlineSmall: _getTitleStyle(18),
titleLarge: _getTitleStyle(16),
bodyLarge: _getBodyStyle(16),
bodyMedium: _getBodyStyle(14),
bodySmall: _getBodyStyle(12),
),
appBarTheme: AppBarTheme(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
elevation: 0,
titleTextStyle: GoogleFonts.comfortaa(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
titleTextStyle: _getTitleStyle(20).copyWith(color: Colors.white),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
@ -56,19 +93,43 @@ class AppTheme {
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: secondaryColor.withOpacity(0.5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: primaryColor.withOpacity(0.5)),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: primaryColor.withOpacity(0.3)),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: primaryColor),
borderSide: BorderSide(color: primaryColor, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
snackBarTheme: SnackBarThemeData(
backgroundColor: errorColor,
contentTextStyle: _getBodyStyle(14).copyWith(color: Colors.white),
),
);
}
// Méthodes privées pour la typographie
TextStyle _getTitleStyle(double fontSize) {
return GoogleFonts.merienda(
fontSize: fontSize,
fontWeight: FontWeight.w600,
color: textColor,
);
}
TextStyle _getBodyStyle(double fontSize) {
return GoogleFonts.merriweather(
fontSize: fontSize,
fontWeight: FontWeight.w300,
color: textColor,
);
}
}

1002
frontend/pubspec.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
name: supernounou
description: Application de gestion de garde d'enfants pour les collectivités locales.
name: petitspas
description: Application de gestion de la garde d'enfants pour les collectivités locales.
publish_to: 'none'
version: 1.0.0+1
@ -11,18 +11,18 @@ dependencies:
sdk: flutter
cupertino_icons: ^1.0.2
# Gestion d'état
provider: ^6.0.5
provider: ^6.1.4
# Navigation
go_router: ^10.0.0
go_router: ^13.0.0
# API
dio: ^5.0.0
# Local storage
shared_preferences: ^2.2.0
# UI
flutter_svg: ^2.0.0
google_fonts: ^5.0.0
google_fonts: ^6.2.1
# Formulaires
form_validator: ^1.1.0
form_validator: ^2.1.1
# Dates
intl: ^0.18.0
# Images
@ -41,4 +41,3 @@ flutter:
uses-material-design: true
assets:
- assets/images/
- assets/icons/

View File

@ -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);
});
}

BIN
frontend/web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

38
frontend/web/index.html Normal file
View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="petitspas">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>petitspas</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

View File

@ -0,0 +1,35 @@
{
"name": "petitspas",
"short_name": "petitspas",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.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"
}
]
}

17
frontend/windows/.gitignore vendored Normal file
View File

@ -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/

View File

@ -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 "$<$<CONFIG:Debug>:_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 "$<TARGET_FILE_DIR:${BINARY_NAME}>")
# 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)

View File

@ -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} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
"${FLUTTER_LIBRARY}"
${FLUTTER_LIBRARY_HEADERS}
${CPP_WRAPPER_SOURCES_CORE}
${CPP_WRAPPER_SOURCES_PLUGIN}
${CPP_WRAPPER_SOURCES_APP}
)

View File

@ -0,0 +1,17 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <printing/printing_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
PrintingPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PrintingPlugin"));
}

View File

@ -0,0 +1,15 @@
//
// Generated file. Do not edit.
//
// clang-format off
#ifndef GENERATED_PLUGIN_REGISTRANT_
#define GENERATED_PLUGIN_REGISTRANT_
#include <flutter/plugin_registry.h>
// Registers Flutter plugins.
void RegisterPlugins(flutter::PluginRegistry* registry);
#endif // GENERATED_PLUGIN_REGISTRANT_

View File

@ -0,0 +1,25 @@
#
# Generated file, do not edit.
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
printing
)
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 $<TARGET_FILE:${plugin}_plugin>)
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)

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,71 @@
#include "flutter_window.h"
#include <optional>
#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<flutter::FlutterViewController>(
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<LRESULT> 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);
}

View File

@ -0,0 +1,33 @@
#ifndef RUNNER_FLUTTER_WINDOW_H_
#define RUNNER_FLUTTER_WINDOW_H_
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <memory>
#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::FlutterViewController> flutter_controller_;
};
#endif // RUNNER_FLUTTER_WINDOW_H_

View File

@ -0,0 +1,43 @@
#include <flutter/dart_project.h>
#include <flutter/flutter_view_controller.h>
#include <windows.h>
#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<std::string> 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;
}

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>

View File

@ -0,0 +1,65 @@
#include "utils.h"
#include <flutter_windows.h>
#include <io.h>
#include <stdio.h>
#include <windows.h>
#include <iostream>
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<std::string> 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::string>();
}
std::vector<std::string> 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;
}

View File

@ -0,0 +1,19 @@
#ifndef RUNNER_UTILS_H_
#define RUNNER_UTILS_H_
#include <string>
#include <vector>
// 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<std::string>,
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
std::vector<std::string> GetCommandLineArguments();
#endif // RUNNER_UTILS_H_

View File

@ -0,0 +1,288 @@
#include "win32_window.h"
#include <dwmapi.h>
#include <flutter_windows.h>
#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<int>(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<EnableNonClientDpiScaling*>(
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<LONG>(origin.x),
static_cast<LONG>(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<CREATESTRUCT*>(lparam);
SetWindowLongPtr(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
auto that = static_cast<Win32Window*>(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<RECT*>(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<Win32Window*>(
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));
}
}

View File

@ -0,0 +1,102 @@
#ifndef RUNNER_WIN32_WINDOW_H_
#define RUNNER_WIN32_WINDOW_H_
#include <windows.h>
#include <functional>
#include <memory>
#include <string>
// 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_