Compare commits
3 Commits
6fe2b89a61
...
26e9738ec7
| Author | SHA1 | Date | |
|---|---|---|---|
| 26e9738ec7 | |||
| 0ec3410457 | |||
| c3acf1970d |
@ -1,4 +1,4 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsDateString,
|
IsDateString,
|
||||||
@ -6,6 +6,7 @@ import {
|
|||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
|
IsUUID,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
ValidateIf,
|
ValidateIf,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
@ -63,4 +64,17 @@ export class CreateEnfantsDto {
|
|||||||
@ApiProperty({ default: false })
|
@ApiProperty({ default: false })
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
is_multiple: boolean;
|
is_multiple: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parent pivot du foyer — obligatoire pour staff (gestionnaire/admin).
|
||||||
|
* Ignoré / interdit en externe pour un PARENT (ticket #132).
|
||||||
|
*/
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
'UUID du parent pivot (staff only). Obligatoire pour GESTIONNAIRE / ADMIN / SUPER_ADMIN.',
|
||||||
|
format: 'uuid',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID('4')
|
||||||
|
parent_user_id?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,33 @@
|
|||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
|
CallHandler,
|
||||||
Controller,
|
Controller,
|
||||||
Delete,
|
Delete,
|
||||||
|
ExecutionContext,
|
||||||
Get,
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
Param,
|
Param,
|
||||||
ParseUUIDPipe,
|
ParseUUIDPipe,
|
||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
|
UploadedFile,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
UploadedFile,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { ApiBearerAuth, ApiTags, ApiConsumes } from '@nestjs/swagger';
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiBody,
|
||||||
|
ApiConsumes,
|
||||||
|
ApiOperation,
|
||||||
|
ApiTags,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
import { diskStorage } from 'multer';
|
import { diskStorage } from 'multer';
|
||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
import { EnfantsService } from './enfants.service';
|
import { EnfantsService } from './enfants.service';
|
||||||
import { CreateEnfantsDto } from './dto/create_enfants.dto';
|
import { CreateEnfantsDto } from './dto/create_enfants.dto';
|
||||||
import { UpdateEnfantsDto } from './dto/update_enfants.dto';
|
import { UpdateEnfantsDto } from './dto/update_enfants.dto';
|
||||||
@ -24,18 +37,7 @@ import { AuthGuard } from 'src/common/guards/auth.guard';
|
|||||||
import { Roles } from 'src/common/decorators/roles.decorator';
|
import { Roles } from 'src/common/decorators/roles.decorator';
|
||||||
import { RolesGuard } from 'src/common/guards/roles.guard';
|
import { RolesGuard } from 'src/common/guards/roles.guard';
|
||||||
|
|
||||||
@ApiBearerAuth('access-token')
|
const photoMulterOptions = {
|
||||||
@ApiTags('Enfants')
|
|
||||||
@UseGuards(AuthGuard, RolesGuard)
|
|
||||||
@Controller('enfants')
|
|
||||||
export class EnfantsController {
|
|
||||||
constructor(private readonly enfantsService: EnfantsService) { }
|
|
||||||
|
|
||||||
@Roles(RoleType.PARENT)
|
|
||||||
@Post()
|
|
||||||
@ApiConsumes('multipart/form-data')
|
|
||||||
@UseInterceptors(
|
|
||||||
FileInterceptor('photo', {
|
|
||||||
storage: diskStorage({
|
storage: diskStorage({
|
||||||
destination: './uploads/photos',
|
destination: './uploads/photos',
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
@ -53,8 +55,52 @@ export class EnfantsController {
|
|||||||
limits: {
|
limits: {
|
||||||
fileSize: 5 * 1024 * 1024,
|
fileSize: 5 * 1024 * 1024,
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multer uniquement si Content-Type multipart (parcours parent).
|
||||||
|
* Les appels staff JSON (#132) passent sans interceptor fichier.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
class OptionalEnfantPhotoInterceptor implements NestInterceptor {
|
||||||
|
private readonly multipart = new (FileInterceptor(
|
||||||
|
'photo',
|
||||||
|
photoMulterOptions,
|
||||||
|
))();
|
||||||
|
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> | Promise<Observable<unknown>> {
|
||||||
|
const req = context.switchToHttp().getRequest();
|
||||||
|
const ct = String(req.headers['content-type'] ?? '');
|
||||||
|
if (!ct.includes('multipart/form-data')) {
|
||||||
|
return next.handle();
|
||||||
|
}
|
||||||
|
return this.multipart.intercept(context, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiBearerAuth('access-token')
|
||||||
|
@ApiTags('Enfants')
|
||||||
|
@UseGuards(AuthGuard, RolesGuard)
|
||||||
|
@Controller('enfants')
|
||||||
|
export class EnfantsController {
|
||||||
|
constructor(private readonly enfantsService: EnfantsService) { }
|
||||||
|
|
||||||
|
@Roles(
|
||||||
|
RoleType.PARENT,
|
||||||
|
RoleType.GESTIONNAIRE,
|
||||||
|
RoleType.ADMINISTRATEUR,
|
||||||
|
RoleType.SUPER_ADMIN,
|
||||||
)
|
)
|
||||||
|
@Post()
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Créer un enfant',
|
||||||
|
description:
|
||||||
|
'PARENT : multipart éventuel, rattache au compte connecté. Staff : JSON + parent_user_id (foyer). Ticket #132.',
|
||||||
|
})
|
||||||
|
@ApiConsumes('application/json', 'multipart/form-data')
|
||||||
|
@ApiBody({ type: CreateEnfantsDto })
|
||||||
|
@UseInterceptors(OptionalEnfantPhotoInterceptor)
|
||||||
create(
|
create(
|
||||||
@Body() dto: CreateEnfantsDto,
|
@Body() dto: CreateEnfantsDto,
|
||||||
@UploadedFile() photo: Express.Multer.File,
|
@UploadedFile() photo: Express.Multer.File,
|
||||||
|
|||||||
@ -13,6 +13,12 @@ import { ParentsChildren } from 'src/entities/parents_children.entity';
|
|||||||
import { RoleType, Users } from 'src/entities/users.entity';
|
import { RoleType, Users } from 'src/entities/users.entity';
|
||||||
import { CreateEnfantsDto } from './dto/create_enfants.dto';
|
import { CreateEnfantsDto } from './dto/create_enfants.dto';
|
||||||
|
|
||||||
|
const STAFF_ROLES: RoleType[] = [
|
||||||
|
RoleType.GESTIONNAIRE,
|
||||||
|
RoleType.ADMINISTRATEUR,
|
||||||
|
RoleType.SUPER_ADMIN,
|
||||||
|
];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EnfantsService {
|
export class EnfantsService {
|
||||||
constructor(
|
constructor(
|
||||||
@ -24,20 +30,34 @@ export class EnfantsService {
|
|||||||
private readonly parentsChildrenRepository: Repository<ParentsChildren>,
|
private readonly parentsChildrenRepository: Repository<ParentsChildren>,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
// Création d'un enfant
|
private isStaff(user: Users): boolean {
|
||||||
async create(dto: CreateEnfantsDto, currentUser: Users, photoFile?: Express.Multer.File): Promise<Children> {
|
return STAFF_ROLES.includes(user.role);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Création d'un enfant.
|
||||||
|
* - PARENT : rattache au parent connecté (multipart photo optionnel).
|
||||||
|
* - Staff : JSON + `parent_user_id` obligatoire (foyer existant). Ticket #132.
|
||||||
|
*/
|
||||||
|
async create(
|
||||||
|
dto: CreateEnfantsDto,
|
||||||
|
currentUser: Users,
|
||||||
|
photoFile?: Express.Multer.File,
|
||||||
|
): Promise<Children> {
|
||||||
|
const pivotUserId = this.resolvePivotParentUserId(dto, currentUser);
|
||||||
|
|
||||||
const parent = await this.parentsRepository.findOne({
|
const parent = await this.parentsRepository.findOne({
|
||||||
where: { user_id: currentUser.id },
|
where: { user_id: pivotUserId },
|
||||||
relations: ['co_parent'],
|
relations: ['co_parent'],
|
||||||
});
|
});
|
||||||
if (!parent) throw new NotFoundException('Parent introuvable');
|
if (!parent) throw new NotFoundException('Parent introuvable');
|
||||||
|
|
||||||
// Vérif métier simple
|
// Vérif métier simple (aligné comportement historique parent)
|
||||||
if (dto.status !== StatutEnfantType.A_NAITRE && !dto.birth_date) {
|
if (dto.status !== StatutEnfantType.A_NAITRE && !dto.birth_date) {
|
||||||
throw new BadRequestException('Un enfant né doit avoir une date de naissance');
|
throw new BadRequestException('Un enfant né doit avoir une date de naissance');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérif doublon éventuel (ex: même prénom + date de naissance pour ce parent)
|
// Vérif doublon éventuel (ex: même prénom + date de naissance)
|
||||||
const exist = await this.childrenRepository.findOne({
|
const exist = await this.childrenRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
first_name: dto.first_name,
|
first_name: dto.first_name,
|
||||||
@ -47,37 +67,84 @@ export class EnfantsService {
|
|||||||
});
|
});
|
||||||
if (exist) throw new ConflictException('Cet enfant existe déjà');
|
if (exist) throw new ConflictException('Cet enfant existe déjà');
|
||||||
|
|
||||||
// Gestion de la photo uploadée
|
// Gestion de la photo uploadée (parcours parent multipart)
|
||||||
|
let photoUrl = dto.photo_url;
|
||||||
|
let consentAt: Date | undefined;
|
||||||
if (photoFile) {
|
if (photoFile) {
|
||||||
dto.photo_url = `/uploads/photos/${photoFile.filename}`;
|
photoUrl = `/uploads/photos/${photoFile.filename}`;
|
||||||
if (dto.consent_photo) {
|
if (dto.consent_photo) {
|
||||||
dto.consent_photo_at = new Date().toISOString();
|
consentAt = new Date();
|
||||||
}
|
}
|
||||||
|
} else if (dto.consent_photo) {
|
||||||
|
consentAt = dto.consent_photo_at
|
||||||
|
? new Date(dto.consent_photo_at)
|
||||||
|
: new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Création
|
const child = this.childrenRepository.create({
|
||||||
const child = this.childrenRepository.create(dto);
|
status: dto.status,
|
||||||
|
first_name: dto.first_name,
|
||||||
|
last_name: dto.last_name,
|
||||||
|
gender: dto.gender,
|
||||||
|
birth_date: dto.birth_date ? new Date(dto.birth_date) : undefined,
|
||||||
|
due_date: dto.due_date ? new Date(dto.due_date) : undefined,
|
||||||
|
photo_url: photoUrl,
|
||||||
|
consent_photo: !!dto.consent_photo,
|
||||||
|
consent_photo_at: consentAt,
|
||||||
|
is_multiple: !!dto.is_multiple,
|
||||||
|
});
|
||||||
await this.childrenRepository.save(child);
|
await this.childrenRepository.save(child);
|
||||||
|
|
||||||
// Lien parent-enfant (Parent 1)
|
// Lien parent-enfant (pivot)
|
||||||
const parentLink = this.parentsChildrenRepository.create({
|
await this.parentsChildrenRepository.save(
|
||||||
|
this.parentsChildrenRepository.create({
|
||||||
parentId: parent.user_id,
|
parentId: parent.user_id,
|
||||||
enfantId: child.id,
|
enfantId: child.id,
|
||||||
});
|
}),
|
||||||
await this.parentsChildrenRepository.save(parentLink);
|
);
|
||||||
|
|
||||||
// Rattachement automatique au co-parent s'il existe
|
// Rattachement automatique au co-parent s'il existe
|
||||||
if (parent.co_parent) {
|
if (parent.co_parent) {
|
||||||
const coParentLink = this.parentsChildrenRepository.create({
|
await this.parentsChildrenRepository.save(
|
||||||
|
this.parentsChildrenRepository.create({
|
||||||
parentId: parent.co_parent.id,
|
parentId: parent.co_parent.id,
|
||||||
enfantId: child.id,
|
enfantId: child.id,
|
||||||
});
|
}),
|
||||||
await this.parentsChildrenRepository.save(coParentLink);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.findOne(child.id, currentUser);
|
return this.findOne(child.id, currentUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolvePivotParentUserId(
|
||||||
|
dto: CreateEnfantsDto,
|
||||||
|
currentUser: Users,
|
||||||
|
): string {
|
||||||
|
if (this.isStaff(currentUser)) {
|
||||||
|
const id = dto.parent_user_id?.trim();
|
||||||
|
if (!id) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'parent_user_id est obligatoire pour créer un enfant (staff)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentUser.role === RoleType.PARENT) {
|
||||||
|
if (
|
||||||
|
dto.parent_user_id &&
|
||||||
|
dto.parent_user_id.trim() !== currentUser.id
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'Un parent ne peut pas créer un enfant pour un autre compte',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return currentUser.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ForbiddenException('Accès interdit');
|
||||||
|
}
|
||||||
|
|
||||||
// Liste des enfants (admin/gestionnaire)
|
// Liste des enfants (admin/gestionnaire)
|
||||||
async findAll(): Promise<Children[]> {
|
async findAll(): Promise<Children[]> {
|
||||||
return this.childrenRepository.find({
|
return this.childrenRepository.find({
|
||||||
@ -90,7 +157,7 @@ export class EnfantsService {
|
|||||||
async findOne(id: string, currentUser: Users): Promise<Children> {
|
async findOne(id: string, currentUser: Users): Promise<Children> {
|
||||||
const child = await this.childrenRepository.findOne({
|
const child = await this.childrenRepository.findOne({
|
||||||
where: { id },
|
where: { id },
|
||||||
relations: ['parentLinks'],
|
relations: ['parentLinks', 'parentLinks.parent', 'parentLinks.parent.user'],
|
||||||
});
|
});
|
||||||
if (!child) throw new NotFoundException('Enfant introuvable');
|
if (!child) throw new NotFoundException('Enfant introuvable');
|
||||||
|
|
||||||
@ -120,7 +187,8 @@ export class EnfantsService {
|
|||||||
const child = await this.childrenRepository.findOne({ where: { id } });
|
const child = await this.childrenRepository.findOne({ where: { id } });
|
||||||
if (!child) throw new NotFoundException('Enfant introuvable');
|
if (!child) throw new NotFoundException('Enfant introuvable');
|
||||||
|
|
||||||
const patch: Partial<Children> = { ...dto } as Partial<Children>;
|
const { parent_user_id: _ignored, ...rest } = dto;
|
||||||
|
const patch: Partial<Children> = { ...rest } as Partial<Children>;
|
||||||
if (dto.consent_photo !== undefined) {
|
if (dto.consent_photo !== undefined) {
|
||||||
patch.consent_photo = dto.consent_photo;
|
patch.consent_photo = dto.consent_photo;
|
||||||
patch.consent_photo_at = dto.consent_photo ? new Date() : null!;
|
patch.consent_photo_at = dto.consent_photo ? new Date() : null!;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:http_parser/http_parser.dart';
|
||||||
import 'package:p_tits_pas/models/user.dart';
|
import 'package:p_tits_pas/models/user.dart';
|
||||||
import 'package:p_tits_pas/models/enfant_admin_model.dart';
|
import 'package:p_tits_pas/models/enfant_admin_model.dart';
|
||||||
import 'package:p_tits_pas/models/parent_model.dart';
|
import 'package:p_tits_pas/models/parent_model.dart';
|
||||||
@ -57,6 +58,33 @@ class UserService {
|
|||||||
return v.toString();
|
return v.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// MIME pour upload photo enfant (filtre Nest : jpg/jpeg/png/gif).
|
||||||
|
static MediaType _imageMediaType(String filename, List<int> bytes) {
|
||||||
|
if (bytes.length >= 3 &&
|
||||||
|
bytes[0] == 0xFF &&
|
||||||
|
bytes[1] == 0xD8 &&
|
||||||
|
bytes[2] == 0xFF) {
|
||||||
|
return MediaType('image', 'jpeg');
|
||||||
|
}
|
||||||
|
if (bytes.length >= 8 &&
|
||||||
|
bytes[0] == 0x89 &&
|
||||||
|
bytes[1] == 0x50 &&
|
||||||
|
bytes[2] == 0x4E &&
|
||||||
|
bytes[3] == 0x47) {
|
||||||
|
return MediaType('image', 'png');
|
||||||
|
}
|
||||||
|
if (bytes.length >= 6 &&
|
||||||
|
bytes[0] == 0x47 &&
|
||||||
|
bytes[1] == 0x49 &&
|
||||||
|
bytes[2] == 0x46) {
|
||||||
|
return MediaType('image', 'gif');
|
||||||
|
}
|
||||||
|
final lower = filename.toLowerCase();
|
||||||
|
if (lower.endsWith('.png')) return MediaType('image', 'png');
|
||||||
|
if (lower.endsWith('.gif')) return MediaType('image', 'gif');
|
||||||
|
return MediaType('image', 'jpeg');
|
||||||
|
}
|
||||||
|
|
||||||
static String _errMessage(dynamic err) {
|
static String _errMessage(dynamic err) {
|
||||||
if (err == null) return 'Erreur inconnue';
|
if (err == null) return 'Erreur inconnue';
|
||||||
if (err is String) return err;
|
if (err is String) return err;
|
||||||
@ -515,6 +543,68 @@ class UserService {
|
|||||||
return enrichEnfantParentNames(enfant);
|
return enrichEnfantParentNames(enfant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Création enfant côté staff (#132) — nécessite `POST /enfants` étendu (voir mini-spec).
|
||||||
|
/// [parentUserId] : parent pivot du foyer (`parent_user_id`).
|
||||||
|
/// Avec [photoBytes] : `multipart/form-data` (champ fichier `photo`), sinon JSON.
|
||||||
|
static Future<EnfantAdminModel> createEnfant({
|
||||||
|
required String parentUserId,
|
||||||
|
required Map<String, dynamic> body,
|
||||||
|
List<int>? photoBytes,
|
||||||
|
String? photoFilename,
|
||||||
|
}) async {
|
||||||
|
final hasPhoto = photoBytes != null && photoBytes.isNotEmpty;
|
||||||
|
final http.Response response;
|
||||||
|
if (hasPhoto) {
|
||||||
|
final token = await TokenService.getToken();
|
||||||
|
final req = http.MultipartRequest(
|
||||||
|
'POST',
|
||||||
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.enfants}'),
|
||||||
|
);
|
||||||
|
req.headers['Accept'] = 'application/json';
|
||||||
|
if (token != null) {
|
||||||
|
req.headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
|
body.forEach((key, value) {
|
||||||
|
if (value == null) return;
|
||||||
|
req.fields[key] = value is bool
|
||||||
|
? (value ? 'true' : 'false')
|
||||||
|
: value.toString();
|
||||||
|
});
|
||||||
|
req.fields['parent_user_id'] = parentUserId;
|
||||||
|
final name = (photoFilename ?? '').trim();
|
||||||
|
final filename = name.isNotEmpty ? name : 'photo.jpg';
|
||||||
|
req.files.add(
|
||||||
|
http.MultipartFile.fromBytes(
|
||||||
|
'photo',
|
||||||
|
photoBytes,
|
||||||
|
filename: filename,
|
||||||
|
contentType: _imageMediaType(filename, photoBytes),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final streamed = await req.send();
|
||||||
|
response = await http.Response.fromStream(streamed);
|
||||||
|
} else {
|
||||||
|
final payload = <String, dynamic>{
|
||||||
|
...body,
|
||||||
|
'parent_user_id': parentUserId,
|
||||||
|
};
|
||||||
|
response = await http.post(
|
||||||
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.enfants}'),
|
||||||
|
headers: await _headers(),
|
||||||
|
body: jsonEncode(payload),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||||
|
throw Exception(
|
||||||
|
_extractErrorMessage(response.body, 'Erreur création enfant'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final enfant = EnfantAdminModel.fromJson(
|
||||||
|
jsonDecode(response.body) as Map<String, dynamic>,
|
||||||
|
);
|
||||||
|
return enrichEnfantParentNames(enfant);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<void> deleteEnfant(String enfantId) async {
|
static Future<void> deleteEnfant(String enfantId) async {
|
||||||
final response = await http.delete(
|
final response = await http.delete(
|
||||||
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.enfants}/$enfantId'),
|
Uri.parse('${ApiConfig.baseUrl}${ApiConfig.enfants}/$enfantId'),
|
||||||
|
|||||||
@ -1,14 +1,27 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||||
|
|
||||||
/// Cadre photo identité AM (35×45 mm) — même logique que [ValidationAmWizard].
|
/// Cadre photo identité AM / enfant (35×45 mm) — même logique que [ValidationAmWizard].
|
||||||
class AdminAmPhotoFrame extends StatelessWidget {
|
class AdminAmPhotoFrame extends StatelessWidget {
|
||||||
final String? photoUrl;
|
final String? photoUrl;
|
||||||
|
final Uint8List? imageBytes;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final VoidCallback? onClear;
|
||||||
|
final String emptyLabel;
|
||||||
|
|
||||||
static const double idPhotoAspectRatio = 35 / 45;
|
static const double idPhotoAspectRatio = 35 / 45;
|
||||||
|
|
||||||
const AdminAmPhotoFrame({super.key, this.photoUrl});
|
const AdminAmPhotoFrame({
|
||||||
|
super.key,
|
||||||
|
this.photoUrl,
|
||||||
|
this.imageBytes,
|
||||||
|
this.onTap,
|
||||||
|
this.onClear,
|
||||||
|
this.emptyLabel = 'Aucune photo',
|
||||||
|
});
|
||||||
|
|
||||||
/// Largeur colonne photo pour remplir [height] (cadre inclus).
|
/// Largeur colonne photo pour remplir [height] (cadre inclus).
|
||||||
static double columnWidthForHeight(double height) {
|
static double columnWidthForHeight(double height) {
|
||||||
@ -36,9 +49,12 @@ class AdminAmPhotoFrame extends StatelessWidget {
|
|||||||
ph = pw / ar;
|
ph = pw / ar;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final hasLocal = imageBytes != null && imageBytes!.isNotEmpty;
|
||||||
|
final showClear = onClear != null && hasLocal;
|
||||||
|
|
||||||
// Cadre gris = taille photo + padding uniforme ; centré dans la colonne
|
// Cadre gris = taille photo + padding uniforme ; centré dans la colonne
|
||||||
// (évite le vide blanc en bas quand le conteneur parent est plus haut).
|
// (évite le vide blanc en bas quand le conteneur parent est plus haut).
|
||||||
return Align(
|
Widget frame = Align(
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -60,23 +76,82 @@ class AdminAmPhotoFrame extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (onTap != null) {
|
||||||
|
frame = MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
child: frame,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showClear) return frame;
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
frame,
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: IconButton(
|
||||||
|
tooltip: 'Retirer la photo',
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
minWidth: 32,
|
||||||
|
minHeight: 32,
|
||||||
|
),
|
||||||
|
icon: Icon(
|
||||||
|
Icons.cancel,
|
||||||
|
size: 22,
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
onPressed: onClear,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _photoContent(String fullUrl, double pw, double ph) {
|
Widget _photoContent(String fullUrl, double pw, double ph) {
|
||||||
|
if (imageBytes != null && imageBytes!.isNotEmpty) {
|
||||||
|
return Image.memory(
|
||||||
|
imageBytes!,
|
||||||
|
width: pw,
|
||||||
|
height: ph,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (fullUrl.isEmpty) {
|
if (fullUrl.isEmpty) {
|
||||||
return ColoredBox(
|
return ColoredBox(
|
||||||
color: Colors.grey.shade200,
|
color: Colors.grey.shade200,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.person_off_outlined, size: 36, color: Colors.grey.shade400),
|
Icon(
|
||||||
|
onTap != null ? Icons.add_a_photo_outlined : Icons.person_off_outlined,
|
||||||
|
size: 36,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Padding(
|
||||||
'Aucune photo',
|
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
child: Text(
|
||||||
|
emptyLabel,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: Colors.grey.shade600, fontSize: 11),
|
style: TextStyle(color: Colors.grey.shade600, fontSize: 11),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -108,7 +183,11 @@ class AdminAmPhotoFrame extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
errorBuilder: (_, __, ___) => ColoredBox(
|
errorBuilder: (_, __, ___) => ColoredBox(
|
||||||
color: Colors.grey.shade200,
|
color: Colors.grey.shade200,
|
||||||
child: Icon(Icons.broken_image_outlined, size: 36, color: Colors.grey.shade400),
|
child: Icon(
|
||||||
|
Icons.broken_image_outlined,
|
||||||
|
size: 36,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
import 'package:p_tits_pas/models/assistante_maternelle_model.dart';
|
||||||
import 'package:p_tits_pas/models/enfant_admin_model.dart';
|
import 'package:p_tits_pas/models/enfant_admin_model.dart';
|
||||||
import 'package:p_tits_pas/services/api/api_config.dart';
|
import 'package:p_tits_pas/services/api/api_config.dart';
|
||||||
@ -10,22 +13,32 @@ import 'package:p_tits_pas/widgets/admin/common/admin_am_edit_modal.dart';
|
|||||||
import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart';
|
import 'package:p_tits_pas/widgets/admin/common/admin_am_photo_frame.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_parent_edit_modal.dart';
|
import 'package:p_tits_pas/widgets/admin/common/admin_parent_edit_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/admin_select_am_modal.dart';
|
import 'package:p_tits_pas/widgets/admin/common/admin_select_am_modal.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/admin/common/admin_select_famille_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
import 'package:p_tits_pas/widgets/admin/common/validation_detail_section.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
|
import 'package:p_tits_pas/widgets/admin/validation_modal_theme.dart';
|
||||||
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
import 'package:p_tits_pas/widgets/common/auth_network_image.dart';
|
||||||
|
|
||||||
/// Fiche enfant consultation / édition (ticket #138) — format paysage comme fiche AM.
|
/// Fiche enfant consultation / édition (#138) ou création (#132).
|
||||||
class AdminChildDetailModal extends StatefulWidget {
|
class AdminChildDetailModal extends StatefulWidget {
|
||||||
final EnfantAdminModel enfant;
|
final EnfantAdminModel? enfant;
|
||||||
final VoidCallback? onSaved;
|
final VoidCallback? onSaved;
|
||||||
final VoidCallback? onDeleted;
|
final VoidCallback? onDeleted;
|
||||||
|
final bool isCreating;
|
||||||
|
|
||||||
const AdminChildDetailModal({
|
const AdminChildDetailModal({
|
||||||
super.key,
|
super.key,
|
||||||
required this.enfant,
|
required EnfantAdminModel this.enfant,
|
||||||
this.onSaved,
|
this.onSaved,
|
||||||
this.onDeleted,
|
this.onDeleted,
|
||||||
});
|
}) : isCreating = false;
|
||||||
|
|
||||||
|
/// Création depuis l'onglet Enfants (#132).
|
||||||
|
const AdminChildDetailModal.create({
|
||||||
|
super.key,
|
||||||
|
this.onSaved,
|
||||||
|
}) : enfant = null,
|
||||||
|
onDeleted = null,
|
||||||
|
isCreating = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AdminChildDetailModal> createState() => _AdminChildDetailModalState();
|
State<AdminChildDetailModal> createState() => _AdminChildDetailModalState();
|
||||||
@ -49,6 +62,13 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
/// AM rattachée au chargement (pour sync différé au Sauvegarder).
|
/// AM rattachée au chargement (pour sync différé au Sauvegarder).
|
||||||
String? _baselineAmUserId;
|
String? _baselineAmUserId;
|
||||||
|
|
||||||
|
/// Famille choisie en mode création (#132).
|
||||||
|
AdminFamilleFoyer? _selectedFamily;
|
||||||
|
|
||||||
|
/// Photo locale (création) — upload multipart `photo`.
|
||||||
|
Uint8List? _photoBytes;
|
||||||
|
String? _photoFilename;
|
||||||
|
|
||||||
static const double _modalWidth = 930;
|
static const double _modalWidth = 930;
|
||||||
static const double _mainRowHeight = 380;
|
static const double _mainRowHeight = 380;
|
||||||
static const double _placementHeight = 96;
|
static const double _placementHeight = 96;
|
||||||
@ -74,6 +94,7 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
_isUnborn ? 'Date prévisionnelle' : 'Date de naissance';
|
_isUnborn ? 'Date prévisionnelle' : 'Date de naissance';
|
||||||
|
|
||||||
bool get _canDelete =>
|
bool get _canDelete =>
|
||||||
|
!widget.isCreating &&
|
||||||
(_currentUserRole ?? '').toLowerCase() == 'super_admin';
|
(_currentUserRole ?? '').toLowerCase() == 'super_admin';
|
||||||
|
|
||||||
bool get _busy => _saving || _deleting;
|
bool get _busy => _saving || _deleting;
|
||||||
@ -82,36 +103,50 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final e = widget.enfant;
|
final e = widget.enfant;
|
||||||
_prenomCtrl = TextEditingController(text: e.firstName ?? '');
|
_prenomCtrl = TextEditingController(text: e?.firstName ?? '');
|
||||||
_nomCtrl = TextEditingController(text: e.lastName ?? '');
|
_nomCtrl = TextEditingController(text: e?.lastName ?? '');
|
||||||
_birthCtrl = TextEditingController(
|
_birthCtrl = TextEditingController(
|
||||||
text: formatIsoDateFr(e.birthDate, ifEmpty: ''),
|
text: formatIsoDateFr(e?.birthDate, ifEmpty: ''),
|
||||||
);
|
);
|
||||||
_dueCtrl = TextEditingController(
|
_dueCtrl = TextEditingController(
|
||||||
text: formatIsoDateFr(e.dueDate, ifEmpty: ''),
|
text: formatIsoDateFr(e?.dueDate, ifEmpty: ''),
|
||||||
);
|
);
|
||||||
_status = normalizeEnfantStatus(e.status);
|
_status = normalizeEnfantStatus(e?.status);
|
||||||
if (!enfantStatusValues.contains(_status)) {
|
if (!enfantStatusValues.contains(_status)) {
|
||||||
_status = 'sans_garde';
|
_status = 'sans_garde';
|
||||||
}
|
}
|
||||||
_gender = _normalizeGender(e.gender, allowUnknown: _isUnborn);
|
_gender = _normalizeGender(e?.gender, allowUnknown: _isUnborn);
|
||||||
_consentPhoto = e.consentPhoto;
|
_consentPhoto = e?.consentPhoto ?? false;
|
||||||
_isMultiple = e.isMultiple;
|
_isMultiple = e?.isMultiple ?? false;
|
||||||
for (final c in [_prenomCtrl, _nomCtrl, _birthCtrl, _dueCtrl]) {
|
for (final c in [_prenomCtrl, _nomCtrl, _birthCtrl, _dueCtrl]) {
|
||||||
c.addListener(_markDirty);
|
c.addListener(_markDirty);
|
||||||
}
|
}
|
||||||
_prenomCtrl.addListener(_onNameChanged);
|
_prenomCtrl.addListener(_onNameChanged);
|
||||||
_nomCtrl.addListener(_onNameChanged);
|
_nomCtrl.addListener(_onNameChanged);
|
||||||
_loadCurrentUserRole();
|
_loadCurrentUserRole();
|
||||||
|
if (widget.isCreating) {
|
||||||
|
_loadingAm = false;
|
||||||
|
_dirty = true;
|
||||||
|
} else {
|
||||||
_loadLinkedAm();
|
_loadLinkedAm();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onNameChanged() => setState(() {});
|
void _onNameChanged() => setState(() {});
|
||||||
|
|
||||||
Future<void> _loadLinkedAm() async {
|
Future<void> _loadLinkedAm() async {
|
||||||
|
if (widget.isCreating) {
|
||||||
|
setState(() => _loadingAm = false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final enfantId = widget.enfant?.id;
|
||||||
|
if (enfantId == null || enfantId.isEmpty) {
|
||||||
|
setState(() => _loadingAm = false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setState(() => _loadingAm = true);
|
setState(() => _loadingAm = true);
|
||||||
try {
|
try {
|
||||||
final am = await UserService.findAmForEnfant(widget.enfant.id);
|
final am = await UserService.findAmForEnfant(enfantId);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
_linkedAm = am;
|
_linkedAm = am;
|
||||||
@ -168,16 +203,23 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
String? _dateToIso(String text) => parseFrDateToIso(text);
|
String? _dateToIso(String text) => parseFrDateToIso(text);
|
||||||
|
|
||||||
String _headerTitle() {
|
String _headerTitle() {
|
||||||
|
if (widget.isCreating) {
|
||||||
|
final fn = _prenomCtrl.text.trim();
|
||||||
|
final ln = _nomCtrl.text.trim();
|
||||||
|
if (fn.isEmpty && ln.isEmpty) return 'Nouvel enfant';
|
||||||
|
return '$fn $ln'.trim();
|
||||||
|
}
|
||||||
final fn = _prenomCtrl.text.trim();
|
final fn = _prenomCtrl.text.trim();
|
||||||
final ln = _nomCtrl.text.trim();
|
final ln = _nomCtrl.text.trim();
|
||||||
if (fn.isEmpty && ln.isEmpty) {
|
if (fn.isEmpty && ln.isEmpty) {
|
||||||
final fallback = widget.enfant.fullName.trim();
|
final fallback = widget.enfant?.fullName.trim() ?? '';
|
||||||
return fallback.isNotEmpty ? fallback : 'Enfant';
|
return fallback.isNotEmpty ? fallback : 'Enfant';
|
||||||
}
|
}
|
||||||
return '$fn $ln'.trim();
|
return '$fn $ln'.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<EnfantParentLink> get _parentLinks => widget.enfant.parentLinks
|
List<EnfantParentLink> get _parentLinks =>
|
||||||
|
(widget.enfant?.parentLinks ?? const <EnfantParentLink>[])
|
||||||
.where((l) => l.parentId.trim().isNotEmpty)
|
.where((l) => l.parentId.trim().isNotEmpty)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
@ -205,6 +247,17 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget? _parentsSubtitle() {
|
Widget? _parentsSubtitle() {
|
||||||
|
if (widget.isCreating) {
|
||||||
|
final family = _selectedFamily;
|
||||||
|
if (family == null) return null;
|
||||||
|
return Text(
|
||||||
|
family.parentNames.isNotEmpty
|
||||||
|
? 'Famille : ${family.parentNames.join(', ')}'
|
||||||
|
: family.displayTitle,
|
||||||
|
style: const TextStyle(fontSize: 13, color: Colors.black54),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final links = _parentLinks;
|
final links = _parentLinks;
|
||||||
if (links.isEmpty) return null;
|
if (links.isEmpty) return null;
|
||||||
|
|
||||||
@ -252,12 +305,21 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
|
|
||||||
Future<void> _save() async {
|
Future<void> _save() async {
|
||||||
if (!_dirty) return;
|
if (!_dirty) return;
|
||||||
|
|
||||||
|
if (widget.isCreating) {
|
||||||
|
await _saveCreate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final enfantId = widget.enfant?.id;
|
||||||
|
if (enfantId == null || enfantId.isEmpty) return;
|
||||||
|
|
||||||
setState(() => _saving = true);
|
setState(() => _saving = true);
|
||||||
try {
|
try {
|
||||||
await _syncAmPlacement();
|
await _syncAmPlacement();
|
||||||
|
|
||||||
await UserService.updateEnfant(
|
await UserService.updateEnfant(
|
||||||
enfantId: widget.enfant.id,
|
enfantId: enfantId,
|
||||||
body: {
|
body: {
|
||||||
'first_name': _prenomCtrl.text.trim(),
|
'first_name': _prenomCtrl.text.trim(),
|
||||||
'last_name': _nomCtrl.text.trim(),
|
'last_name': _nomCtrl.text.trim(),
|
||||||
@ -291,21 +353,99 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _saveCreate() async {
|
||||||
|
final family = _selectedFamily;
|
||||||
|
if (family == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Choisissez une famille / un dossier avant d\'enregistrer'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final prenom = _prenomCtrl.text.trim();
|
||||||
|
final nom = _nomCtrl.text.trim();
|
||||||
|
if (prenom.isEmpty || nom.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Prénom et nom sont obligatoires')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_isUnborn && _dateToIso(_birthCtrl.text) == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Date de naissance invalide ou manquante')),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final hasPhoto = _photoBytes != null && _photoBytes!.isNotEmpty;
|
||||||
|
if (hasPhoto && !_consentPhoto) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Activez le consentement photo pour enregistrer une photo',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() => _saving = true);
|
||||||
|
try {
|
||||||
|
await UserService.createEnfant(
|
||||||
|
parentUserId: family.pivotParentUserId,
|
||||||
|
photoBytes: hasPhoto ? _photoBytes : null,
|
||||||
|
photoFilename: _photoFilename,
|
||||||
|
body: {
|
||||||
|
'first_name': prenom,
|
||||||
|
'last_name': nom,
|
||||||
|
'status': _status,
|
||||||
|
'gender': _gender,
|
||||||
|
if (_isUnborn)
|
||||||
|
if (_dateToIso(_dueCtrl.text) != null)
|
||||||
|
'due_date': _dateToIso(_dueCtrl.text)
|
||||||
|
else if (_dateToIso(_birthCtrl.text) != null)
|
||||||
|
'birth_date': _dateToIso(_birthCtrl.text),
|
||||||
|
'consent_photo': _consentPhoto,
|
||||||
|
'is_multiple': _isMultiple,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_dirty = false;
|
||||||
|
_saving = false;
|
||||||
|
});
|
||||||
|
widget.onSaved?.call();
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Enfant créé')),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _saving = false);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(e.toString().replaceFirst('Exception: ', ''))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Applique rattachement / détachement AM (différé jusqu'au Sauvegarder).
|
/// Applique rattachement / détachement AM (différé jusqu'au Sauvegarder).
|
||||||
Future<void> _syncAmPlacement() async {
|
Future<void> _syncAmPlacement() async {
|
||||||
|
final enfantId = widget.enfant?.id;
|
||||||
|
if (enfantId == null || enfantId.isEmpty) return;
|
||||||
|
|
||||||
final baselineId = _baselineAmUserId;
|
final baselineId = _baselineAmUserId;
|
||||||
final currentId = _linkedAm?.user.id;
|
final currentId = _linkedAm?.user.id;
|
||||||
|
|
||||||
if (baselineId != null && baselineId != currentId) {
|
if (baselineId != null && baselineId != currentId) {
|
||||||
await UserService.detachEnfantFromAm(
|
await UserService.detachEnfantFromAm(
|
||||||
amUserId: baselineId,
|
amUserId: baselineId,
|
||||||
enfantId: widget.enfant.id,
|
enfantId: enfantId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (currentId != null && currentId != baselineId) {
|
if (currentId != null && currentId != baselineId) {
|
||||||
await UserService.attachEnfantToAm(
|
await UserService.attachEnfantToAm(
|
||||||
amUserId: currentId,
|
amUserId: currentId,
|
||||||
enfantId: widget.enfant.id,
|
enfantId: enfantId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -342,7 +482,9 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
|
|
||||||
setState(() => _deleting = true);
|
setState(() => _deleting = true);
|
||||||
try {
|
try {
|
||||||
await UserService.deleteEnfant(widget.enfant.id);
|
final id = widget.enfant?.id;
|
||||||
|
if (id == null || id.isEmpty) return;
|
||||||
|
await UserService.deleteEnfant(id);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
widget.onDeleted?.call();
|
widget.onDeleted?.call();
|
||||||
widget.onSaved?.call();
|
widget.onSaved?.call();
|
||||||
@ -376,10 +518,12 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
|
|
||||||
/// Recharge AM + statut après une modale externe (ex. fiche AM).
|
/// Recharge AM + statut après une modale externe (ex. fiche AM).
|
||||||
Future<void> _reloadPlacementFromServer() async {
|
Future<void> _reloadPlacementFromServer() async {
|
||||||
|
final id = widget.enfant?.id;
|
||||||
|
if (id == null || id.isEmpty) return;
|
||||||
try {
|
try {
|
||||||
final results = await Future.wait([
|
final results = await Future.wait([
|
||||||
UserService.findAmForEnfant(widget.enfant.id),
|
UserService.findAmForEnfant(id),
|
||||||
UserService.getEnfant(widget.enfant.id),
|
UserService.getEnfant(id),
|
||||||
]);
|
]);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
final am = results[0] as AssistanteMaternelleModel?;
|
final am = results[0] as AssistanteMaternelleModel?;
|
||||||
@ -603,6 +747,43 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _pickPhoto() async {
|
||||||
|
if (_busy || !widget.isCreating) return;
|
||||||
|
try {
|
||||||
|
final picked = await ImagePicker().pickImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
imageQuality: 75,
|
||||||
|
maxWidth: 1200,
|
||||||
|
maxHeight: 1200,
|
||||||
|
);
|
||||||
|
if (picked == null) return;
|
||||||
|
final bytes = await picked.readAsBytes();
|
||||||
|
if (bytes.isEmpty) return;
|
||||||
|
final name = picked.name.trim();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_photoBytes = bytes;
|
||||||
|
_photoFilename = name.isNotEmpty ? name : 'photo.jpg';
|
||||||
|
_consentPhoto = true;
|
||||||
|
_dirty = true;
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Impossible de charger la photo')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearPhoto() {
|
||||||
|
if (_busy || !widget.isCreating) return;
|
||||||
|
setState(() {
|
||||||
|
_photoBytes = null;
|
||||||
|
_photoFilename = null;
|
||||||
|
_dirty = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Widget _mainRow() {
|
Widget _mainRow() {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, c) {
|
builder: (context, c) {
|
||||||
@ -625,7 +806,20 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: AdminAmPhotoFrame(
|
child: AdminAmPhotoFrame(
|
||||||
photoUrl: widget.enfant.photoUrl,
|
photoUrl: widget.isCreating
|
||||||
|
? null
|
||||||
|
: widget.enfant?.photoUrl,
|
||||||
|
imageBytes: widget.isCreating ? _photoBytes : null,
|
||||||
|
onTap: widget.isCreating && !_busy ? _pickPhoto : null,
|
||||||
|
onClear: widget.isCreating &&
|
||||||
|
!_busy &&
|
||||||
|
_photoBytes != null &&
|
||||||
|
_photoBytes!.isNotEmpty
|
||||||
|
? _clearPhoto
|
||||||
|
: null,
|
||||||
|
emptyLabel: widget.isCreating
|
||||||
|
? 'Cliquer pour ajouter'
|
||||||
|
: 'Aucune photo',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@ -893,8 +1087,10 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get _placementTitle =>
|
String get _placementTitle {
|
||||||
_isScolarise ? 'Scolarisation' : 'Assistante maternelle';
|
if (widget.isCreating) return 'Famille / dossier';
|
||||||
|
return _isScolarise ? 'Scolarisation' : 'Assistante maternelle';
|
||||||
|
}
|
||||||
|
|
||||||
Widget _placementBlock() {
|
Widget _placementBlock() {
|
||||||
return Column(
|
return Column(
|
||||||
@ -910,11 +1106,161 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_placementSection(),
|
if (widget.isCreating) _familyPlacementSection() else _placementSection(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _pickFamily() async {
|
||||||
|
if (_busy) return;
|
||||||
|
final selected = await AdminSelectFamilleModal.show(
|
||||||
|
context,
|
||||||
|
title: 'Choisir une famille',
|
||||||
|
);
|
||||||
|
if (selected == null || !mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_selectedFamily = selected;
|
||||||
|
_dirty = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _familyPlacementSection() {
|
||||||
|
final family = _selectedFamily;
|
||||||
|
if (family == null) {
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: _busy ? null : _pickFamily,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Container(
|
||||||
|
height: _placementHeight,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.family_restroom,
|
||||||
|
size: 30,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Aucune famille sélectionnée',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Cliquer pour choisir un dossier / foyer',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(Icons.add_circle_outline, color: Colors.grey.shade500),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: _busy ? null : _pickFamily,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: Container(
|
||||||
|
height: _placementHeight,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF8F5FC),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
border: Border.all(color: const Color(0xFFD8CCE8)),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFEDE5FA),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.family_restroom,
|
||||||
|
size: 30,
|
||||||
|
color: Color(0xFF6B3FA0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
family.displayTitle,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (family.parentNames.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
family.parentNames.join(', '),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.black54,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.swap_horiz, color: Colors.grey.shade700),
|
||||||
|
tooltip: 'Changer de famille',
|
||||||
|
onPressed: _busy ? null : _pickFamily,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _placementSection() {
|
Widget _placementSection() {
|
||||||
if (!_showsAmPlacement) return _scolariseCard();
|
if (!_showsAmPlacement) return _scolariseCard();
|
||||||
|
|
||||||
@ -976,7 +1322,11 @@ class _AdminChildDetailModalState extends State<AdminChildDetailModal> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Text(_dirty ? 'Sauvegarder' : 'Aucune modification'),
|
: Text(
|
||||||
|
widget.isCreating
|
||||||
|
? 'Créer'
|
||||||
|
: (_dirty ? 'Sauvegarder' : 'Aucune modification'),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,127 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:p_tits_pas/models/parent_model.dart';
|
||||||
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/admin/common/admin_select_list_modal.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/admin/common/admin_user_card.dart';
|
||||||
|
|
||||||
|
/// Foyer / famille sélectionnable pour rattacher un nouvel enfant (#132).
|
||||||
|
class AdminFamilleFoyer {
|
||||||
|
/// Parent pivot pour `POST /enfants` (`parent_user_id`).
|
||||||
|
final String pivotParentUserId;
|
||||||
|
final String? numeroDossier;
|
||||||
|
final String displayTitle;
|
||||||
|
final List<String> parentNames;
|
||||||
|
|
||||||
|
const AdminFamilleFoyer({
|
||||||
|
required this.pivotParentUserId,
|
||||||
|
required this.displayTitle,
|
||||||
|
required this.parentNames,
|
||||||
|
this.numeroDossier,
|
||||||
|
});
|
||||||
|
|
||||||
|
String get subtitle {
|
||||||
|
final parts = <String>[];
|
||||||
|
final dossier = (numeroDossier ?? '').trim();
|
||||||
|
if (dossier.isNotEmpty) parts.add('Dossier $dossier');
|
||||||
|
if (parentNames.isNotEmpty) {
|
||||||
|
parts.add('Responsables : ${parentNames.join(', ')}');
|
||||||
|
}
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit la liste des foyers uniques à partir de `GET /parents`.
|
||||||
|
List<AdminFamilleFoyer> buildFamilleFoyers(List<ParentModel> parents) {
|
||||||
|
final seenDossiers = <String>{};
|
||||||
|
final seenUserIds = <String>{};
|
||||||
|
final foyers = <AdminFamilleFoyer>[];
|
||||||
|
|
||||||
|
for (final p in parents) {
|
||||||
|
final dossier = (p.user.numeroDossier ?? '').trim();
|
||||||
|
if (dossier.isNotEmpty) {
|
||||||
|
if (seenDossiers.contains(dossier)) continue;
|
||||||
|
seenDossiers.add(dossier);
|
||||||
|
} else if (seenUserIds.contains(p.user.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seenUserIds.add(p.user.id);
|
||||||
|
final co = p.coParent;
|
||||||
|
if (co != null) seenUserIds.add(co.id);
|
||||||
|
|
||||||
|
final names = <String>[
|
||||||
|
if (p.user.fullName.trim().isNotEmpty) p.user.fullName.trim(),
|
||||||
|
if (co != null && co.fullName.trim().isNotEmpty) co.fullName.trim(),
|
||||||
|
];
|
||||||
|
|
||||||
|
final title = dossier.isNotEmpty
|
||||||
|
? 'Dossier $dossier'
|
||||||
|
: (names.isNotEmpty ? names.first : 'Famille');
|
||||||
|
|
||||||
|
foyers.add(
|
||||||
|
AdminFamilleFoyer(
|
||||||
|
pivotParentUserId: p.user.id,
|
||||||
|
numeroDossier: dossier.isNotEmpty ? dossier : null,
|
||||||
|
displayTitle: title,
|
||||||
|
parentNames: names,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
foyers.sort(
|
||||||
|
(a, b) => a.displayTitle.toLowerCase().compareTo(b.displayTitle.toLowerCase()),
|
||||||
|
);
|
||||||
|
return foyers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sélection d'une famille / dossier pour créer un enfant — ticket #132.
|
||||||
|
class AdminSelectFamilleModal {
|
||||||
|
AdminSelectFamilleModal._();
|
||||||
|
|
||||||
|
static Future<AdminFamilleFoyer?> show(
|
||||||
|
BuildContext context, {
|
||||||
|
String title = 'Choisir une famille',
|
||||||
|
}) {
|
||||||
|
return AdminSelectListModal.show<AdminFamilleFoyer>(
|
||||||
|
context,
|
||||||
|
title: title,
|
||||||
|
searchHint: 'Rechercher par dossier, nom…',
|
||||||
|
emptyMessage: 'Aucune famille disponible',
|
||||||
|
noResultsMessage: 'Aucun résultat pour cette recherche',
|
||||||
|
loadItems: () async {
|
||||||
|
final parents = await UserService.getParents();
|
||||||
|
return buildFamilleFoyers(parents);
|
||||||
|
},
|
||||||
|
matchesQuery: (f, q) {
|
||||||
|
final dossier = (f.numeroDossier ?? '').toLowerCase();
|
||||||
|
final title = f.displayTitle.toLowerCase();
|
||||||
|
final names = f.parentNames.join(' ').toLowerCase();
|
||||||
|
return dossier.contains(q) || title.contains(q) || names.contains(q);
|
||||||
|
},
|
||||||
|
itemBuilder: (context, f, onSelect) {
|
||||||
|
return AdminUserCard(
|
||||||
|
title: f.displayTitle,
|
||||||
|
fallbackIcon: Icons.family_restroom,
|
||||||
|
subtitleLines: [
|
||||||
|
if (f.parentNames.isNotEmpty) f.parentNames.join(', '),
|
||||||
|
if ((f.numeroDossier ?? '').isNotEmpty)
|
||||||
|
'Dossier ${f.numeroDossier}',
|
||||||
|
],
|
||||||
|
onCardTap: onSelect,
|
||||||
|
margin: const EdgeInsets.only(bottom: 4),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 5,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.check_circle_outline),
|
||||||
|
tooltip: 'Choisir',
|
||||||
|
onPressed: onSelect,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import 'package:p_tits_pas/screens/administrateurs/creation/gestionnaires_create
|
|||||||
import 'package:p_tits_pas/services/user_service.dart';
|
import 'package:p_tits_pas/services/user_service.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart';
|
import 'package:p_tits_pas/widgets/admin/admin_management_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart';
|
import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart';
|
||||||
|
import 'package:p_tits_pas/widgets/admin/common/admin_child_detail_modal.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/enfant_management_widget.dart';
|
import 'package:p_tits_pas/widgets/admin/enfant_management_widget.dart';
|
||||||
import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart';
|
import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart';
|
||||||
@ -26,6 +27,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
|||||||
int _subIndex = 0;
|
int _subIndex = 0;
|
||||||
int _gestionnaireRefreshTick = 0;
|
int _gestionnaireRefreshTick = 0;
|
||||||
int _adminRefreshTick = 0;
|
int _adminRefreshTick = 0;
|
||||||
|
int _enfantRefreshTick = 0;
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final TextEditingController _amCapacityController = TextEditingController();
|
final TextEditingController _amCapacityController = TextEditingController();
|
||||||
String? _parentStatus;
|
String? _parentStatus;
|
||||||
@ -264,6 +266,7 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
|||||||
);
|
);
|
||||||
case 1:
|
case 1:
|
||||||
return EnfantManagementWidget(
|
return EnfantManagementWidget(
|
||||||
|
key: ValueKey('enfants-$_enfantRefreshTick'),
|
||||||
searchQuery: _searchController.text,
|
searchQuery: _searchController.text,
|
||||||
statusFilter: _enfantStatus,
|
statusFilter: _enfantStatus,
|
||||||
);
|
);
|
||||||
@ -311,6 +314,23 @@ class _UserManagementPanelState extends State<UserManagementPanel> {
|
|||||||
|
|
||||||
Future<void> _handleAddPressed() async {
|
Future<void> _handleAddPressed() async {
|
||||||
final contentIndex = _subIndex - _contentIndexOffset;
|
final contentIndex = _subIndex - _contentIndexOffset;
|
||||||
|
|
||||||
|
if (contentIndex == 1) {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
return AdminChildDetailModal.create(
|
||||||
|
onSaved: () {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _enfantRefreshTick++);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (contentIndex == 3) {
|
if (contentIndex == 3) {
|
||||||
final created = await showDialog<bool>(
|
final created = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
@ -206,7 +206,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.2.2"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
|
||||||
|
|||||||
@ -21,6 +21,7 @@ dependencies:
|
|||||||
js: ^0.6.7
|
js: ^0.6.7
|
||||||
url_launcher: ^6.2.4
|
url_launcher: ^6.2.4
|
||||||
http: ^1.2.2
|
http: ^1.2.2
|
||||||
|
http_parser: ^4.0.2
|
||||||
# flutter_secure_storage: ^9.0.0
|
# flutter_secure_storage: ^9.0.0
|
||||||
pdfx: ^2.5.0
|
pdfx: ^2.5.0
|
||||||
universal_platform: ^1.1.0
|
universal_platform: ^1.1.0
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user