Compare commits
7
Commits
04c0b05aae
..
stable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d32d956b0e | ||
|
|
1fca0cf132 | ||
|
|
b16dd4b55c | ||
|
|
8682421453 | ||
|
|
dfe7daed14 | ||
|
|
11aa66feff | ||
|
|
d23f3c9f4f |
@@ -0,0 +1,18 @@
|
||||
# Fins de ligne : toujours LF dans le dépôt (évite les conflits Linux/Windows)
|
||||
* text=auto eol=lf
|
||||
|
||||
# Fichiers binaires : pas de conversion
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
*.webp binary
|
||||
*.pdf binary
|
||||
*.woff binary
|
||||
*.woff2 binary
|
||||
*.ttf binary
|
||||
*.eot binary
|
||||
|
||||
# Scripts shell : toujours LF
|
||||
*.sh text eol=lf
|
||||
@@ -16,7 +16,6 @@ import { AllExceptionsFilter } from './common/filters/all_exceptions.filters';
|
||||
import { EnfantsModule } from './routes/enfants/enfants.module';
|
||||
import { AppConfigModule } from './modules/config/config.module';
|
||||
import { DocumentsLegauxModule } from './modules/documents-legaux';
|
||||
import { RelaisModule } from './routes/relais/relais.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -54,7 +53,6 @@ import { RelaisModule } from './routes/relais/relais.module';
|
||||
AuthModule,
|
||||
AppConfigModule,
|
||||
DocumentsLegauxModule,
|
||||
RelaisModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
config();
|
||||
|
||||
export default new DataSource({
|
||||
type: 'postgres',
|
||||
host: process.env.DATABASE_HOST,
|
||||
port: parseInt(process.env.DATABASE_PORT || '5432', 10),
|
||||
username: process.env.DATABASE_USERNAME,
|
||||
password: process.env.DATABASE_PASSWORD,
|
||||
database: process.env.DATABASE_NAME,
|
||||
entities: ['src/**/*.entity.ts'],
|
||||
migrations: ['src/migrations/*.ts'],
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
|
||||
import { Users } from './users.entity';
|
||||
|
||||
@Entity('relais', { schema: 'public' })
|
||||
export class Relais {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'nom' })
|
||||
nom: string;
|
||||
|
||||
@Column({ name: 'adresse' })
|
||||
adresse: string;
|
||||
|
||||
@Column({ type: 'jsonb', name: 'horaires_ouverture', nullable: true })
|
||||
horaires_ouverture?: any;
|
||||
|
||||
@Column({ name: 'ligne_fixe', nullable: true })
|
||||
ligne_fixe?: string;
|
||||
|
||||
@Column({ default: true, name: 'actif' })
|
||||
actif: boolean;
|
||||
|
||||
@Column({ type: 'text', name: 'notes', nullable: true })
|
||||
notes?: string;
|
||||
|
||||
@CreateDateColumn({ name: 'cree_le', type: 'timestamptz' })
|
||||
cree_le: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'modifie_le', type: 'timestamptz' })
|
||||
modifie_le: Date;
|
||||
|
||||
@OneToMany(() => Users, user => user.relais)
|
||||
gestionnaires: Users[];
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import {
|
||||
Entity, PrimaryGeneratedColumn, Column,
|
||||
CreateDateColumn, UpdateDateColumn,
|
||||
OneToOne, OneToMany, ManyToOne, JoinColumn
|
||||
OneToOne, OneToMany
|
||||
} from 'typeorm';
|
||||
import { AssistanteMaternelle } from './assistantes_maternelles.entity';
|
||||
import { Parents } from './parents.entity';
|
||||
import { Message } from './messages.entity';
|
||||
import { Relais } from './relais.entity';
|
||||
|
||||
// Enums alignés avec la BDD PostgreSQL
|
||||
export enum RoleType {
|
||||
@@ -148,11 +147,4 @@ export class Users {
|
||||
|
||||
@OneToMany(() => Parents, parent => parent.co_parent)
|
||||
co_parent_in?: Parents[];
|
||||
|
||||
@Column({ nullable: true, name: 'relais_id' })
|
||||
relaisId?: string;
|
||||
|
||||
@ManyToOne(() => Relais, relais => relais.gestionnaires, { nullable: true })
|
||||
@JoinColumn({ name: 'relais_id' })
|
||||
relais?: Relais;
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MailService } from './mail.service';
|
||||
import { AppConfigModule } from '../config/config.module';
|
||||
|
||||
@Module({
|
||||
imports: [AppConfigModule],
|
||||
providers: [MailService],
|
||||
exports: [MailService],
|
||||
})
|
||||
export class MailModule {}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { AppConfigService } from '../config/config.service';
|
||||
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
private readonly logger = new Logger(MailService.name);
|
||||
|
||||
constructor(private readonly configService: AppConfigService) {}
|
||||
|
||||
/**
|
||||
* Envoi d'un email générique
|
||||
* @param to Destinataire
|
||||
* @param subject Sujet
|
||||
* @param html Contenu HTML
|
||||
* @param text Contenu texte (optionnel)
|
||||
*/
|
||||
async sendEmail(to: string, subject: string, html: string, text?: string): Promise<void> {
|
||||
try {
|
||||
// Récupération de la configuration SMTP
|
||||
const smtpHost = this.configService.get<string>('smtp_host');
|
||||
const smtpPort = this.configService.get<number>('smtp_port');
|
||||
const smtpSecure = this.configService.get<boolean>('smtp_secure');
|
||||
const smtpAuthRequired = this.configService.get<boolean>('smtp_auth_required');
|
||||
const smtpUser = this.configService.get<string>('smtp_user');
|
||||
const smtpPassword = this.configService.get<string>('smtp_password');
|
||||
const emailFromName = this.configService.get<string>('email_from_name');
|
||||
const emailFromAddress = this.configService.get<string>('email_from_address');
|
||||
|
||||
// Import dynamique de nodemailer
|
||||
const nodemailer = await import('nodemailer');
|
||||
|
||||
// Configuration du transporteur
|
||||
const transportConfig: any = {
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: smtpSecure,
|
||||
};
|
||||
|
||||
if (smtpAuthRequired && smtpUser && smtpPassword) {
|
||||
transportConfig.auth = {
|
||||
user: smtpUser,
|
||||
pass: smtpPassword,
|
||||
};
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport(transportConfig);
|
||||
|
||||
// Envoi de l'email
|
||||
await transporter.sendMail({
|
||||
from: `"${emailFromName}" <${emailFromAddress}>`,
|
||||
to,
|
||||
subject,
|
||||
text: text || html.replace(/<[^>]*>?/gm, ''), // Fallback texte simple
|
||||
html,
|
||||
});
|
||||
|
||||
this.logger.log(`📧 Email envoyé à ${to} : ${subject}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`❌ Erreur lors de l'envoi de l'email à ${to}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoi de l'email de bienvenue pour un gestionnaire
|
||||
* @param to Email du gestionnaire
|
||||
* @param prenom Prénom
|
||||
* @param nom Nom
|
||||
* @param token Token de création de mot de passe (si applicable) ou mot de passe temporaire (si applicable)
|
||||
* @note Pour l'instant, on suppose que le gestionnaire doit définir son mot de passe via "Mot de passe oublié" ou un lien d'activation
|
||||
* Mais le ticket #17 parle de "Flag changement_mdp_obligatoire = TRUE", ce qui implique qu'on lui donne un mot de passe temporaire ou qu'on lui envoie un lien.
|
||||
* Le ticket #24 parle de "API Création mot de passe" via token.
|
||||
* Pour le ticket #17, on crée le gestionnaire avec un mot de passe (hashé).
|
||||
* Si on suit le ticket #35 (Frontend), on saisit un mot de passe.
|
||||
* Donc on envoie juste un email de confirmation de création de compte.
|
||||
*/
|
||||
async sendGestionnaireWelcomeEmail(to: string, prenom: string, nom: string): Promise<void> {
|
||||
const appName = this.configService.get<string>('app_name', 'P\'titsPas');
|
||||
const appUrl = this.configService.get<string>('app_url', 'https://app.ptits-pas.fr');
|
||||
|
||||
const subject = `Bienvenue sur ${appName}`;
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #4CAF50;">Bienvenue ${prenom} ${nom} !</h2>
|
||||
<p>Votre compte gestionnaire sur <strong>${appName}</strong> a été créé avec succès.</p>
|
||||
<p>Vous pouvez dès à présent vous connecter avec l'adresse email <strong>${to}</strong> et le mot de passe qui vous a été communiqué.</p>
|
||||
<p>Lors de votre première connexion, il vous sera demandé de modifier votre mot de passe pour des raisons de sécurité.</p>
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${appUrl}" style="background-color: #4CAF50; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; font-weight: bold;">Accéder à l'application</a>
|
||||
</div>
|
||||
<hr style="border: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="color: #666; font-size: 12px;">
|
||||
Cet email a été envoyé automatiquement. Merci de ne pas y répondre.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
await this.sendEmail(to, subject, html);
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsObject } from 'class-validator';
|
||||
|
||||
export class CreateRelaisDto {
|
||||
@ApiProperty({ example: 'Relais Petite Enfance Centre' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
nom: string;
|
||||
|
||||
@ApiProperty({ example: '12 rue de la Mairie, 75000 Paris' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
adresse: string;
|
||||
|
||||
@ApiProperty({ example: { lundi: '09:00-17:00' }, required: false })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
horaires_ouverture?: any;
|
||||
|
||||
@ApiProperty({ example: '0123456789', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
ligne_fixe?: string;
|
||||
|
||||
@ApiProperty({ default: true, required: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
actif?: boolean;
|
||||
|
||||
@ApiProperty({ example: 'Notes internes...', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
notes?: string;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateRelaisDto } from './create-relais.dto';
|
||||
|
||||
export class UpdateRelaisDto extends PartialType(CreateRelaisDto) {}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common';
|
||||
import { RelaisService } from './relais.service';
|
||||
import { CreateRelaisDto } from './dto/create-relais.dto';
|
||||
import { UpdateRelaisDto } from './dto/update-relais.dto';
|
||||
import { ApiBearerAuth, ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { AuthGuard } from 'src/common/guards/auth.guard';
|
||||
import { RolesGuard } from 'src/common/guards/roles.guard';
|
||||
import { Roles } from 'src/common/decorators/roles.decorator';
|
||||
import { RoleType } from 'src/entities/users.entity';
|
||||
|
||||
@ApiTags('Relais')
|
||||
@ApiBearerAuth('access-token')
|
||||
@UseGuards(AuthGuard, RolesGuard)
|
||||
@Controller('relais')
|
||||
export class RelaisController {
|
||||
constructor(private readonly relaisService: RelaisService) {}
|
||||
|
||||
@Post()
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||
@ApiOperation({ summary: 'Créer un relais' })
|
||||
@ApiResponse({ status: 201, description: 'Le relais a été créé.' })
|
||||
create(@Body() createRelaisDto: CreateRelaisDto) {
|
||||
return this.relaisService.create(createRelaisDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||
@ApiOperation({ summary: 'Lister tous les relais' })
|
||||
@ApiResponse({ status: 200, description: 'Liste des relais.' })
|
||||
findAll() {
|
||||
return this.relaisService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||
@ApiOperation({ summary: 'Récupérer un relais par ID' })
|
||||
@ApiResponse({ status: 200, description: 'Le relais trouvé.' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.relaisService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||
@ApiOperation({ summary: 'Mettre à jour un relais' })
|
||||
@ApiResponse({ status: 200, description: 'Le relais a été mis à jour.' })
|
||||
update(@Param('id') id: string, @Body() updateRelaisDto: UpdateRelaisDto) {
|
||||
return this.relaisService.update(id, updateRelaisDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Roles(RoleType.SUPER_ADMIN, RoleType.ADMINISTRATEUR)
|
||||
@ApiOperation({ summary: 'Supprimer un relais' })
|
||||
@ApiResponse({ status: 200, description: 'Le relais a été supprimé.' })
|
||||
remove(@Param('id') id: string) {
|
||||
return this.relaisService.remove(id);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { RelaisService } from './relais.service';
|
||||
import { RelaisController } from './relais.controller';
|
||||
import { Relais } from 'src/entities/relais.entity';
|
||||
import { AuthModule } from 'src/routes/auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Relais]),
|
||||
AuthModule,
|
||||
],
|
||||
controllers: [RelaisController],
|
||||
providers: [RelaisService],
|
||||
exports: [RelaisService],
|
||||
})
|
||||
export class RelaisModule {}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Relais } from 'src/entities/relais.entity';
|
||||
import { CreateRelaisDto } from './dto/create-relais.dto';
|
||||
import { UpdateRelaisDto } from './dto/update-relais.dto';
|
||||
|
||||
@Injectable()
|
||||
export class RelaisService {
|
||||
constructor(
|
||||
@InjectRepository(Relais)
|
||||
private readonly relaisRepository: Repository<Relais>,
|
||||
) {}
|
||||
|
||||
create(createRelaisDto: CreateRelaisDto) {
|
||||
const relais = this.relaisRepository.create(createRelaisDto);
|
||||
return this.relaisRepository.save(relais);
|
||||
}
|
||||
|
||||
findAll() {
|
||||
return this.relaisRepository.find({ order: { nom: 'ASC' } });
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const relais = await this.relaisRepository.findOne({ where: { id } });
|
||||
if (!relais) {
|
||||
throw new NotFoundException(`Relais #${id} not found`);
|
||||
}
|
||||
return relais;
|
||||
}
|
||||
|
||||
async update(id: string, updateRelaisDto: UpdateRelaisDto) {
|
||||
const relais = await this.findOne(id);
|
||||
Object.assign(relais, updateRelaisDto);
|
||||
return this.relaisRepository.save(relais);
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const relais = await this.findOne(id);
|
||||
return this.relaisRepository.remove(relais);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,4 @@
|
||||
import { ApiProperty, OmitType } from "@nestjs/swagger";
|
||||
import { OmitType } from "@nestjs/swagger";
|
||||
import { CreateUserDto } from "./create_user.dto";
|
||||
import { IsOptional, IsUUID } from "class-validator";
|
||||
|
||||
export class CreateGestionnaireDto extends OmitType(CreateUserDto, ['role'] as const) {
|
||||
@ApiProperty({ required: false, description: 'ID du relais de rattachement' })
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
relaisId?: string;
|
||||
}
|
||||
export class CreateGestionnaireDto extends OmitType(CreateUserDto, ['role'] as const) {}
|
||||
|
||||
@@ -4,13 +4,11 @@ import { GestionnairesController } from './gestionnaires.controller';
|
||||
import { Users } from 'src/entities/users.entity';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AuthModule } from 'src/routes/auth/auth.module';
|
||||
import { MailModule } from 'src/modules/mail/mail.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Users]),
|
||||
AuthModule,
|
||||
MailModule,
|
||||
],
|
||||
controllers: [GestionnairesController],
|
||||
providers: [GestionnairesService],
|
||||
|
||||
@@ -9,14 +9,12 @@ import { RoleType, Users } from 'src/entities/users.entity';
|
||||
import { CreateGestionnaireDto } from '../dto/create_gestionnaire.dto';
|
||||
import { UpdateGestionnaireDto } from '../dto/update_gestionnaire.dto';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { MailService } from 'src/modules/mail/mail.service';
|
||||
|
||||
@Injectable()
|
||||
export class GestionnairesService {
|
||||
constructor(
|
||||
@InjectRepository(Users)
|
||||
private readonly gestionnaireRepository: Repository<Users>,
|
||||
private readonly mailService: MailService,
|
||||
) { }
|
||||
|
||||
// Création d’un gestionnaire
|
||||
@@ -41,41 +39,21 @@ export class GestionnairesService {
|
||||
date_consentement_photo: dto.date_consentement_photo
|
||||
? new Date(dto.date_consentement_photo)
|
||||
: undefined,
|
||||
changement_mdp_obligatoire: true, // Forcé à true pour les nouveaux gestionnaires
|
||||
changement_mdp_obligatoire: dto.changement_mdp_obligatoire ?? false,
|
||||
role: RoleType.GESTIONNAIRE,
|
||||
relaisId: dto.relaisId,
|
||||
});
|
||||
|
||||
const savedUser = await this.gestionnaireRepository.save(entity);
|
||||
|
||||
// Envoi de l'email de bienvenue
|
||||
try {
|
||||
await this.mailService.sendGestionnaireWelcomeEmail(
|
||||
savedUser.email,
|
||||
savedUser.prenom || '',
|
||||
savedUser.nom || '',
|
||||
);
|
||||
} catch (error) {
|
||||
// On ne bloque pas la création si l'envoi d'email échoue, mais on log l'erreur
|
||||
console.error('Erreur lors de l\'envoi de l\'email de bienvenue au gestionnaire', error);
|
||||
}
|
||||
|
||||
return savedUser;
|
||||
return this.gestionnaireRepository.save(entity);
|
||||
}
|
||||
|
||||
// Liste des gestionnaires
|
||||
async findAll(): Promise<Users[]> {
|
||||
return this.gestionnaireRepository.find({
|
||||
where: { role: RoleType.GESTIONNAIRE },
|
||||
relations: ['relais'],
|
||||
});
|
||||
return this.gestionnaireRepository.find({ where: { role: RoleType.GESTIONNAIRE } });
|
||||
}
|
||||
|
||||
// Récupérer un gestionnaire par ID
|
||||
async findOne(id: string): Promise<Users> {
|
||||
const gestionnaire = await this.gestionnaireRepository.findOne({
|
||||
where: { id, role: RoleType.GESTIONNAIRE },
|
||||
relations: ['relais'],
|
||||
});
|
||||
if (!gestionnaire) throw new NotFoundException('Gestionnaire introuvable');
|
||||
return gestionnaire;
|
||||
|
||||
+2
-18
@@ -331,29 +331,13 @@ CREATE INDEX idx_acceptations_utilisateur ON acceptations_documents(id_utilisate
|
||||
CREATE INDEX idx_acceptations_document ON acceptations_documents(id_document);
|
||||
|
||||
-- ==========================================================
|
||||
-- Table : relais
|
||||
-- ==========================================================
|
||||
CREATE TABLE relais (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
nom VARCHAR(255) NOT NULL,
|
||||
adresse TEXT NOT NULL,
|
||||
horaires_ouverture JSONB,
|
||||
ligne_fixe VARCHAR(20),
|
||||
actif BOOLEAN DEFAULT true,
|
||||
notes TEXT,
|
||||
cree_le TIMESTAMPTZ DEFAULT now(),
|
||||
modifie_le TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- ==========================================================
|
||||
-- Modification Table : utilisateurs (ajout colonnes documents et relais)
|
||||
-- Modification Table : utilisateurs (ajout colonnes documents)
|
||||
-- ==========================================================
|
||||
ALTER TABLE utilisateurs
|
||||
ADD COLUMN IF NOT EXISTS cgu_version_acceptee INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS cgu_acceptee_le TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS privacy_version_acceptee INTEGER,
|
||||
ADD COLUMN IF NOT EXISTS privacy_acceptee_le TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS relais_id UUID REFERENCES relais(id) ON DELETE SET NULL;
|
||||
ADD COLUMN IF NOT EXISTS privacy_acceptee_le TIMESTAMPTZ;
|
||||
|
||||
-- ==========================================================
|
||||
-- Seed : Documents légaux génériques v1
|
||||
|
||||
+10
-77
@@ -1,7 +1,7 @@
|
||||
# 🎫 Liste Complète des Tickets - Projet P'titsPas
|
||||
|
||||
**Version** : 1.5
|
||||
**Date** : 17 Février 2026
|
||||
**Version** : 1.4
|
||||
**Date** : 9 Février 2026
|
||||
**Auteur** : Équipe PtitsPas
|
||||
**Estimation totale** : ~184h
|
||||
|
||||
@@ -28,11 +28,7 @@
|
||||
| 15 | [Frontend] Écran Paramètres (accès permanent) | Ouvert |
|
||||
| 16 | [Doc] Documentation configuration on-premise | Ouvert |
|
||||
| 17–88 | (voir sections ci‑dessous ; #82, #78, #79, #81, #83 ; #86, #87, #88 fermés en doublon) | — |
|
||||
| 91 | [Frontend] Inscription AM – Branchement soumission formulaire à l'API | Ouvert |
|
||||
| 92 | [Frontend] Dashboard Admin - Données réelles et branchement API | ✅ Terminé |
|
||||
| 93 | [Frontend] Panneau Admin - Homogeneiser la presentation des onglets | Ouvert |
|
||||
| 94 | [Backend] Relais - modele, API CRUD et liaison gestionnaire | ✅ Terminé |
|
||||
| 95 | [Frontend] Admin - gestion des relais et rattachement gestionnaire | Ouvert |
|
||||
| 92 | [Frontend] Dashboard Admin - Données réelles et branchement API | Ouvert |
|
||||
|
||||
*Gitea #1 et #2 = anciens tickets de test (fermés). Liste complète : https://git.ptits-pas.fr/jmartin/petitspas/issues*
|
||||
|
||||
@@ -645,22 +641,6 @@ Enregistrer les acceptations de documents légaux lors de l'inscription (traçab
|
||||
|
||||
---
|
||||
|
||||
### Ticket #94 : [Backend] Relais - Modèle, API CRUD et liaison gestionnaire ✅
|
||||
**Estimation** : 4h
|
||||
**Labels** : `backend`, `p2`, `admin`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2026-02-21)
|
||||
|
||||
**Description** :
|
||||
Le back-office admin doit gérer des Relais avec des données réelles en base, et permettre une liaison simple avec les gestionnaires.
|
||||
|
||||
**Tâches** :
|
||||
- [x] Créer le modèle `Relais` (nom, adresse, horaires, téléphone, actif, notes)
|
||||
- [x] Exposer les endpoints admin CRUD pour les relais (`GET`, `POST`, `PATCH`, `DELETE`)
|
||||
- [x] Ajouter la liaison : un gestionnaire peut être rattaché à un relais principal (`relais_id` dans `users` ?)
|
||||
- [x] Validations (champs requis, format horaires)
|
||||
|
||||
---
|
||||
|
||||
## 🟢 PRIORITÉ 3 : Frontend - Interfaces
|
||||
|
||||
### Ticket #35 : [Frontend] Écran Création Gestionnaire
|
||||
@@ -914,10 +894,9 @@ Créer l'écran de gestion des documents légaux (CGU/Privacy) pour l'admin.
|
||||
|
||||
---
|
||||
|
||||
### Ticket #92 : [Frontend] Dashboard Admin - Données réelles et branchement API ✅
|
||||
### Ticket #92 : [Frontend] Dashboard Admin - Données réelles et branchement API
|
||||
**Estimation** : 8h
|
||||
**Labels** : `frontend`, `p3`, `admin`
|
||||
**Statut** : ✅ TERMINÉ (Fermé le 2026-02-17)
|
||||
|
||||
**Description** :
|
||||
Le dashboard admin (onglets Gestionnaires | Parents | Assistantes maternelles | Administrateurs) affiche actuellement des données en dur (mock). Remplacer par des appels API pour afficher les vrais utilisateurs et permettre les actions de gestion (voir, modifier, valider/refuser). Référence : [90_AUDIT.md](./90_AUDIT.md).
|
||||
@@ -1039,51 +1018,6 @@ Adapter l'écran de choix Parent/AM pour une meilleure expérience mobile et coh
|
||||
|
||||
---
|
||||
|
||||
### Ticket #91 : [Frontend] Inscription AM – Branchement soumission formulaire à l'API
|
||||
**Estimation** : 3h
|
||||
**Labels** : `frontend`, `p3`, `auth`, `cdc`
|
||||
|
||||
**Description** :
|
||||
Branchement du formulaire d'inscription AM (étape 4) à l'endpoint d'inscription.
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Construire le body (DTO) à partir de `AmRegistrationData`
|
||||
- [ ] Appel HTTP `POST /api/v1/auth/register/am`
|
||||
- [ ] Gestion réponse (201 : succès + redirection ; 4xx : erreur)
|
||||
- [ ] Conversion photo en base64 si nécessaire
|
||||
|
||||
---
|
||||
|
||||
### Ticket #93 : [Frontend] Panneau Admin - Homogénéisation des onglets
|
||||
**Estimation** : 4h
|
||||
**Labels** : `frontend`, `p3`, `admin`, `ux`
|
||||
|
||||
**Description** :
|
||||
Uniformiser l'UI/UX des 4 onglets du dashboard admin (Gestionnaires, Parents, AM, Admins).
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Standardiser le header de liste (Recherche, Filtres, Bouton Action)
|
||||
- [ ] Standardiser les cartes utilisateurs (`ListTile` uniforme)
|
||||
- [ ] Standardiser les états (Loading, Erreur, Vide)
|
||||
- [ ] Factoriser les composants partagés
|
||||
|
||||
---
|
||||
|
||||
### Ticket #95 : [Frontend] Admin - Gestion des Relais et rattachement gestionnaire
|
||||
**Estimation** : 5h
|
||||
**Labels** : `frontend`, `p3`, `admin`
|
||||
|
||||
**Description** :
|
||||
Interface de gestion des Relais dans le dashboard admin et rattachement des gestionnaires.
|
||||
|
||||
**Tâches** :
|
||||
- [ ] Section Relais avec 2 sous-onglets : Paramètres techniques / Paramètres territoriaux
|
||||
- [ ] Liste, Création, Édition, Activation/Désactivation des relais
|
||||
- [ ] Champs UI : nom, adresse, horaires, téléphone, statut, notes
|
||||
- [ ] Onglet Gestionnaires : Ajout contrôle de rattachement au relais principal
|
||||
|
||||
---
|
||||
|
||||
## 🔵 PRIORITÉ 4 : Tests & Documentation
|
||||
|
||||
### Ticket #52 : [Tests] Tests unitaires Backend
|
||||
@@ -1301,29 +1235,28 @@ Rédiger les documents légaux génériques (CGU et Politique de confidentialit
|
||||
|
||||
## 📊 Résumé final
|
||||
|
||||
**Total** : 69 tickets
|
||||
**Estimation** : ~200h de développement
|
||||
**Total** : 65 tickets
|
||||
**Estimation** : ~184h de développement
|
||||
|
||||
### Par priorité
|
||||
- **P0 (Bloquant BDD)** : 7 tickets (~5h)
|
||||
- **P1 (Bloquant Config)** : 7 tickets (~22h)
|
||||
- **P2 (Backend)** : 19 tickets (~54h)
|
||||
- **P3 (Frontend)** : 25 tickets (~83h)
|
||||
- **P2 (Backend)** : 18 tickets (~50h)
|
||||
- **P3 (Frontend)** : 22 tickets (~71h) ← +1 mobile RegisterChoice
|
||||
- **P4 (Tests/Doc)** : 4 tickets (~24h)
|
||||
- **Critiques** : 6 tickets (~13h)
|
||||
- **Juridique** : 1 ticket (~8h)
|
||||
|
||||
### Par domaine
|
||||
- **BDD** : 7 tickets
|
||||
- **Backend** : 24 tickets
|
||||
- **Frontend** : 25 tickets
|
||||
- **Backend** : 23 tickets
|
||||
- **Frontend** : 22 tickets ← +1 mobile RegisterChoice
|
||||
- **Tests** : 3 tickets
|
||||
- **Documentation** : 5 tickets
|
||||
- **Infra** : 2 tickets
|
||||
- **Juridique** : 1 ticket
|
||||
|
||||
### Modifications par rapport à la version initiale
|
||||
- ✅ **v1.5** : Ajout tickets #91, #93, #94, #95. Ticket #92 terminé.
|
||||
- ✅ **v1.4** : Numéros de section du doc = numéros Gitea (Ticket #n = issue #n). Tableau et sections renumérotés. Doublons #86, #87, #88 fermés sur Gitea (#86→#12, #87→#14, #88→#15) ; tickets sources #12, #14, #15 mis à jour (doc + body Gitea).
|
||||
- ✅ **Concept v1.3** : Configuration initiale = un seul panneau Paramètres (3 sections) dans le dashboard ; plus de page dédiée « Setup Wizard » ; navigation bloquée jusqu’à sauvegarde au premier déploiement. Tickets #10, #12, #13 alignés.
|
||||
- ❌ **Supprimé** : Tickets "Renvoyer email validation" (backend + frontend) - Pas prioritaire
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Créer l’issue #84 (correctifs modale MDP) via l’API Gitea
|
||||
|
||||
1. Définir un token valide :
|
||||
`export GITEA_TOKEN="votre_token"`
|
||||
ou créer `.gitea-token` à la racine du projet avec le token seul.
|
||||
|
||||
2. Créer l’issue :
|
||||
```bash
|
||||
cd /chemin/vers/PetitsPas
|
||||
curl -s -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @scripts/issue-84-payload.json \
|
||||
"https://git.ptits-pas.fr/api/v1/repos/jmartin/petitspas/issues"
|
||||
```
|
||||
|
||||
3. En cas de succès (HTTP 201), la réponse JSON contient le numéro de l’issue créée.
|
||||
|
||||
Payload utilisé : `scripts/issue-84-payload.json` (titre + corps depuis `scripts/issue-84-body.txt`).
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# Crée une issue Gitea via l'API.
|
||||
# Usage: GITEA_TOKEN=xxx ./scripts/create-gitea-issue.sh
|
||||
# Ou: mettre le token dans .gitea-token à la racine du projet.
|
||||
|
||||
set -e
|
||||
BASE_URL="${GITEA_URL:-https://git.ptits-pas.fr/api/v1}"
|
||||
REPO="jmartin/petitspas"
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
if [ -f .gitea-token ]; then
|
||||
GITEA_TOKEN=$(cat .gitea-token)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "Définir GITEA_TOKEN ou créer .gitea-token avec votre token Gitea."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TITLE="$1"
|
||||
BODY="$2"
|
||||
if [ -z "$TITLE" ]; then
|
||||
echo "Usage: $0 \"Titre de l'issue\" \"Corps (optionnel)\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build JSON (escape body for JSON)
|
||||
BODY_ESC=$(echo "$BODY" | jq -Rs . 2>/dev/null || echo "null")
|
||||
if [ "$BODY_ESC" = "null" ] || [ -z "$BODY" ]; then
|
||||
PAYLOAD=$(jq -n --arg t "$TITLE" '{title: $t}')
|
||||
else
|
||||
PAYLOAD=$(jq -n --arg t "$TITLE" --arg b "$BODY" '{title: $t, body: $b}')
|
||||
fi
|
||||
|
||||
RESP=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
"$BASE_URL/repos/$REPO/issues")
|
||||
HTTP_CODE=$(echo "$RESP" | tail -1)
|
||||
BODY_RESP=$(echo "$RESP" | sed '$d')
|
||||
|
||||
if [ "$HTTP_CODE" = "201" ]; then
|
||||
ISSUE_NUM=$(echo "$BODY_RESP" | jq -r .number)
|
||||
echo "Issue #$ISSUE_NUM créée."
|
||||
echo "$BODY_RESP" | jq .
|
||||
else
|
||||
echo "Erreur HTTP $HTTP_CODE: $BODY_RESP"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# Poste un commentaire sur une issue Gitea puis la ferme.
|
||||
# Usage: GITEA_TOKEN=xxx ./scripts/gitea-close-issue-with-comment.sh <numéro> "Commentaire"
|
||||
# Ou: mettre le token dans .gitea-token à la racine du projet.
|
||||
# Exemple: ./scripts/gitea-close-issue-with-comment.sh 15 "Livré : panneau Paramètres opérationnel."
|
||||
|
||||
set -e
|
||||
ISSUE="${1:?Usage: $0 <numéro_issue> \"Commentaire\"}"
|
||||
COMMENT="${2:?Usage: $0 <numéro_issue> \"Commentaire\"}"
|
||||
BASE_URL="${GITEA_URL:-https://git.ptits-pas.fr/api/v1}"
|
||||
REPO="jmartin/petitspas"
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
if [ -f .gitea-token ]; then
|
||||
GITEA_TOKEN=$(cat .gitea-token)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "Définir GITEA_TOKEN ou créer .gitea-token avec votre token Gitea."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1) Poster le commentaire
|
||||
echo "Ajout du commentaire sur l'issue #$ISSUE..."
|
||||
# Échapper pour JSON (guillemets et backslash)
|
||||
COMMENT_ESC=$(printf '%s' "$COMMENT" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\r//g')
|
||||
PAYLOAD="{\"body\":\"$COMMENT_ESC\"}"
|
||||
RESP=$(curl -s -w "\n%{http_code}" -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
"$BASE_URL/repos/$REPO/issues/$ISSUE/comments")
|
||||
HTTP_CODE=$(echo "$RESP" | tail -1)
|
||||
BODY=$(echo "$RESP" | sed '$d')
|
||||
|
||||
if [ "$HTTP_CODE" != "201" ]; then
|
||||
echo "Erreur HTTP $HTTP_CODE lors du commentaire: $BODY"
|
||||
exit 1
|
||||
fi
|
||||
echo "Commentaire ajouté."
|
||||
|
||||
# 2) Fermer l'issue
|
||||
echo "Fermeture de l'issue #$ISSUE..."
|
||||
RESP2=$(curl -s -w "\n%{http_code}" -X PATCH \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"state":"closed"}' \
|
||||
"$BASE_URL/repos/$REPO/issues/$ISSUE")
|
||||
HTTP_CODE2=$(echo "$RESP2" | tail -1)
|
||||
BODY2=$(echo "$RESP2" | sed '$d')
|
||||
|
||||
if [ "$HTTP_CODE2" = "200" ] || [ "$HTTP_CODE2" = "201" ]; then
|
||||
echo "Issue #$ISSUE fermée."
|
||||
else
|
||||
echo "Erreur HTTP $HTTP_CODE2: $BODY2"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,14 @@
|
||||
Correctifs et améliorations de la modale de changement de mot de passe obligatoire affichée à la première connexion admin.
|
||||
|
||||
**Périmètre :**
|
||||
- Ajustements visuels / UX de la modale (ChangePasswordDialog)
|
||||
- Cohérence charte graphique, espacements, lisibilité
|
||||
- Comportement (validation, messages d'erreur, fermeture)
|
||||
- Lien de test en debug sur l'écran login (« Test modale MDP ») pour faciliter les réglages
|
||||
|
||||
**Tâches :**
|
||||
- [ ] Revoir le design de la modale (relief, bordures, couleurs)
|
||||
- [ ] Vérifier les champs (MDP actuel, nouveau, confirmation) et validations
|
||||
- [ ] Ajuster les textes et messages d'erreur
|
||||
- [ ] Tester sur mobile et desktop
|
||||
- [ ] Retirer ou conditionner le lien « Test modale MDP » en production si besoin
|
||||
@@ -0,0 +1 @@
|
||||
{"title": "[Frontend] Bug – Correctifs modale Changement MDP (première connexion admin)", "body": "Correctifs et améliorations de la modale de changement de mot de passe obligatoire affichée à la première connexion admin.\n\n**Périmètre :**\n- Ajustements visuels / UX de la modale (ChangePasswordDialog)\n- Cohérence charte graphique, espacements, lisibilité\n- Comportement (validation, messages d'erreur, fermeture)\n- Lien de test en debug sur l'écran login (« Test modale MDP ») pour faciliter les réglages\n\n**Tâches :**\n- [ ] Revoir le design de la modale (relief, bordures, couleurs)\n- [ ] Vérifier les champs (MDP actuel, nouveau, confirmation) et validations\n- [ ] Ajuster les textes et messages d'erreur\n- [ ] Tester sur mobile et desktop\n- [ ] Retirer ou conditionner le lien « Test modale MDP » en production si besoin\n"}
|
||||
Reference in New Issue
Block a user