Compare commits
6 Commits
2b377db1c6
...
9fd6cb7b76
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fd6cb7b76 | ||
| 864d72eb40 | |||
| 04ab6e0a7e | |||
| 979114b93d | |||
| ee940e25b7 | |||
| 10a5cb1fed |
4
.env.example
Normal file
4
.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Configuration du Frontend en développement local
|
||||||
|
|
||||||
|
# URL de l'API backend (doit correspondre au backend lancé localement)
|
||||||
|
API_URL=http://localhost:3000/api
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -52,3 +52,4 @@ Xcf/**
|
|||||||
# Release notes
|
# Release notes
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
Ressources/
|
Ressources/
|
||||||
|
.env
|
||||||
|
|||||||
62
README-DEV.md
Normal file
62
README-DEV.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# 🎨 Guide de développement Frontend
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
- Docker et Docker Compose installés
|
||||||
|
- Le backend doit être démarré (voir README-DEV du backend)
|
||||||
|
|
||||||
|
## 🏃♂️ Démarrage rapide
|
||||||
|
|
||||||
|
### 1. Cloner le projet
|
||||||
|
```bash
|
||||||
|
git clone <url-du-depot-frontend>
|
||||||
|
cd ptitspas-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configuration
|
||||||
|
```bash
|
||||||
|
# Copier le fichier d'exemple
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Lancer le frontend
|
||||||
|
```bash
|
||||||
|
# Démarrer le frontend (le backend doit être déjà lancé)
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
# Voir les logs
|
||||||
|
docker compose -f docker-compose.dev.yml logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 Accès
|
||||||
|
|
||||||
|
- **Frontend** : http://localhost:8000
|
||||||
|
|
||||||
|
## 📋 Workflow de développement complet
|
||||||
|
|
||||||
|
1. **Démarrer le backend** (dans le dépôt backend) :
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Démarrer le frontend** (dans ce dépôt) :
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.dev.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Accéder aux services** :
|
||||||
|
- Frontend : http://localhost:8000
|
||||||
|
- Backend API : http://localhost:3000/api
|
||||||
|
- PgAdmin : http://localhost:8080
|
||||||
|
|
||||||
|
## 🛠️ Commandes utiles
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Arrêter le frontend
|
||||||
|
docker compose -f docker-compose.dev.yml down
|
||||||
|
|
||||||
|
# Rebuild après modification
|
||||||
|
docker compose -f docker-compose.dev.yml up --build
|
||||||
|
|
||||||
|
# Voir l'état
|
||||||
|
docker compose -f docker-compose.dev.yml ps
|
||||||
|
```
|
||||||
3320
backend/package-lock.json
generated
3320
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "petitspas-backend",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Backend pour l'application P'titsPas",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node dist/index.js",
|
|
||||||
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
|
||||||
"build": "tsc",
|
|
||||||
"test": "jest",
|
|
||||||
"init-admin": "ts-node src/scripts/initAdmin.ts"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@nestjs/common": "^11.1.0",
|
|
||||||
"@prisma/client": "^6.7.0",
|
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
|
||||||
"bcrypt": "^5.1.1",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"helmet": "^7.1.0",
|
|
||||||
"jsonwebtoken": "^9.0.2",
|
|
||||||
"morgan": "^1.10.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/bcrypt": "^5.0.2",
|
|
||||||
"@types/cors": "^2.8.17",
|
|
||||||
"@types/express": "^4.17.21",
|
|
||||||
"@types/helmet": "^4.0.0",
|
|
||||||
"@types/morgan": "^1.9.9",
|
|
||||||
"@types/node": "^20.11.19",
|
|
||||||
"prisma": "^6.7.0",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"ts-node-dev": "^2.0.0",
|
|
||||||
"typescript": "^5.3.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
// 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
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
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 {}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
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' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
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 {}
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
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' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import cors from 'cors';
|
|
||||||
import helmet from 'helmet';
|
|
||||||
import morgan from 'morgan';
|
|
||||||
import themeRoutes from './routes/theme.routes';
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const port = process.env.PORT || 3000;
|
|
||||||
|
|
||||||
// Middleware
|
|
||||||
app.use(cors());
|
|
||||||
app.use(helmet());
|
|
||||||
app.use(morgan('dev'));
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
// Routes
|
|
||||||
app.use('/api/themes', themeRoutes);
|
|
||||||
|
|
||||||
// Gestion des erreurs
|
|
||||||
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
||||||
console.error(err.stack);
|
|
||||||
res.status(500).json({ error: 'Une erreur est survenue' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Démarrage du serveur
|
|
||||||
app.listen(port, () => {
|
|
||||||
console.log(`Serveur démarré sur le port ${port}`);
|
|
||||||
});
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
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();
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
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 },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2018",
|
|
||||||
"module": "commonjs",
|
|
||||||
"lib": ["es2018", "esnext.asynciterable"],
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"removeComments": true,
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"strictFunctionTypes": true,
|
|
||||||
"noImplicitThis": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"baseUrl": "."
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules"],
|
|
||||||
"include": ["./src/**/*.ts"]
|
|
||||||
}
|
|
||||||
21
docker-compose.dev.yml
Normal file
21
docker-compose.dev.yml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Docker Compose pour développement local du Frontend
|
||||||
|
# Usage: docker compose -f docker-compose.dev.yml up -d
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Frontend Flutter
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: ptitspas-frontend-dev
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
API_URL: ${API_URL:-http://localhost:3000/api}
|
||||||
|
ports:
|
||||||
|
- "8000:80"
|
||||||
|
networks:
|
||||||
|
- ptitspas_dev
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ptitspas_dev:
|
||||||
|
driver: bridge
|
||||||
@ -20,6 +20,11 @@ public final class GeneratedPluginRegistrant {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
|
Log.e(TAG, "Error registering plugin flutter_plugin_android_lifecycle, io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin", e);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
flutterEngine.getPlugins().add(new com.it_nomads.fluttersecurestorage.FlutterSecureStoragePlugin());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Error registering plugin flutter_secure_storage, com.it_nomads.fluttersecurestorage.FlutterSecureStoragePlugin", e);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin());
|
flutterEngine.getPlugins().add(new io.flutter.plugins.imagepicker.ImagePickerPlugin());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
14
frontend/lib/config/env.dart
Normal file
14
frontend/lib/config/env.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
class Env {
|
||||||
|
// Base URL de l'API, surchargeable à la compilation via --dart-define=API_BASE_URL
|
||||||
|
static const String apiBaseUrl = String.fromEnvironment(
|
||||||
|
'API_BASE_URL',
|
||||||
|
defaultValue: 'https://ynov.ptits-pas.fr',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Construit une URL vers l'API v1 à partir d'un chemin (commençant par '/')
|
||||||
|
static String apiV1(String path) => "${apiBaseUrl}/api/v1$path";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class GestionnairesCreate extends StatelessWidget {
|
||||||
|
const GestionnairesCreate({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Créer un gestionnaire'),
|
||||||
|
),
|
||||||
|
body: const Center(
|
||||||
|
child: Text('Formulaire de création de gestionnaire'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
|
import 'package:p_tits_pas/services/auth_service.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:p_tits_pas/services/bug_report_service.dart';
|
import 'package:p_tits_pas/services/bug_report_service.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@ -19,6 +20,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final _emailController = TextEditingController();
|
final _emailController = TextEditingController();
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController();
|
||||||
|
final AuthService _authService = AuthService();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -47,6 +49,46 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleLogin() async {
|
||||||
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
|
try {
|
||||||
|
final response = await _authService.login(
|
||||||
|
_emailController.text,
|
||||||
|
_passwordController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// Navigation selon le rôle
|
||||||
|
switch (response.role.toLowerCase()) {
|
||||||
|
case 'parent':
|
||||||
|
Navigator.pushReplacementNamed(context, '/parent-dashboard');
|
||||||
|
break;
|
||||||
|
case 'assistante_maternelle':
|
||||||
|
Navigator.pushReplacementNamed(context, '/assistante_maternelle_dashboard');
|
||||||
|
break;
|
||||||
|
case 'admin':
|
||||||
|
Navigator.pushReplacementNamed(context, '/admin_dashboard');
|
||||||
|
break;
|
||||||
|
case 'gestionnaire':
|
||||||
|
Navigator.pushReplacementNamed(context, '/gestionnaire_dashboard');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Navigator.pushReplacementNamed(context, '/home');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Échec de la connexion. Vérifiez vos identifiants.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -144,11 +186,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
height: 40,
|
height: 40,
|
||||||
text: 'Se connecter',
|
text: 'Se connecter',
|
||||||
textColor: const Color(0xFF2D6A4F),
|
textColor: const Color(0xFF2D6A4F),
|
||||||
onPressed: () {
|
onPressed: _handleLogin,
|
||||||
if (_formKey.currentState?.validate() ?? false) {
|
|
||||||
// TODO: Implémenter la logique de connexion
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|||||||
21
frontend/lib/services/api/api_config.dart
Normal file
21
frontend/lib/services/api/api_config.dart
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
class ApiConfig {
|
||||||
|
// static const String baseUrl = 'https://ynov.ptits-pas.fr/api/v1';
|
||||||
|
static const String baseUrl = 'http://localhost:3000/api/v1';
|
||||||
|
|
||||||
|
// Auth endpoints
|
||||||
|
static const String login = '/auth/login';
|
||||||
|
static const String register = '/auth/register';
|
||||||
|
static const String refreshToken = '/auth/refresh';
|
||||||
|
|
||||||
|
// Users endpoints
|
||||||
|
static const String users = '/users';
|
||||||
|
static const String userProfile = '/users/profile';
|
||||||
|
static const String userChildren = '/users/children';
|
||||||
|
|
||||||
|
// Dashboard endpoints
|
||||||
|
static const String dashboard = '/dashboard';
|
||||||
|
static const String events = '/events';
|
||||||
|
static const String contracts = '/contracts';
|
||||||
|
static const String conversations = '/conversations';
|
||||||
|
static const String notifications = '/notifications';
|
||||||
|
}
|
||||||
@ -1,9 +1,78 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||||
import '../models/user.dart';
|
import '../models/user.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
|
||||||
|
class AuthResponse {
|
||||||
|
final String acessToken;
|
||||||
|
final String role;
|
||||||
|
|
||||||
|
AuthResponse({required this.acessToken, required this.role});
|
||||||
|
|
||||||
|
factory AuthResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return AuthResponse(
|
||||||
|
acessToken: json['acessToken'],
|
||||||
|
role: json['role'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
static const String _usersKey = 'users';
|
ApiConfig apiConfig = ApiConfig();
|
||||||
|
String baseUrl = ApiConfig.baseUrl;
|
||||||
|
final storage = const FlutterSecureStorage();
|
||||||
|
|
||||||
|
//login
|
||||||
|
Future<AuthResponse> login(String email, String password) async {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse('$baseUrl${ApiConfig.login}'),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode({'email': email, 'password': password}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
|
final authResponse = AuthResponse.fromJson(data);
|
||||||
|
|
||||||
|
await storage.write(key: 'access_token', value: authResponse.acessToken);
|
||||||
|
await storage.write(key: 'role', value: authResponse.role);
|
||||||
|
return authResponse;
|
||||||
|
} else {
|
||||||
|
throw Exception('Failed to login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//register
|
||||||
|
Future<AppUser> register({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
required String firstName,
|
||||||
|
required String lastName,
|
||||||
|
required String role,
|
||||||
|
}) async {
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse('$baseUrl${ApiConfig.register}'),
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: jsonEncode({
|
||||||
|
'email': email,
|
||||||
|
'password': password,
|
||||||
|
'firstName': firstName,
|
||||||
|
'lastName': lastName,
|
||||||
|
'role': role,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 201) {
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
|
return AppUser.fromJson(data['user']);
|
||||||
|
} else {
|
||||||
|
throw Exception('Failed to register');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*static const String _usersKey = 'users';
|
||||||
static const String _parentsKey = 'parents';
|
static const String _parentsKey = 'parents';
|
||||||
static const String _childrenKey = 'children';
|
static const String _childrenKey = 'children';
|
||||||
|
|
||||||
@ -38,5 +107,5 @@ class AuthService {
|
|||||||
// Méthode pour récupérer l'utilisateur connecté (mode démonstration)
|
// Méthode pour récupérer l'utilisateur connecté (mode démonstration)
|
||||||
static Future<AppUser?> getCurrentUser() async {
|
static Future<AppUser?> getCurrentUser() async {
|
||||||
return null; // Aucun utilisateur en mode démonstration
|
return null; // Aucun utilisateur en mode démonstration
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:p_tits_pas/config/env.dart';
|
||||||
|
|
||||||
class BugReportService {
|
class BugReportService {
|
||||||
static const String _apiUrl = 'https://ynov.ptits-pas.fr/api/bug-reports';
|
static final String _apiUrl = Env.apiV1('/bug-reports');
|
||||||
|
|
||||||
static Future<void> sendReport(String description) async {
|
static Future<void> sendReport(String description) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
20
frontend/lib/services/login_navigation_service.dart
Normal file
20
frontend/lib/services/login_navigation_service.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
class NavigationService {
|
||||||
|
static void handleLoginSuccess(BuildContext context, String role) {
|
||||||
|
switch (role) {
|
||||||
|
case 'admin':
|
||||||
|
Navigator.pushReplacementNamed(context, '/admin_dashboard');
|
||||||
|
break;
|
||||||
|
case 'gestionnaire':
|
||||||
|
Navigator.pushReplacementNamed(context, '/gestionnaire_dashboard');
|
||||||
|
break;
|
||||||
|
case 'parent':
|
||||||
|
Navigator.pushReplacementNamed(context, '/parent-dashboard');
|
||||||
|
break;
|
||||||
|
case 'assistante_maternelle':
|
||||||
|
Navigator.pushReplacementNamed(context, '/assistante_maternelle_dashboard');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,40 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>P'titsPas - Connexion</title>
|
|
||||||
<link rel="stylesheet" href="styles.css">
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Merienda:wght@400;600&display=swap" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<img src="assets/river.png" alt="" class="river" aria-hidden="true">
|
|
||||||
|
|
||||||
<main class="login-container">
|
|
||||||
<img src="assets/logo.png" alt="P'titsPas" class="logo">
|
|
||||||
<h1 class="slogan">Grandir pas à pas, sereinement</h1>
|
|
||||||
|
|
||||||
<form class="login-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="email">Adresse e-mail</label>
|
|
||||||
<input type="email" id="email" name="email"
|
|
||||||
placeholder="Votre adresse e-mail"
|
|
||||||
aria-label="Saisissez votre adresse e-mail"
|
|
||||||
required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Mot de passe</label>
|
|
||||||
<input type="password" id="password" name="password"
|
|
||||||
placeholder="Votre mot de passe"
|
|
||||||
aria-label="Saisissez votre mot de passe"
|
|
||||||
required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="login-button">
|
|
||||||
Se connecter
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,133 +0,0 @@
|
|||||||
/* Reset et styles de base */
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Merienda', cursive;
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Décor de fond */
|
|
||||||
.river {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 60vw;
|
|
||||||
opacity: 0.15;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Container principal */
|
|
||||||
.login-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 480px;
|
|
||||||
padding: 2rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logo */
|
|
||||||
.logo {
|
|
||||||
max-width: 220px;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Slogan */
|
|
||||||
.slogan {
|
|
||||||
font-family: 'Merienda', cursive;
|
|
||||||
text-align: center;
|
|
||||||
color: #3a3a3a;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Formulaire */
|
|
||||||
.login-form {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Labels */
|
|
||||||
label {
|
|
||||||
color: #3a3a3a;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Champs de saisie */
|
|
||||||
input {
|
|
||||||
height: 80px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 30px;
|
|
||||||
padding: 0 1.2rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-family: inherit;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="email"] {
|
|
||||||
background-image: url('assets/field_email.png');
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="password"] {
|
|
||||||
background-image: url('assets/field_password.png');
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bouton de connexion */
|
|
||||||
.login-button {
|
|
||||||
height: 80px;
|
|
||||||
background-image: url('assets/btn_green.png');
|
|
||||||
background-size: cover;
|
|
||||||
border: none;
|
|
||||||
border-radius: 40px;
|
|
||||||
color: #ffffff;
|
|
||||||
font-family: "Merienda", cursive;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: filter 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-button:hover {
|
|
||||||
filter: brightness(1.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Media queries pour mobile */
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.river {
|
|
||||||
width: 40vw;
|
|
||||||
opacity: 0.10;
|
|
||||||
clip-path: inset(0 0 20% 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
max-width: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slogan {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -139,6 +139,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.28"
|
version: "2.0.28"
|
||||||
|
flutter_secure_storage:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage
|
||||||
|
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.2.4"
|
||||||
|
flutter_secure_storage_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_linux
|
||||||
|
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.3"
|
||||||
|
flutter_secure_storage_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_macos
|
||||||
|
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
|
flutter_secure_storage_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_platform_interface
|
||||||
|
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
flutter_secure_storage_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_web
|
||||||
|
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
flutter_secure_storage_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_windows
|
||||||
|
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -169,10 +217,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.5.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -626,6 +674,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.13.0"
|
||||||
xdg_directories:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -18,7 +18,8 @@ dependencies:
|
|||||||
image_picker: ^1.0.7
|
image_picker: ^1.0.7
|
||||||
js: ^0.6.7
|
js: ^0.6.7
|
||||||
url_launcher: ^6.2.4
|
url_launcher: ^6.2.4
|
||||||
http: ^1.2.0
|
http: ^1.5.0
|
||||||
|
flutter_secure_storage: ^9.2.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@ -7,11 +7,14 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
|
flutter_secure_storage_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/login_screen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/register_choice_screen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/parent_register_step1_screen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/parent_register_step2_screen.dart';
|
|
||||||
import 'package:p_tits_pas/screens/auth/parent_register_step3_screen.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
// TODO: Initialiser SharedPreferences, Provider, etc.
|
|
||||||
runApp(const MyApp());
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
|
||||||
const MyApp({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
title: 'P\'titsPas',
|
|
||||||
theme: ThemeData(
|
|
||||||
primarySwatch: Colors.blue, // TODO: Utiliser la palette de la charte graphique
|
|
||||||
textTheme: GoogleFonts.merriweatherTextTheme(
|
|
||||||
Theme.of(context).textTheme,
|
|
||||||
),
|
|
||||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
|
||||||
),
|
|
||||||
// Gestionnaire de routes initial (simple pour l'instant)
|
|
||||||
initialRoute: '/', // Ou '/login' selon le point d'entrée désiré
|
|
||||||
routes: {
|
|
||||||
'/': (context) => const LoginScreen(), // Exemple, pourrait être RegisterChoiceScreen aussi
|
|
||||||
'/login': (context) => const LoginScreen(),
|
|
||||||
'/register-choice': (context) => const RegisterChoiceScreen(),
|
|
||||||
'/parent-register/step1': (context) => const ParentRegisterStep1Screen(),
|
|
||||||
'/parent-register/step2': (context) => const ParentRegisterStep2Screen(),
|
|
||||||
'/parent-register/step3': (context) => const ParentRegisterStep3Screen(),
|
|
||||||
// TODO: Ajouter les autres routes (step 4, etc., dashboard...)
|
|
||||||
},
|
|
||||||
// Gestion des routes inconnues
|
|
||||||
onUnknownRoute: (settings) {
|
|
||||||
return MaterialPageRoute(
|
|
||||||
builder: (context) => Scaffold(
|
|
||||||
body: Center(
|
|
||||||
child: Text(
|
|
||||||
'Route inconnue :\n${settings.name}',
|
|
||||||
style: GoogleFonts.merriweather(fontSize: 20, color: Colors.red),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
CustomAppTextField(
|
|
||||||
controller: _firstNameController,
|
|
||||||
labelText: 'Prénom',
|
|
||||||
hintText: 'Facultatif si à naître',
|
|
||||||
isRequired: !widget.childData.isUnbornChild,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6.0),
|
|
||||||
CustomAppTextField(
|
|
||||||
controller: _lastNameController,
|
|
||||||
labelText: 'Nom',
|
|
||||||
hintText: 'Nom de l\'enfant',
|
|
||||||
enabled: true,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 9.0),
|
|
||||||
CustomAppTextField(
|
|
||||||
controller: _dobController,
|
|
||||||
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
|
|
||||||
hintText: 'JJ/MM/AAAA',
|
|
||||||
readOnly: true,
|
|
||||||
onTap: widget.onDateSelect,
|
|
||||||
suffixIcon: Icons.calendar_today,
|
|
||||||
),
|
|
||||||
0
test-hook.txt
Normal file
0
test-hook.txt
Normal file
Loading…
x
Reference in New Issue
Block a user