Merge master into develop - Synchronisation des branches
Fusion des travaux : - Backend complet (ConfigService, DocumentsLegaux, Auth, etc.) - Frontend étapes inscription 1-2 - Infrastructure Docker - Documentation technique Résolution des conflits : - Images déplacées vers frontend/assets/images/ - Dossier Archives supprimé - Backend : version master conservée - Frontend : améliorations UI de develop conservées
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
class Env {
|
||||
// Base URL de l'API, surchargeable à la compilation via --dart-define=API_BASE_URL
|
||||
static const String apiBaseUrl = String.fromEnvironment(
|
||||
'API_BASE_URL',
|
||||
defaultValue: 'https://ynov.ptits-pas.fr',
|
||||
);
|
||||
|
||||
// Construit une URL vers l'API v1 à partir d'un chemin (commençant par '/')
|
||||
static String apiV1(String path) => "${apiBaseUrl}/api/v1$path";
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/assistant_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/child_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/contract_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/conversation_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/event_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/notification_model.dart';
|
||||
import 'package:p_tits_pas/services/dashboardService.dart';
|
||||
|
||||
class ParentDashboardController extends ChangeNotifier {
|
||||
final DashboardService _dashboardService;
|
||||
|
||||
ParentDashboardController(this._dashboardService);
|
||||
|
||||
// État des données
|
||||
List<ChildModel> _children = [];
|
||||
String? _selectedChildId;
|
||||
AssistantModel? _selectedAssistant;
|
||||
List<EventModel> _upcomingEvents = [];
|
||||
List<ContractModel> _contracts = [];
|
||||
List<ConversationModel> _conversations = [];
|
||||
List<NotificationModel> _notifications = [];
|
||||
|
||||
// État de chargement
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
|
||||
// Getters
|
||||
List<ChildModel> get children => _children;
|
||||
String? get selectedChildId => _selectedChildId;
|
||||
ChildModel? get selectedChild => _children.where((c) => c.id == _selectedChildId).firstOrNull;
|
||||
AssistantModel? get selectedAssistant => _selectedAssistant;
|
||||
List<EventModel> get upcomingEvents => _upcomingEvents;
|
||||
List<ContractModel> get contracts => _contracts;
|
||||
List<ConversationModel> get conversations => _conversations;
|
||||
List<NotificationModel> get notifications => _notifications;
|
||||
bool get isLoading => _isLoading;
|
||||
String? get error => _error;
|
||||
|
||||
// Initialisation du dashboard
|
||||
Future<void> initDashboard() async {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await Future.wait([
|
||||
_loadChildren(),
|
||||
_loadUpcomingEvents(),
|
||||
_loadContracts(),
|
||||
_loadConversations(),
|
||||
_loadNotifications(),
|
||||
]);
|
||||
|
||||
// Sélectionner le premier enfant par défaut
|
||||
if (_children.isNotEmpty && _selectedChildId == null) {
|
||||
await selectChild(_children.first.id);
|
||||
}
|
||||
} catch (e) {
|
||||
_error = 'Erreur lors du chargement du tableau de bord: $e';
|
||||
} finally {
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Sélection d'un enfant
|
||||
Future<void> selectChild(String childId) async {
|
||||
_selectedChildId = childId;
|
||||
notifyListeners();
|
||||
|
||||
// Charger les données spécifiques à cet enfant
|
||||
await _loadChildSpecificData(childId);
|
||||
}
|
||||
|
||||
// Afficher le modal d'ajout d'enfant
|
||||
void showAddChildModal() {
|
||||
// Logique pour ouvrir le modal d'ajout d'enfant
|
||||
// Sera implémentée dans le ticket FRONT-09
|
||||
}
|
||||
|
||||
// Méthodes privées de chargement des données
|
||||
Future<void> _loadChildren() async {
|
||||
_children = await _dashboardService.getChildren();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _loadChildSpecificData(String childId) async {
|
||||
try {
|
||||
// Charger l'assistante maternelle associée à cet enfant
|
||||
_selectedAssistant = await _dashboardService.getAssistantForChild(childId);
|
||||
|
||||
// Filtrer les événements et contrats pour cet enfant
|
||||
_upcomingEvents = await _dashboardService.getEventsForChild(childId);
|
||||
_contracts = await _dashboardService.getContractsForChild(childId);
|
||||
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
_error = 'Erreur lors du chargement des données pour l\'enfant: $e';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadUpcomingEvents() async {
|
||||
_upcomingEvents = await _dashboardService.getUpcomingEvents();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _loadContracts() async {
|
||||
_contracts = await _dashboardService.getContracts();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _loadConversations() async {
|
||||
_conversations = await _dashboardService.getConversations();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _loadNotifications() async {
|
||||
_notifications = await _dashboardService.getNotifications();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Méthodes d'action
|
||||
Future<void> markNotificationAsRead(String notificationId) async {
|
||||
try {
|
||||
await _dashboardService.markNotificationAsRead(notificationId);
|
||||
await _loadNotifications(); // Recharger les notifications
|
||||
} catch (e) {
|
||||
_error = 'Erreur lors du marquage de la notification: $e';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refreshDashboard() async {
|
||||
await initDashboard();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import 'dart:io';
|
||||
|
||||
class ChildminderId {
|
||||
String firstName;
|
||||
String lastName;
|
||||
String address;
|
||||
String postalCode;
|
||||
String city;
|
||||
String phone;
|
||||
String email;
|
||||
String password;
|
||||
File? profilePicture;
|
||||
bool photoConsent;
|
||||
|
||||
ChildminderId({
|
||||
this.firstName = '',
|
||||
this.lastName = '',
|
||||
this.address = '',
|
||||
this.postalCode = '',
|
||||
this.city = '',
|
||||
this.phone = '',
|
||||
this.email = '',
|
||||
this.password = '',
|
||||
this.profilePicture,
|
||||
this.photoConsent = false,
|
||||
});
|
||||
}
|
||||
|
||||
class ChildminderProfessional {
|
||||
String dateOfBirth;
|
||||
String birthCity;
|
||||
String birthCountry;
|
||||
String socialSecurityNumber; // NIR
|
||||
String agreementNumber;
|
||||
int maxChildren;
|
||||
|
||||
ChildminderProfessional({
|
||||
this.dateOfBirth = '',
|
||||
this.birthCity = '',
|
||||
this.birthCountry = '',
|
||||
this.socialSecurityNumber = '',
|
||||
this.agreementNumber = '',
|
||||
this.maxChildren = 1,
|
||||
});
|
||||
}
|
||||
|
||||
class ChildminderRegistrationData {
|
||||
ChildminderId identity;
|
||||
ChildminderProfessional professional;
|
||||
String presentationMessage;
|
||||
bool cguAccepted;
|
||||
bool isPhotoRequired;
|
||||
|
||||
ChildminderRegistrationData({
|
||||
ChildminderId? identityData,
|
||||
ChildminderProfessional? professionalData,
|
||||
this.presentationMessage = '',
|
||||
this.cguAccepted = false,
|
||||
this.isPhotoRequired = false,
|
||||
}) : identity = identityData ?? ChildminderId(),
|
||||
professional = professionalData ?? ChildminderProfessional();
|
||||
|
||||
void updateIdentity(ChildminderId data) {
|
||||
identity = data;
|
||||
}
|
||||
|
||||
void updateProfessional(ChildminderProfessional data) {
|
||||
professional = data;
|
||||
}
|
||||
|
||||
void updatePresentation(String message) {
|
||||
presentationMessage = message;
|
||||
}
|
||||
|
||||
void acceptCGU() {
|
||||
cguAccepted = true;
|
||||
}
|
||||
|
||||
bool get isComplete {
|
||||
return identity.firstName.isNotEmpty &&
|
||||
identity.lastName.isNotEmpty &&
|
||||
identity.address.isNotEmpty &&
|
||||
identity.postalCode.isNotEmpty &&
|
||||
identity.city.isNotEmpty &&
|
||||
identity.phone.isNotEmpty &&
|
||||
identity.email.isNotEmpty &&
|
||||
identity.password.isNotEmpty &&
|
||||
professional.dateOfBirth.isNotEmpty &&
|
||||
professional.birthCity.isNotEmpty &&
|
||||
professional.birthCountry.isNotEmpty &&
|
||||
professional.socialSecurityNumber.isNotEmpty &&
|
||||
professional.agreementNumber.isNotEmpty &&
|
||||
cguAccepted &&
|
||||
(!isPhotoRequired || (identity.profilePicture != null && identity.photoConsent));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
class AssistantModel {
|
||||
final String id;
|
||||
final String firstName;
|
||||
final String lastName;
|
||||
final String? photoUrl;
|
||||
final double hourlyRate;
|
||||
final double dailyFees;
|
||||
final AssistantStatus status;
|
||||
final String? address;
|
||||
final String? phone;
|
||||
final String? email;
|
||||
|
||||
AssistantModel({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
required this.lastName,
|
||||
this.photoUrl,
|
||||
required this.hourlyRate,
|
||||
required this.dailyFees,
|
||||
required this.status,
|
||||
this.address,
|
||||
this.phone,
|
||||
this.email,
|
||||
});
|
||||
|
||||
factory AssistantModel.fromJson(Map<String, dynamic> json) {
|
||||
return AssistantModel(
|
||||
id: json['id'],
|
||||
firstName: json['firstName'],
|
||||
lastName: json['lastName'],
|
||||
photoUrl: json['photoUrl'],
|
||||
hourlyRate: json['hourlyRate'].toDouble(),
|
||||
dailyFees: json['dailyFees'].toDouble(),
|
||||
status: AssistantStatus.values.byName(json['status']),
|
||||
address: json['address'],
|
||||
phone: json['phone'],
|
||||
email: json['email'],
|
||||
);
|
||||
}
|
||||
|
||||
String get fullName => '$firstName $lastName';
|
||||
String get hourlyRateFormatted => '${hourlyRate.toStringAsFixed(2)} €/h';
|
||||
String get dailyFeesFormatted => '${dailyFees.toStringAsFixed(2)} €/jour';
|
||||
}
|
||||
|
||||
enum AssistantStatus {
|
||||
available,
|
||||
busy,
|
||||
onHoliday,
|
||||
unavailable,
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
class ChildModel {
|
||||
final String id;
|
||||
final String firstName;
|
||||
final String? lastName;
|
||||
final String? photoUrl;
|
||||
final DateTime birthDate;
|
||||
final ChildStatus status;
|
||||
final String? assistantId;
|
||||
|
||||
ChildModel({
|
||||
required this.id,
|
||||
required this.firstName,
|
||||
this.lastName,
|
||||
this.photoUrl,
|
||||
required this.birthDate,
|
||||
required this.status,
|
||||
this.assistantId,
|
||||
});
|
||||
|
||||
factory ChildModel.fromJson(Map<String, dynamic> json) {
|
||||
return ChildModel(
|
||||
id: json['id'],
|
||||
firstName: json['firstName'],
|
||||
lastName: json['lastName'],
|
||||
photoUrl: json['photoUrl'],
|
||||
birthDate: DateTime.parse(json['birthDate']),
|
||||
status: ChildStatus.values.byName(json['status']),
|
||||
assistantId: json['assistantId'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'firstName': firstName,
|
||||
'lastName': lastName,
|
||||
'photoUrl': photoUrl,
|
||||
'birthDate': birthDate.toIso8601String(),
|
||||
'status': status.name,
|
||||
'assistantId': assistantId,
|
||||
};
|
||||
}
|
||||
|
||||
String get fullName => lastName != null ? '$firstName $lastName' : firstName;
|
||||
|
||||
int get ageInMonths {
|
||||
final now = DateTime.now();
|
||||
return (now.year - birthDate.year) * 12 + (now.month - birthDate.month);
|
||||
}
|
||||
}
|
||||
|
||||
enum ChildStatus {
|
||||
withAssistant, // En garde chez l'assistante
|
||||
available, // Disponible
|
||||
onHoliday, // En vacances
|
||||
sick, // Malade
|
||||
searching, // Recherche d'assistante
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
class ContractModel {
|
||||
final String id;
|
||||
final String childId;
|
||||
final String assistantId;
|
||||
final ContractStatus status;
|
||||
final DateTime startDate;
|
||||
final DateTime? endDate;
|
||||
final double hourlyRate;
|
||||
final Map<String, dynamic>? terms;
|
||||
final DateTime createdAt;
|
||||
final DateTime? signedAt;
|
||||
|
||||
ContractModel({
|
||||
required this.id,
|
||||
required this.childId,
|
||||
required this.assistantId,
|
||||
required this.status,
|
||||
required this.startDate,
|
||||
this.endDate,
|
||||
required this.hourlyRate,
|
||||
this.terms,
|
||||
required this.createdAt,
|
||||
this.signedAt,
|
||||
});
|
||||
|
||||
factory ContractModel.fromJson(Map<String, dynamic> json) {
|
||||
return ContractModel(
|
||||
id: json['id'],
|
||||
childId: json['childId'],
|
||||
assistantId: json['assistantId'],
|
||||
status: ContractStatus.values.byName(json['status']),
|
||||
startDate: DateTime.parse(json['startDate']),
|
||||
endDate: json['endDate'] != null ? DateTime.parse(json['endDate']) : null,
|
||||
hourlyRate: json['hourlyRate'].toDouble(),
|
||||
terms: json['terms'],
|
||||
createdAt: DateTime.parse(json['createdAt']),
|
||||
signedAt: json['signedAt'] != null ? DateTime.parse(json['signedAt']) : null,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isActive => status == ContractStatus.active;
|
||||
bool get needsSignature => status == ContractStatus.draft;
|
||||
String get statusLabel {
|
||||
switch (status) {
|
||||
case ContractStatus.draft:
|
||||
return 'Brouillon';
|
||||
case ContractStatus.pending:
|
||||
return 'En attente de validation';
|
||||
case ContractStatus.active:
|
||||
return 'En cours';
|
||||
case ContractStatus.ended:
|
||||
return 'Terminé';
|
||||
case ContractStatus.cancelled:
|
||||
return 'Annulé';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ContractStatus {
|
||||
draft,
|
||||
pending,
|
||||
active,
|
||||
ended,
|
||||
cancelled,
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
class ConversationModel {
|
||||
final String id;
|
||||
final String title;
|
||||
final List<String> participantIds;
|
||||
final List<MessageModel> messages;
|
||||
final DateTime lastMessageAt;
|
||||
final int unreadCount;
|
||||
final String? childId;
|
||||
|
||||
ConversationModel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.participantIds,
|
||||
required this.messages,
|
||||
required this.lastMessageAt,
|
||||
this.unreadCount = 0,
|
||||
this.childId,
|
||||
});
|
||||
|
||||
MessageModel? get lastMessage => messages.isNotEmpty ? messages.last : null;
|
||||
bool get hasUnreadMessages => unreadCount > 0;
|
||||
}
|
||||
|
||||
class MessageModel {
|
||||
final String id;
|
||||
final String content;
|
||||
final String senderId;
|
||||
final DateTime sentAt;
|
||||
final bool isFromAI;
|
||||
final MessageStatus status;
|
||||
|
||||
MessageModel({
|
||||
required this.id,
|
||||
required this.content,
|
||||
required this.senderId,
|
||||
required this.sentAt,
|
||||
this.isFromAI = false,
|
||||
required this.status,
|
||||
});
|
||||
}
|
||||
|
||||
enum MessageStatus {
|
||||
sent,
|
||||
delivered,
|
||||
read,
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
class EventModel {
|
||||
final String id;
|
||||
final String title;
|
||||
final String? description;
|
||||
final DateTime startDate;
|
||||
final DateTime? endDate;
|
||||
final EventType type;
|
||||
final EventStatus status;
|
||||
final String? childId;
|
||||
final String? assistantId;
|
||||
final String? createdBy;
|
||||
|
||||
EventModel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.description,
|
||||
required this.startDate,
|
||||
this.endDate,
|
||||
required this.type,
|
||||
required this.status,
|
||||
this.childId,
|
||||
this.assistantId,
|
||||
this.createdBy,
|
||||
});
|
||||
|
||||
factory EventModel.fromJson(Map<String, dynamic> json) {
|
||||
return EventModel(
|
||||
id: json['id'],
|
||||
title: json['title'],
|
||||
description: json['description'],
|
||||
startDate: DateTime.parse(json['startDate']),
|
||||
endDate: json['endDate'] != null ? DateTime.parse(json['endDate']) : null,
|
||||
type: EventType.values.byName(json['type']),
|
||||
status: EventStatus.values.byName(json['status']),
|
||||
childId: json['childId'],
|
||||
assistantId: json['assistantId'],
|
||||
createdBy: json['createdBy'],
|
||||
);
|
||||
}
|
||||
|
||||
bool get isMultiDay => endDate != null && !isSameDay(startDate, endDate!);
|
||||
bool get isPending => status == EventStatus.pending;
|
||||
bool get needsConfirmation => isPending && createdBy != 'current_user';
|
||||
|
||||
static bool isSameDay(DateTime date1, DateTime date2) {
|
||||
return date1.year == date2.year &&
|
||||
date1.month == date2.month &&
|
||||
date1.day == date2.day;
|
||||
}
|
||||
}
|
||||
|
||||
enum EventType {
|
||||
parentVacation, // Vacances parents
|
||||
childAbsence, // Absence enfant
|
||||
rpeActivity, // Activité RPE
|
||||
assistantVacation, // Congés assistante maternelle
|
||||
sickLeave, // Arrêt maladie
|
||||
personalNote, // Note personnelle
|
||||
}
|
||||
|
||||
enum EventStatus {
|
||||
confirmed,
|
||||
pending,
|
||||
refused,
|
||||
cancelled,
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
class NotificationModel {
|
||||
final String id;
|
||||
final String title;
|
||||
final String content;
|
||||
final NotificationType type;
|
||||
final DateTime createdAt;
|
||||
final bool isRead;
|
||||
final String? actionUrl;
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
NotificationModel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.content,
|
||||
required this.type,
|
||||
required this.createdAt,
|
||||
this.isRead = false,
|
||||
this.actionUrl,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
factory NotificationModel.fromJson(Map<String, dynamic> json) {
|
||||
return NotificationModel(
|
||||
id: json['id'],
|
||||
title: json['title'],
|
||||
content: json['content'],
|
||||
type: NotificationType.values.byName(json['type']),
|
||||
createdAt: DateTime.parse(json['createdAt']),
|
||||
isRead: json['isRead'] ?? false,
|
||||
actionUrl: json['actionUrl'],
|
||||
metadata: json['metadata'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum NotificationType {
|
||||
newEvent, // Nouvel événement
|
||||
fileModified, // Dossier modifié
|
||||
contractPending, // Contrat en attente
|
||||
paymentPending, // Paiement en attente
|
||||
unreadMessage, // Message non lu
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'dart:io'; // Pour File
|
||||
import '../models/card_assets.dart'; // Import de l'enum CardColorVertical
|
||||
|
||||
class ParentData {
|
||||
String firstName;
|
||||
String lastName;
|
||||
String address; // Rue et numéro
|
||||
String postalCode; // Ajout
|
||||
String city; // Ajout
|
||||
String phone;
|
||||
String email;
|
||||
String password; // Peut-être pas nécessaire pour le récap, mais pour la création initiale si
|
||||
File? profilePicture; // Chemin ou objet File
|
||||
|
||||
ParentData({
|
||||
this.firstName = '',
|
||||
this.lastName = '',
|
||||
this.address = '', // Rue
|
||||
this.postalCode = '', // Ajout
|
||||
this.city = '', // Ajout
|
||||
this.phone = '',
|
||||
this.email = '',
|
||||
this.password = '',
|
||||
this.profilePicture,
|
||||
});
|
||||
}
|
||||
|
||||
class ChildData {
|
||||
String firstName;
|
||||
String lastName;
|
||||
String dob; // Date de naissance ou prévisionnelle
|
||||
bool photoConsent;
|
||||
bool multipleBirth;
|
||||
bool isUnbornChild;
|
||||
File? imageFile;
|
||||
CardColorVertical cardColor; // Nouveau champ pour la couleur de la carte
|
||||
|
||||
ChildData({
|
||||
this.firstName = '',
|
||||
this.lastName = '',
|
||||
this.dob = '',
|
||||
this.photoConsent = false,
|
||||
this.multipleBirth = false,
|
||||
this.isUnbornChild = false,
|
||||
this.imageFile,
|
||||
required this.cardColor, // Rendre requis dans le constructeur
|
||||
});
|
||||
}
|
||||
|
||||
class UserRegistrationData {
|
||||
ParentData parent1;
|
||||
ParentData? parent2; // Optionnel
|
||||
List<ChildData> children;
|
||||
String motivationText;
|
||||
bool cguAccepted;
|
||||
|
||||
UserRegistrationData({
|
||||
ParentData? parent1Data,
|
||||
this.parent2,
|
||||
List<ChildData>? childrenData,
|
||||
this.motivationText = '',
|
||||
this.cguAccepted = false,
|
||||
}) : parent1 = parent1Data ?? ParentData(),
|
||||
children = childrenData ?? [];
|
||||
|
||||
// Méthode pour ajouter/mettre à jour le parent 1
|
||||
void updateParent1(ParentData data) {
|
||||
parent1 = data;
|
||||
}
|
||||
|
||||
// Méthode pour ajouter/mettre à jour le parent 2
|
||||
void updateParent2(ParentData? data) {
|
||||
parent2 = data;
|
||||
}
|
||||
|
||||
// Méthode pour ajouter un enfant
|
||||
void addChild(ChildData child) {
|
||||
children.add(child);
|
||||
}
|
||||
|
||||
// Méthode pour mettre à jour un enfant (si nécessaire plus tard)
|
||||
void updateChild(int index, ChildData child) {
|
||||
if (index >= 0 && index < children.length) {
|
||||
children[index] = child;
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour la motivation
|
||||
void updateMotivation(String text) {
|
||||
motivationText = text;
|
||||
}
|
||||
|
||||
// Accepter les CGU
|
||||
void acceptCGU() {
|
||||
cguAccepted = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/assistante_maternelle_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/gestionnaire_management_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/parent_managmant_widget.dart';
|
||||
import 'package:p_tits_pas/widgets/app_footer.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/dashboard_admin.dart';
|
||||
|
||||
class AdminDashboardScreen extends StatefulWidget {
|
||||
const AdminDashboardScreen({super.key});
|
||||
|
||||
@override
|
||||
_AdminDashboardScreenState createState() => _AdminDashboardScreenState();
|
||||
}
|
||||
|
||||
class _AdminDashboardScreenState extends State<AdminDashboardScreen> {
|
||||
int selectedIndex = 0;
|
||||
|
||||
void onTabChange(int index) {
|
||||
setState(() {
|
||||
selectedIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(60.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
child: DashboardAppBarAdmin(
|
||||
selectedIndex: selectedIndex,
|
||||
onTabChange: onTabChange,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _getBody(),
|
||||
),
|
||||
const AppFooter(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
switch (selectedIndex) {
|
||||
case 0:
|
||||
return const GestionnaireManagementWidget();
|
||||
case 1:
|
||||
return const ParentManagementWidget();
|
||||
case 2:
|
||||
return const AssistanteMaternelleManagementWidget();
|
||||
case 3:
|
||||
return const Center(child: Text("👨💼 Administrateurs"));
|
||||
default:
|
||||
return const Center(child: Text("Page non trouvée"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GestionnairesCreate extends StatelessWidget {
|
||||
const GestionnairesCreate({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Créer un gestionnaire'),
|
||||
),
|
||||
body: const Center(
|
||||
child: Text('Formulaire de création de gestionnaire'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/am_user_registration_data.dart';
|
||||
import 'package:p_tits_pas/models/card_assets.dart';
|
||||
import 'package:p_tits_pas/utils/data_generator.dart';
|
||||
import 'package:p_tits_pas/widgets/FormFieldConfig.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
|
||||
class AmRegisterStep1Screen extends StatefulWidget {
|
||||
const AmRegisterStep1Screen({super.key});
|
||||
@override
|
||||
State <AmRegisterStep1Screen> createState() => _AmRegisterStep1ScreenState();
|
||||
}
|
||||
|
||||
class _AmRegisterStep1ScreenState extends State<AmRegisterStep1Screen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late ChildminderRegistrationData _registrationData;
|
||||
|
||||
final _lastNameController = TextEditingController();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
final _addressController = TextEditingController();
|
||||
final _postalCodeController = TextEditingController();
|
||||
final _cityController = TextEditingController();
|
||||
|
||||
// File? _selectedImage;
|
||||
// bool _photoConsent = false;
|
||||
// final ImagePicker _picker = ImagePicker();
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registrationData = ChildminderRegistrationData();
|
||||
_generateAndFillData();
|
||||
}
|
||||
|
||||
void _generateAndFillData() {
|
||||
final String genFirstName = DataGenerator.firstName();
|
||||
final String genLastName = DataGenerator.lastName();
|
||||
final String genAddress = DataGenerator.address();
|
||||
final String genPostalCode = DataGenerator.postalCode();
|
||||
final String genCity = DataGenerator.city();
|
||||
final String genPhone = DataGenerator.phone();
|
||||
final String genEmail = DataGenerator.email(genFirstName, genLastName);
|
||||
final String genPassword = DataGenerator.password();
|
||||
|
||||
_addressController.text = genAddress;
|
||||
_postalCodeController.text = genPostalCode;
|
||||
_cityController.text = genCity;
|
||||
_firstNameController.text = genFirstName;
|
||||
_lastNameController.text = genLastName;
|
||||
_phoneController.text = genPhone;
|
||||
_emailController.text = genEmail;
|
||||
_passwordController.text = genPassword;
|
||||
_confirmPasswordController.text = genPassword;
|
||||
|
||||
setState(() {
|
||||
_registrationData.updateIdentity(
|
||||
ChildminderId(
|
||||
firstName: genFirstName,
|
||||
lastName: genLastName,
|
||||
address: genAddress,
|
||||
postalCode: genPostalCode,
|
||||
city: genCity,
|
||||
phone: genPhone,
|
||||
email: genEmail,
|
||||
password: genPassword.trim(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_lastNameController.dispose();
|
||||
_firstNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
_addressController.dispose();
|
||||
_postalCodeController.dispose();
|
||||
_cityController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<List<ModularFormField>> get formFields => [
|
||||
[
|
||||
ModularFormField(
|
||||
label: 'Nom',
|
||||
hint: 'Votre nom de famille',
|
||||
controller: _lastNameController,
|
||||
isRequired: true,
|
||||
flex: 12,
|
||||
),
|
||||
ModularFormField(
|
||||
label: 'Prénom',
|
||||
hint: 'Votre prénom',
|
||||
controller: _firstNameController,
|
||||
isRequired: true,
|
||||
flex: 12,
|
||||
),
|
||||
],
|
||||
[
|
||||
ModularFormField(
|
||||
label: 'Téléphone',
|
||||
hint: 'Votre numéro de téléphone',
|
||||
controller: _phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
flex: 12,
|
||||
),
|
||||
ModularFormField(
|
||||
label: 'Email',
|
||||
hint: 'Votre adresse email',
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
flex: 12,
|
||||
),
|
||||
],
|
||||
[
|
||||
ModularFormField(
|
||||
label: 'Mot de passe',
|
||||
hint: 'Votre mot de passe',
|
||||
controller: _passwordController,
|
||||
isPassword: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return 'Mot de passe requis';
|
||||
if (value.length < 6) return '6 caractères minimum';
|
||||
return null;
|
||||
},
|
||||
isRequired: true,
|
||||
flex: 12,
|
||||
),
|
||||
ModularFormField(
|
||||
label: 'Confirmer le mot de passe',
|
||||
hint: 'Confirmez votre mot de passe',
|
||||
controller: _confirmPasswordController,
|
||||
isPassword: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return 'Mot de passe requis';
|
||||
if (value != _passwordController.text) return 'Les mots de passe ne correspondent pas';
|
||||
return null;
|
||||
},
|
||||
isRequired: true,
|
||||
flex: 12,
|
||||
),
|
||||
],
|
||||
[
|
||||
ModularFormField(
|
||||
label: 'Adresse (N° et Rue)',
|
||||
hint: 'Numéro et nom de votre rue',
|
||||
controller: _addressController,
|
||||
isRequired: true,
|
||||
),
|
||||
],
|
||||
[
|
||||
ModularFormField(
|
||||
label: 'Code postal',
|
||||
hint: 'Votre code postal',
|
||||
controller: _postalCodeController,
|
||||
keyboardType: TextInputType.number,
|
||||
isRequired: true,
|
||||
flex: 1,
|
||||
),
|
||||
ModularFormField(
|
||||
label: 'Ville',
|
||||
hint: 'Votre ville',
|
||||
controller: _cityController,
|
||||
flex: 4,
|
||||
isRequired: true,
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
void _handleSubmit() {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
_registrationData.updateIdentity(
|
||||
ChildminderId(
|
||||
firstName: _firstNameController.text,
|
||||
lastName: _lastNameController.text,
|
||||
address: _addressController.text,
|
||||
postalCode: _postalCodeController.text,
|
||||
city: _cityController.text,
|
||||
phone: _phoneController.text,
|
||||
email: _emailController.text,
|
||||
password: _passwordController.text,
|
||||
),
|
||||
);
|
||||
print('Vérification des données:');
|
||||
print('Adresse: ${_registrationData.identity.address}');
|
||||
print('Nom: ${_registrationData.identity.lastName}');
|
||||
print('Prénom: ${_registrationData.identity.firstName}');
|
||||
Navigator.pushNamed(context, '/am-register/step2',
|
||||
arguments: _registrationData);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
'assets/images/paper2.png',
|
||||
fit: BoxFit.cover,
|
||||
repeat: ImageRepeat.repeat,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Étape 1/4',
|
||||
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Informations d\'identité de l\'assistante maternelle',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Container(
|
||||
width: screenSize.width * 0.6,
|
||||
padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50),
|
||||
constraints: const BoxConstraints(minHeight: 570),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(CardColorHorizontal.lavender.path),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: ModularForm(
|
||||
formKey: _formKey,
|
||||
fieldGroups: formFields,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
left: 40,
|
||||
child: IconButton(
|
||||
icon: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(math.pi),
|
||||
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: 'Retour',
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
right: 40,
|
||||
child: IconButton(
|
||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
onPressed: _handleSubmit,
|
||||
tooltip: 'Suivant',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:p_tits_pas/models/am_user_registration_data.dart';
|
||||
import 'package:p_tits_pas/models/card_assets.dart';
|
||||
import 'package:p_tits_pas/utils/data_generator.dart';
|
||||
import 'package:p_tits_pas/widgets/FormFieldConfig.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
class AmRegisterStep2Screen extends StatefulWidget {
|
||||
final ChildminderRegistrationData registrationData;
|
||||
const AmRegisterStep2Screen({super.key, required this.registrationData});
|
||||
|
||||
@override
|
||||
State<AmRegisterStep2Screen> createState() => _AmRegisterStep2ScreenState();
|
||||
}
|
||||
|
||||
class _AmRegisterStep2ScreenState extends State<AmRegisterStep2Screen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late ChildminderRegistrationData _registrationData;
|
||||
|
||||
final _dateOfBirthController = TextEditingController();
|
||||
final _birthCityController = TextEditingController();
|
||||
final _birthCountryController = TextEditingController();
|
||||
final _socialSecurityController = TextEditingController();
|
||||
final _agreementNumberController = TextEditingController();
|
||||
final _maxChildrenController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registrationData = widget.registrationData;
|
||||
_generateAndFillData();
|
||||
}
|
||||
|
||||
void _generateAndFillData() {
|
||||
_dateOfBirthController.text = DataGenerator.birthDate();
|
||||
_birthCityController.text = DataGenerator.city();
|
||||
_birthCountryController.text = "France";
|
||||
_socialSecurityController.text = DataGenerator.socialSecurityNumber();
|
||||
_agreementNumberController.text = DataGenerator.agreementNumber();
|
||||
_maxChildrenController.text = "3";
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_dateOfBirthController.dispose();
|
||||
_birthCityController.dispose();
|
||||
_birthCountryController.dispose();
|
||||
_socialSecurityController.dispose();
|
||||
_agreementNumberController.dispose();
|
||||
_maxChildrenController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String? _validateSocialSecurity(String? value) {
|
||||
if (value == null || value.isEmpty)
|
||||
return 'Numéro de sécurité sociale requis';
|
||||
|
||||
// Supprime les espaces pour la validation
|
||||
String cleanValue = value.replaceAll(' ', '');
|
||||
|
||||
// Vérifie que c'est bien 13 ou 15 chiffres
|
||||
if (cleanValue.length != 13 && cleanValue.length != 15) {
|
||||
return 'Format invalide (13 ou 15 chiffres)';
|
||||
}
|
||||
|
||||
if (!RegExp(r'^[0-9]+$').hasMatch(cleanValue)) {
|
||||
return 'Seuls les chiffres sont autorisés';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
List<List<ModularFormField>> get formFields => [
|
||||
[
|
||||
ModularFormField(
|
||||
label: 'Date de naissance',
|
||||
hint: 'JJ/MM/AAAA',
|
||||
controller: _dateOfBirthController,
|
||||
keyboardType: TextInputType.datetime,
|
||||
isRequired: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty)
|
||||
return 'Date de naissance requise';
|
||||
// Validation basique du format de date
|
||||
if (!RegExp(r'^[0-3][0-9]/[0-1][0-9]/[1-2][0-9]{3}$')
|
||||
.hasMatch(value)) {
|
||||
return 'Format invalide (JJ/MM/AAAA)';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
flex: 12,
|
||||
),
|
||||
],
|
||||
[
|
||||
ModularFormField(
|
||||
label: 'Ville de naissance',
|
||||
hint: 'Votre ville de naissance',
|
||||
controller: _birthCityController,
|
||||
isRequired: true,
|
||||
flex: 12,
|
||||
),
|
||||
ModularFormField(
|
||||
label: 'Pays de naissance',
|
||||
hint: 'Votre pays de naissance',
|
||||
controller: _birthCountryController,
|
||||
isRequired: true,
|
||||
flex: 12,
|
||||
),
|
||||
],
|
||||
[
|
||||
ModularFormField(
|
||||
label: 'Numéro de Sécurité Sociale (NIR)',
|
||||
hint: '1234567890123',
|
||||
controller: _socialSecurityController,
|
||||
keyboardType: TextInputType.number,
|
||||
isRequired: true,
|
||||
validator: _validateSocialSecurity,
|
||||
),
|
||||
],
|
||||
[
|
||||
ModularFormField(
|
||||
label: 'Numéro d\'agrément',
|
||||
hint: 'Votre numéro d\'agrément',
|
||||
controller: _agreementNumberController,
|
||||
isRequired: true,
|
||||
flex: 12,
|
||||
),
|
||||
ModularFormField(
|
||||
label: 'Nombre d\'enfants max',
|
||||
hint: 'Ex: 3',
|
||||
controller: _maxChildrenController,
|
||||
keyboardType: TextInputType.number,
|
||||
isRequired: true,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return 'Nombre requis';
|
||||
int? number = int.tryParse(value);
|
||||
if (number == null || number < 1 || number > 6) {
|
||||
return 'Entre 1 et 6 enfants';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
flex: 6,
|
||||
),
|
||||
],
|
||||
];
|
||||
void _handleSubmit() {
|
||||
print('Vérification des données2:');
|
||||
print('Adresse: ${_registrationData.identity.address}');
|
||||
print('Nom: ${_registrationData.identity.lastName}');
|
||||
print('Prénom: ${_registrationData.identity.firstName}');
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
_registrationData.updateProfessional(
|
||||
ChildminderProfessional(
|
||||
dateOfBirth: _dateOfBirthController.text,
|
||||
birthCity: _birthCityController.text,
|
||||
birthCountry: _birthCountryController.text,
|
||||
socialSecurityNumber: _socialSecurityController.text,
|
||||
agreementNumber: _agreementNumberController.text,
|
||||
maxChildren: int.tryParse(_maxChildrenController.text) ?? 1,
|
||||
),
|
||||
);
|
||||
|
||||
Navigator.pushNamed(context, '/am-register/step3',
|
||||
arguments: _registrationData);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
'assets/images/paper2.png',
|
||||
fit: BoxFit.cover,
|
||||
repeat: ImageRepeat.repeat,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Étape 2/4',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 16, color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Informations professionnelles de l\'assistante maternelle',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Container(
|
||||
width: screenSize.width * 0.6,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 50, horizontal: 50),
|
||||
constraints: const BoxConstraints(minHeight: 570),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(CardColorHorizontal.peach.path),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: ModularForm(
|
||||
formKey: _formKey,
|
||||
fieldGroups: formFields,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
left: 40,
|
||||
child: IconButton(
|
||||
icon: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(math.pi),
|
||||
child:
|
||||
Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: 'Retour',
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
right: 40,
|
||||
child: IconButton(
|
||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
onPressed: _handleSubmit,
|
||||
tooltip: 'Suivant',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:p_tits_pas/models/am_user_registration_data.dart';
|
||||
import 'package:p_tits_pas/models/card_assets.dart';
|
||||
import 'package:p_tits_pas/widgets/app_custom_checkbox.dart';
|
||||
import 'package:p_tits_pas/widgets/custom_decorated_text_field.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
|
||||
class AmRegisterStep3Screen extends StatefulWidget {
|
||||
final ChildminderRegistrationData registrationData;
|
||||
const AmRegisterStep3Screen({super.key, required this.registrationData});
|
||||
|
||||
@override
|
||||
State<AmRegisterStep3Screen> createState() => _AmRegisterStep3ScreenState();
|
||||
}
|
||||
|
||||
class _AmRegisterStep3ScreenState extends State<AmRegisterStep3Screen> {
|
||||
|
||||
late ChildminderRegistrationData _registrationData;
|
||||
final _presentationMessageController = TextEditingController();
|
||||
bool _cguAccepted = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registrationData = widget.registrationData;
|
||||
_presentationMessageController.text = _registrationData.presentationMessage;
|
||||
// _cguAccepted = _registrationData.cguAccepted;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_presentationMessageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showCGUModal() {
|
||||
const String loremIpsumText = '''
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
|
||||
|
||||
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna.
|
||||
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
|
||||
Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
|
||||
|
||||
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna. Etiam et felis dolor.
|
||||
|
||||
Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
|
||||
|
||||
Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
|
||||
''';
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false, // L'utilisateur doit utiliser le bouton
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Conditions Générales d\'Utilisation',
|
||||
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: SizedBox(
|
||||
width: MediaQuery.of(dialogContext).size.width * 0.7, // 70% de la largeur de l'écran
|
||||
height: MediaQuery.of(dialogContext).size.height * 0.6, // 60% de la hauteur de l'écran
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
loremIpsumText,
|
||||
style: GoogleFonts.merienda(fontSize: 13),
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
),
|
||||
),
|
||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
actions: <Widget>[
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(dialogContext).primaryColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
|
||||
),
|
||||
child: Text(
|
||||
'Valider et Accepter',
|
||||
style: GoogleFonts.merienda(fontSize: 15, color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop(); // Ferme la modale
|
||||
setState(() {
|
||||
_cguAccepted = true; // Met à jour l'état
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final cardWidth = screenSize.width * 0.6;
|
||||
final double imageAspectRatio = 2.0;
|
||||
final cardHeight = cardWidth / imageAspectRatio;
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
|
||||
),
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Étape 3/4',
|
||||
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Message à destination du gestionnaire pour justifier votre demande ou ajouter des précisions',
|
||||
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Container(
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(CardColorHorizontal.green.path),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomDecoratedTextField(
|
||||
controller: _presentationMessageController,
|
||||
hintText: 'Écrivez ici pour motiver votre demande...',
|
||||
fieldHeight: cardHeight * 0.6,
|
||||
maxLines: 10,
|
||||
expandDynamically: true,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (!_cguAccepted) {
|
||||
_showCGUModal();
|
||||
}
|
||||
},
|
||||
child: AppCustomCheckbox(
|
||||
label: 'J\'accepte les conditions générales d\'utilisation',
|
||||
value: _cguAccepted,
|
||||
onChanged: (newValue) {
|
||||
if (!_cguAccepted) {
|
||||
_showCGUModal();
|
||||
} else {
|
||||
setState(() => _cguAccepted = false);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Chevrons de navigation
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
left: 40,
|
||||
child: IconButton(
|
||||
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: 'Retour',
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
right: 40,
|
||||
child: IconButton(
|
||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
onPressed: _cguAccepted
|
||||
? () {
|
||||
_registrationData.updatePresentation(_presentationMessageController.text);
|
||||
_registrationData.acceptCGU();
|
||||
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/am-register/step4',
|
||||
arguments: _registrationData
|
||||
);
|
||||
}
|
||||
: null,
|
||||
tooltip: 'Suivant',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:p_tits_pas/models/am_user_registration_data.dart';
|
||||
import 'package:p_tits_pas/models/card_assets.dart';
|
||||
import 'package:p_tits_pas/widgets/Summary.dart';
|
||||
import 'package:p_tits_pas/widgets/custom_decorated_text_field.dart';
|
||||
import 'package:p_tits_pas/widgets/image_button.dart';
|
||||
|
||||
Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) {
|
||||
const FontWeight labelFontWeight = FontWeight.w600;
|
||||
|
||||
// Ne pas afficher le label si labelFontSize est 0 ou si label est vide
|
||||
bool showLabel = label.isNotEmpty && labelFontSize > 0;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showLabel)
|
||||
Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)),
|
||||
if (showLabel)
|
||||
const SizedBox(height: 4),
|
||||
// Utiliser Expanded si multiLine et pas de hauteur fixe, sinon Container
|
||||
multiLine && fieldHeight == null
|
||||
? Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
image: const DecorationImage(
|
||||
image: AssetImage('assets/images/input_field_bg.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView( // Pour le défilement si le texte dépasse
|
||||
child: Text(
|
||||
value.isNotEmpty ? value : '-',
|
||||
style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0), // Garder une taille de texte par défaut si label caché
|
||||
maxLines: null, // Permettre un nombre illimité de lignes
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
width: double.infinity,
|
||||
height: multiLine ? null : fieldHeight,
|
||||
constraints: multiLine ? BoxConstraints(minHeight: fieldHeight) : null,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
image: const DecorationImage(
|
||||
image: AssetImage('assets/images/input_field_bg.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
value.isNotEmpty ? value : '-',
|
||||
style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0),
|
||||
maxLines: multiLine ? null : 1,
|
||||
overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class AmRegisterStep4Screen extends StatelessWidget {
|
||||
final ChildminderRegistrationData registrationData;
|
||||
|
||||
const AmRegisterStep4Screen({super.key, required this.registrationData});
|
||||
|
||||
Widget _buildAm1Card(BuildContext context, ChildminderRegistrationData data) {
|
||||
const double verticalSpacing = 28.0; // Espacement vertical augmenté
|
||||
const double labelFontSize = 22.0; // Taille de label augmentée
|
||||
|
||||
List<Widget> details = [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.identity.lastName, labelFontSize: labelFontSize)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.identity.firstName, labelFontSize: labelFontSize)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: verticalSpacing),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.identity.phone, labelFontSize: labelFontSize)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.identity.email, multiLine: true, labelFontSize: labelFontSize)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: verticalSpacing),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Adresse:", "${data.identity.address}\n${data.identity.postalCode} ${data.identity.city}".trim(), labelFontSize: labelFontSize)),
|
||||
const SizedBox(width: 20),
|
||||
],
|
||||
),
|
||||
];
|
||||
return SummaryCard(
|
||||
backgroundImagePath: CardColorHorizontal.peach.path,
|
||||
title: 'Informations d’identité',
|
||||
content: details,
|
||||
onEdit: () => Navigator.of(context).pushNamed('/am-register/step1', arguments: registrationData),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAm2Card(BuildContext context, ChildminderRegistrationData data) {
|
||||
const double verticalSpacing = 28.0;
|
||||
const double labelFontSize = 22.0;
|
||||
|
||||
List<Widget> myDetails = [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Date de naissance:", data.professional.dateOfBirth, labelFontSize: labelFontSize)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Ville de naissance:", data.professional.birthCity, labelFontSize: labelFontSize)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: verticalSpacing),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Pays de naissance:", data.professional.birthCountry, labelFontSize: labelFontSize)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Numéro de sécurité sociale:", data.professional.socialSecurityNumber, labelFontSize: labelFontSize)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: verticalSpacing),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Numéro d'agrément:", data.professional.agreementNumber, labelFontSize: labelFontSize)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Nombre d'enfants maximum:", data.professional.maxChildren.toString(), labelFontSize: labelFontSize)),
|
||||
],
|
||||
),
|
||||
];
|
||||
return SummaryCard(
|
||||
backgroundImagePath: CardColorHorizontal.lavender.path,
|
||||
title: 'Informations professionnelles',
|
||||
content: myDetails,
|
||||
onEdit: () => Navigator.of(context)
|
||||
.pushNamed('/am-register/step2', arguments: registrationData),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMotivationCard(BuildContext context, ChildminderRegistrationData data) {
|
||||
return SummaryCard(
|
||||
backgroundImagePath: CardColorHorizontal.green.path,
|
||||
title: 'Motivation',
|
||||
content: [
|
||||
Expanded(child: CustomDecoratedTextField(
|
||||
controller: TextEditingController(text: data.presentationMessage),
|
||||
hintText: 'Parlez-nous de votre motivation',
|
||||
fieldHeight: 200,
|
||||
maxLines: 10,
|
||||
expandDynamically: true,
|
||||
readOnly: true,
|
||||
fontSize: 18.0,)),
|
||||
],
|
||||
onEdit: () => Navigator.of(context)
|
||||
.pushNamed('/am-register/step3', arguments: registrationData),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY),
|
||||
),
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(40.0),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('Etape 4/4',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 16, color: Colors.black54)),
|
||||
const SizedBox(height: 20),
|
||||
Text('Récapitulatif de votre demande',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87),
|
||||
textAlign: TextAlign.center),
|
||||
const SizedBox(height: 30),
|
||||
_buildAm1Card(context, registrationData),
|
||||
const SizedBox(height: 20),
|
||||
if (registrationData.professional != null) ...[
|
||||
_buildAm2Card(context, registrationData),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
_buildMotivationCard(context, registrationData),
|
||||
const SizedBox(height: 40),
|
||||
ImageButton(
|
||||
bg: 'assets/images/btn_green.png',
|
||||
text: 'Soumettre ma demande',
|
||||
textColor: const Color(0xFF2D6A4F),
|
||||
width: 350,
|
||||
height: 50,
|
||||
fontSize: 18,
|
||||
onPressed: () {
|
||||
// Vérification des données requises
|
||||
_showConfirmationModal(context);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
left: 40,
|
||||
child: IconButton(
|
||||
icon: Transform.flip(
|
||||
flipX: true,
|
||||
child: Image.asset('assets/images/chevron_right.png',
|
||||
height: 40)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: 'Retour',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showConfirmationModal(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Demande enregistrée',
|
||||
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: Text(
|
||||
'Votre dossier a bien été pris en compte. Un gestionnaire le validera bientôt.',
|
||||
style: GoogleFonts.merienda(fontSize: 14),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('OK',
|
||||
style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
Navigator.of(context)
|
||||
.pushNamedAndRemoveUntil('/login', (route) => false);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'dart:math' as math; // Pour la rotation du chevron
|
||||
import '../../../models/parent_user_registration_data.dart'; // Import du modèle de données
|
||||
import '../../../utils/data_generator.dart'; // Import du générateur de données
|
||||
import '../../../widgets/custom_app_text_field.dart'; // Import du widget CustomAppTextField
|
||||
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
||||
|
||||
class ParentRegisterStep1Screen extends StatefulWidget {
|
||||
const ParentRegisterStep1Screen({super.key});
|
||||
|
||||
@override
|
||||
State<ParentRegisterStep1Screen> createState() => _ParentRegisterStep1ScreenState();
|
||||
}
|
||||
|
||||
class _ParentRegisterStep1ScreenState extends State<ParentRegisterStep1Screen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late UserRegistrationData _registrationData;
|
||||
|
||||
// Contrôleurs pour les champs (restauration CP et Ville)
|
||||
final _lastNameController = TextEditingController();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _addressController = TextEditingController();
|
||||
final _postalCodeController = TextEditingController();
|
||||
final _cityController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registrationData = UserRegistrationData();
|
||||
_generateAndFillData();
|
||||
}
|
||||
|
||||
void _generateAndFillData() {
|
||||
final String genFirstName = DataGenerator.firstName();
|
||||
final String genLastName = DataGenerator.lastName();
|
||||
|
||||
// Utilisation des méthodes publiques de DataGenerator
|
||||
_addressController.text = DataGenerator.address();
|
||||
_postalCodeController.text = DataGenerator.postalCode();
|
||||
_cityController.text = DataGenerator.city();
|
||||
|
||||
_firstNameController.text = genFirstName;
|
||||
_lastNameController.text = genLastName;
|
||||
_phoneController.text = DataGenerator.phone();
|
||||
_emailController.text = DataGenerator.email(genFirstName, genLastName);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_lastNameController.dispose();
|
||||
_firstNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
_emailController.dispose();
|
||||
_addressController.dispose();
|
||||
_postalCodeController.dispose();
|
||||
_cityController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
// Fond papier
|
||||
Positioned.fill(
|
||||
child: Image.asset(
|
||||
'assets/images/paper2.png',
|
||||
fit: BoxFit.cover,
|
||||
repeat: ImageRepeat.repeat,
|
||||
),
|
||||
),
|
||||
|
||||
// Contenu centré
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Indicateur d'étape
|
||||
Text(
|
||||
'Étape 1/6',
|
||||
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Texte d'instruction
|
||||
Text(
|
||||
'Informations du Parent Principal',
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// Carte jaune contenant le formulaire
|
||||
Container(
|
||||
width: screenSize.width * 0.6,
|
||||
padding: const EdgeInsets.symmetric(vertical: 50, horizontal: 50),
|
||||
constraints: const BoxConstraints(minHeight: 570),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(CardColorHorizontal.peach.path),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Votre nom de famille', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 1, child: const SizedBox()),
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Votre prénom', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Votre numéro de téléphone', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 1, child: const SizedBox()),
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Votre adresse e-mail', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
CustomAppTextField(
|
||||
controller: _addressController,
|
||||
labelText: 'Adresse (N° et Rue)',
|
||||
hintText: 'Numéro et nom de votre rue',
|
||||
style: CustomAppTextFieldStyle.beige,
|
||||
fieldWidth: double.infinity,
|
||||
labelFontSize: 22.0,
|
||||
inputFontSize: 20.0,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Code postal', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Votre ville', style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Chevron de navigation gauche (Retour)
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20, // Centré verticalement
|
||||
left: 40,
|
||||
child: IconButton(
|
||||
icon: Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.rotationY(math.pi), // Inverse horizontalement
|
||||
child: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
),
|
||||
onPressed: () => Navigator.pop(context), // Retour à l'écran de choix
|
||||
tooltip: 'Retour',
|
||||
),
|
||||
),
|
||||
|
||||
// Chevron de navigation droit (Suivant)
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20, // Centré verticalement
|
||||
right: 40,
|
||||
child: IconButton(
|
||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
onPressed: () {
|
||||
if (_formKey.currentState?.validate() ?? false) {
|
||||
_registrationData.updateParent1(
|
||||
ParentData(
|
||||
firstName: _firstNameController.text,
|
||||
lastName: _lastNameController.text,
|
||||
address: _addressController.text,
|
||||
postalCode: _postalCodeController.text,
|
||||
city: _cityController.text,
|
||||
phone: _phoneController.text,
|
||||
email: _emailController.text,
|
||||
password: '', // Pas de mot de passe à cette étape
|
||||
)
|
||||
);
|
||||
Navigator.pushNamed(context, '/parent-register/step2', arguments: _registrationData);
|
||||
}
|
||||
},
|
||||
tooltip: 'Suivant',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'dart:math' as math; // Pour la rotation du chevron
|
||||
import '../../../models/parent_user_registration_data.dart'; // Import du modèle
|
||||
import '../../../utils/data_generator.dart'; // Import du générateur
|
||||
import '../../../widgets/custom_app_text_field.dart'; // Import du widget
|
||||
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
||||
|
||||
class ParentRegisterStep2Screen extends StatefulWidget {
|
||||
final UserRegistrationData registrationData; // Accepte les données de l'étape 1
|
||||
|
||||
const ParentRegisterStep2Screen({super.key, required this.registrationData});
|
||||
|
||||
@override
|
||||
State<ParentRegisterStep2Screen> createState() => _ParentRegisterStep2ScreenState();
|
||||
}
|
||||
|
||||
class _ParentRegisterStep2ScreenState extends State<ParentRegisterStep2Screen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late UserRegistrationData _registrationData; // Copie locale pour modification
|
||||
|
||||
bool _addParent2 = true; // Pour le test, on ajoute toujours le parent 2
|
||||
bool _sameAddressAsParent1 = false; // Peut être généré aléatoirement aussi
|
||||
|
||||
// Contrôleurs pour les champs du parent 2
|
||||
final _lastNameController = TextEditingController();
|
||||
final _firstNameController = TextEditingController();
|
||||
final _phoneController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _addressController = TextEditingController();
|
||||
final _postalCodeController = TextEditingController();
|
||||
final _cityController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registrationData = widget.registrationData; // Récupère les données de l'étape 1
|
||||
if (_addParent2) {
|
||||
_generateAndFillParent2Data();
|
||||
}
|
||||
}
|
||||
|
||||
void _generateAndFillParent2Data() {
|
||||
final String genFirstName = DataGenerator.firstName();
|
||||
final String genLastName = DataGenerator.lastName();
|
||||
_firstNameController.text = genFirstName;
|
||||
_lastNameController.text = genLastName;
|
||||
_phoneController.text = DataGenerator.phone();
|
||||
_emailController.text = DataGenerator.email(genFirstName, genLastName);
|
||||
|
||||
_sameAddressAsParent1 = DataGenerator.boolean();
|
||||
if (!_sameAddressAsParent1) {
|
||||
_addressController.text = DataGenerator.address();
|
||||
_postalCodeController.text = DataGenerator.postalCode();
|
||||
_cityController.text = DataGenerator.city();
|
||||
} else {
|
||||
_addressController.clear();
|
||||
_postalCodeController.clear();
|
||||
_cityController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_lastNameController.dispose();
|
||||
_firstNameController.dispose();
|
||||
_phoneController.dispose();
|
||||
_emailController.dispose();
|
||||
_addressController.dispose();
|
||||
_postalCodeController.dispose();
|
||||
_cityController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get _parent2FieldsEnabled => _addParent2;
|
||||
bool get _addressFieldsEnabled => _addParent2 && !_sameAddressAsParent1;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
|
||||
),
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Étape 2/6', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Informations du Deuxième Parent (Optionnel)',
|
||||
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Container(
|
||||
width: screenSize.width * 0.6,
|
||||
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 50),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(image: AssetImage(CardColorHorizontal.blue.path), fit: BoxFit.fill),
|
||||
),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 12,
|
||||
child: Row(children: [
|
||||
const Icon(Icons.person_add_alt_1, size: 20), const SizedBox(width: 8),
|
||||
Flexible(child: Text('Ajouter Parent 2 ?', style: GoogleFonts.merienda(fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis)),
|
||||
const Spacer(),
|
||||
Switch(value: _addParent2, onChanged: (val) => setState(() {
|
||||
_addParent2 = val ?? false;
|
||||
if (_addParent2) _generateAndFillParent2Data(); else _clearParent2Fields();
|
||||
}), activeColor: Theme.of(context).primaryColor),
|
||||
]),
|
||||
),
|
||||
Expanded(flex: 1, child: const SizedBox()),
|
||||
Expanded(
|
||||
flex: 12,
|
||||
child: Row(children: [
|
||||
Icon(Icons.home_work_outlined, size: 20, color: _addParent2 ? null : Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
Flexible(child: Text('Même Adresse ?', style: GoogleFonts.merienda(color: _addParent2 ? null : Colors.grey), overflow: TextOverflow.ellipsis)),
|
||||
const Spacer(),
|
||||
Switch(value: _sameAddressAsParent1, onChanged: _addParent2 ? (val) => setState(() {
|
||||
_sameAddressAsParent1 = val ?? false;
|
||||
if (_sameAddressAsParent1) {
|
||||
_addressController.text = _registrationData.parent1.address;
|
||||
_postalCodeController.text = _registrationData.parent1.postalCode;
|
||||
_cityController.text = _registrationData.parent1.city;
|
||||
} else {
|
||||
_addressController.text = DataGenerator.address();
|
||||
_postalCodeController.text = DataGenerator.postalCode();
|
||||
_cityController.text = DataGenerator.city();
|
||||
}
|
||||
}) : null, activeColor: Theme.of(context).primaryColor),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _lastNameController, labelText: 'Nom', hintText: 'Nom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 1, child: const SizedBox()),
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _firstNameController, labelText: 'Prénom', hintText: 'Prénom du parent 2', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _phoneController, labelText: 'Téléphone', keyboardType: TextInputType.phone, hintText: 'Son téléphone', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
Expanded(flex: 1, child: const SizedBox()),
|
||||
Expanded(flex: 12, child: CustomAppTextField(controller: _emailController, labelText: 'Email', keyboardType: TextInputType.emailAddress, hintText: 'Son email', enabled: _parent2FieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
CustomAppTextField(controller: _addressController, labelText: 'Adresse (N° et Rue)', hintText: 'Son numéro et nom de rue', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0),
|
||||
const SizedBox(height: 32),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(flex: 1, child: CustomAppTextField(controller: _postalCodeController, labelText: 'Code Postal', keyboardType: TextInputType.number, hintText: 'Son code postal', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(flex: 4, child: CustomAppTextField(controller: _cityController, labelText: 'Ville', hintText: 'Sa ville', enabled: _addressFieldsEnabled, style: CustomAppTextFieldStyle.beige, fieldWidth: double.infinity, labelFontSize: 22.0, inputFontSize: 20.0)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
left: 40,
|
||||
child: IconButton(
|
||||
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: 'Retour',
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
right: 40,
|
||||
child: IconButton(
|
||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
onPressed: () {
|
||||
if (!_addParent2 || (_formKey.currentState?.validate() ?? false)) {
|
||||
if (_addParent2) {
|
||||
_registrationData.updateParent2(
|
||||
ParentData(
|
||||
firstName: _firstNameController.text,
|
||||
lastName: _lastNameController.text,
|
||||
address: _sameAddressAsParent1 ? _registrationData.parent1.address : _addressController.text,
|
||||
postalCode: _sameAddressAsParent1 ? _registrationData.parent1.postalCode : _postalCodeController.text,
|
||||
city: _sameAddressAsParent1 ? _registrationData.parent1.city : _cityController.text,
|
||||
phone: _phoneController.text,
|
||||
email: _emailController.text,
|
||||
password: '', // Pas de mot de passe à cette étape
|
||||
)
|
||||
);
|
||||
} else {
|
||||
_registrationData.updateParent2(null);
|
||||
}
|
||||
Navigator.pushNamed(context, '/parent-register/step3', arguments: _registrationData);
|
||||
}
|
||||
},
|
||||
tooltip: 'Suivant',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _clearParent2Fields() {
|
||||
_formKey.currentState?.reset();
|
||||
_lastNameController.clear();
|
||||
_firstNameController.clear();
|
||||
_phoneController.clear();
|
||||
_emailController.clear();
|
||||
_addressController.clear();
|
||||
_postalCodeController.clear();
|
||||
_cityController.clear();
|
||||
_sameAddressAsParent1 = false;
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'dart:math' as math; // Pour la rotation du chevron
|
||||
import 'package:flutter/gestures.dart'; // Pour PointerDeviceKind
|
||||
import '../../../widgets/hover_relief_widget.dart'; // Import du nouveau widget
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
// import 'package:image_cropper/image_cropper.dart'; // Supprimé
|
||||
import 'dart:io' show File, Platform; // Ajout de Platform
|
||||
import 'package:flutter/foundation.dart' show kIsWeb; // Import pour kIsWeb
|
||||
import '../../../widgets/custom_app_text_field.dart'; // Import du nouveau widget TextField
|
||||
import '../../../widgets/app_custom_checkbox.dart'; // Import du nouveau widget Checkbox
|
||||
import '../../../models/parent_user_registration_data.dart'; // Import du modèle de données
|
||||
import '../../../utils/data_generator.dart'; // Import du générateur
|
||||
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
||||
|
||||
// La classe _ChildFormData est supprimée car on utilise ChildData du modèle
|
||||
|
||||
class ParentRegisterStep3Screen extends StatefulWidget {
|
||||
final UserRegistrationData registrationData; // Accepte les données
|
||||
|
||||
const ParentRegisterStep3Screen({super.key, required this.registrationData});
|
||||
|
||||
@override
|
||||
State<ParentRegisterStep3Screen> createState() => _ParentRegisterStep3ScreenState();
|
||||
}
|
||||
|
||||
class _ParentRegisterStep3ScreenState extends State<ParentRegisterStep3Screen> {
|
||||
late UserRegistrationData _registrationData; // Stocke l'état complet
|
||||
final ScrollController _scrollController = ScrollController(); // Pour le défilement horizontal
|
||||
bool _isScrollable = false;
|
||||
bool _showLeftFade = false;
|
||||
bool _showRightFade = false;
|
||||
static const double _fadeExtent = 0.05; // Pourcentage de fondu
|
||||
|
||||
// Liste ordonnée des couleurs de cartes pour les enfants
|
||||
static const List<CardColorVertical> _childCardColors = [
|
||||
CardColorVertical.lavender, // Premier enfant toujours lavande
|
||||
CardColorVertical.pink,
|
||||
CardColorVertical.peach,
|
||||
CardColorVertical.lime,
|
||||
CardColorVertical.red,
|
||||
CardColorVertical.green,
|
||||
CardColorVertical.blue,
|
||||
];
|
||||
|
||||
// Garder une trace des couleurs déjà utilisées
|
||||
final Set<CardColorVertical> _usedColors = {};
|
||||
|
||||
// Utilisation de GlobalKey pour les cartes enfants si validation complexe future
|
||||
// Map<int, GlobalKey<FormState>> _childFormKeys = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registrationData = widget.registrationData;
|
||||
// Initialiser les couleurs utilisées avec les enfants existants
|
||||
for (var child in _registrationData.children) {
|
||||
_usedColors.add(child.cardColor);
|
||||
}
|
||||
// S'il n'y a pas d'enfant, en ajouter un automatiquement avec des données générées
|
||||
if (_registrationData.children.isEmpty) {
|
||||
_addChild();
|
||||
}
|
||||
_scrollController.addListener(_scrollListener);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.removeListener(_scrollListener);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _scrollListener() {
|
||||
if (!_scrollController.hasClients) return;
|
||||
final position = _scrollController.position;
|
||||
final newIsScrollable = position.maxScrollExtent > 0.0;
|
||||
final newShowLeftFade = newIsScrollable && position.pixels > (position.viewportDimension * _fadeExtent / 2);
|
||||
final newShowRightFade = newIsScrollable && position.pixels < (position.maxScrollExtent - (position.viewportDimension * _fadeExtent / 2));
|
||||
if (newIsScrollable != _isScrollable || newShowLeftFade != _showLeftFade || newShowRightFade != _showRightFade) {
|
||||
setState(() {
|
||||
_isScrollable = newIsScrollable;
|
||||
_showLeftFade = newShowLeftFade;
|
||||
_showRightFade = newShowRightFade;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _addChild() {
|
||||
setState(() {
|
||||
bool isUnborn = DataGenerator.boolean();
|
||||
|
||||
// Trouver la première couleur non utilisée
|
||||
CardColorVertical cardColor = _childCardColors.firstWhere(
|
||||
(color) => !_usedColors.contains(color),
|
||||
orElse: () => _childCardColors[0], // Fallback sur la première couleur si toutes sont utilisées
|
||||
);
|
||||
|
||||
final newChild = ChildData(
|
||||
lastName: _registrationData.parent1.lastName,
|
||||
firstName: DataGenerator.firstName(),
|
||||
dob: DataGenerator.dob(isUnborn: isUnborn),
|
||||
isUnbornChild: isUnborn,
|
||||
photoConsent: DataGenerator.boolean(),
|
||||
multipleBirth: DataGenerator.boolean(),
|
||||
cardColor: cardColor,
|
||||
);
|
||||
_registrationData.addChild(newChild);
|
||||
_usedColors.add(cardColor);
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollListener();
|
||||
if (_scrollController.hasClients && _scrollController.position.maxScrollExtent > 0.0) {
|
||||
_scrollController.animateTo(_scrollController.position.maxScrollExtent, duration: const Duration(milliseconds: 300), curve: Curves.easeOut);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _removeChild(int index) {
|
||||
if (_registrationData.children.length > 1 && index >= 0 && index < _registrationData.children.length) {
|
||||
setState(() {
|
||||
// Ne pas retirer la couleur de _usedColors pour éviter sa réutilisation
|
||||
_registrationData.children.removeAt(index);
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollListener());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickImage(int childIndex) async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
try {
|
||||
final XFile? pickedFile = await picker.pickImage(
|
||||
source: ImageSource.gallery, imageQuality: 70, maxWidth: 1024, maxHeight: 1024);
|
||||
if (pickedFile != null) {
|
||||
setState(() {
|
||||
if (childIndex < _registrationData.children.length) {
|
||||
_registrationData.children[childIndex].imageFile = File(pickedFile.path);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) { print("Erreur image: $e"); }
|
||||
}
|
||||
|
||||
Future<void> _selectDate(BuildContext context, int childIndex) async {
|
||||
final ChildData currentChild = _registrationData.children[childIndex];
|
||||
final DateTime now = DateTime.now();
|
||||
DateTime initialDatePickerDate = now;
|
||||
DateTime firstDatePickerDate = DateTime(1980); DateTime lastDatePickerDate = now;
|
||||
|
||||
if (currentChild.isUnbornChild) {
|
||||
firstDatePickerDate = now; lastDatePickerDate = now.add(const Duration(days: 300));
|
||||
if (currentChild.dob.isNotEmpty) {
|
||||
try {
|
||||
List<String> parts = currentChild.dob.split('/');
|
||||
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
|
||||
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
|
||||
initialDatePickerDate = parsedDate;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
} else {
|
||||
if (currentChild.dob.isNotEmpty) {
|
||||
try {
|
||||
List<String> parts = currentChild.dob.split('/');
|
||||
DateTime? parsedDate = DateTime.tryParse("${parts[2]}-${parts[1].padLeft(2, '0')}-${parts[0].padLeft(2, '0')}");
|
||||
if (parsedDate != null && !parsedDate.isBefore(firstDatePickerDate) && !parsedDate.isAfter(lastDatePickerDate)) {
|
||||
initialDatePickerDate = parsedDate;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context, initialDate: initialDatePickerDate, firstDate: firstDatePickerDate,
|
||||
lastDate: lastDatePickerDate, locale: const Locale('fr', 'FR'),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() {
|
||||
currentChild.dob = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
|
||||
),
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Étape 3/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Informations Enfants',
|
||||
style: GoogleFonts.merienda(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black87),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 150.0),
|
||||
child: SizedBox(
|
||||
height: 684.0,
|
||||
child: ShaderMask(
|
||||
shaderCallback: (Rect bounds) {
|
||||
final Color leftFade = (_isScrollable && _showLeftFade) ? Colors.transparent : Colors.black;
|
||||
final Color rightFade = (_isScrollable && _showRightFade) ? Colors.transparent : Colors.black;
|
||||
if (!_isScrollable) { return LinearGradient(colors: const <Color>[Colors.black, Colors.black, Colors.black, Colors.black], stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0],).createShader(bounds); }
|
||||
return LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: <Color>[ leftFade, Colors.black, Colors.black, rightFade ], stops: const [0.0, _fadeExtent, 1.0 - _fadeExtent, 1.0], ).createShader(bounds);
|
||||
},
|
||||
blendMode: BlendMode.dstIn,
|
||||
child: Scrollbar(
|
||||
controller: _scrollController,
|
||||
thumbVisibility: true,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
itemCount: _registrationData.children.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index < _registrationData.children.length) {
|
||||
// Carte Enfant
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 20.0),
|
||||
child: _ChildCardWidget(
|
||||
key: ValueKey(_registrationData.children[index].hashCode), // Utiliser une clé basée sur les données
|
||||
childData: _registrationData.children[index],
|
||||
childIndex: index,
|
||||
onPickImage: () => _pickImage(index),
|
||||
onDateSelect: () => _selectDate(context, index),
|
||||
onFirstNameChanged: (value) => setState(() => _registrationData.children[index].firstName = value),
|
||||
onLastNameChanged: (value) => setState(() => _registrationData.children[index].lastName = value),
|
||||
onTogglePhotoConsent: (newValue) => setState(() => _registrationData.children[index].photoConsent = newValue),
|
||||
onToggleMultipleBirth: (newValue) => setState(() => _registrationData.children[index].multipleBirth = newValue),
|
||||
onToggleIsUnborn: (newValue) => setState(() {
|
||||
_registrationData.children[index].isUnbornChild = newValue;
|
||||
// Générer une nouvelle date si on change le statut
|
||||
_registrationData.children[index].dob = DataGenerator.dob(isUnborn: newValue);
|
||||
}),
|
||||
onRemove: () => _removeChild(index),
|
||||
canBeRemoved: _registrationData.children.length > 1,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Bouton Ajouter
|
||||
return Center(
|
||||
child: HoverReliefWidget(
|
||||
onPressed: _addChild,
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
child: Image.asset('assets/images/plus.png', height: 80, width: 80),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Chevrons de navigation
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
left: 40,
|
||||
child: IconButton(
|
||||
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: 'Retour',
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
right: 40,
|
||||
child: IconButton(
|
||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
onPressed: () {
|
||||
// TODO: Validation (si nécessaire)
|
||||
Navigator.pushNamed(context, '/parent-register/step4', arguments: _registrationData);
|
||||
},
|
||||
tooltip: 'Suivant',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Widget pour la carte enfant (adapté pour prendre ChildData et des callbacks)
|
||||
class _ChildCardWidget extends StatefulWidget { // Transformé en StatefulWidget pour gérer les contrôleurs internes
|
||||
final ChildData childData;
|
||||
final int childIndex;
|
||||
final VoidCallback onPickImage;
|
||||
final VoidCallback onDateSelect;
|
||||
final ValueChanged<String> onFirstNameChanged;
|
||||
final ValueChanged<String> onLastNameChanged;
|
||||
final ValueChanged<bool> onTogglePhotoConsent;
|
||||
final ValueChanged<bool> onToggleMultipleBirth;
|
||||
final ValueChanged<bool> onToggleIsUnborn;
|
||||
final VoidCallback onRemove;
|
||||
final bool canBeRemoved;
|
||||
|
||||
const _ChildCardWidget({
|
||||
required Key key,
|
||||
required this.childData,
|
||||
required this.childIndex,
|
||||
required this.onPickImage,
|
||||
required this.onDateSelect,
|
||||
required this.onFirstNameChanged,
|
||||
required this.onLastNameChanged,
|
||||
required this.onTogglePhotoConsent,
|
||||
required this.onToggleMultipleBirth,
|
||||
required this.onToggleIsUnborn,
|
||||
required this.onRemove,
|
||||
required this.canBeRemoved,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_ChildCardWidget> createState() => _ChildCardWidgetState();
|
||||
}
|
||||
|
||||
class _ChildCardWidgetState extends State<_ChildCardWidget> {
|
||||
late TextEditingController _firstNameController;
|
||||
late TextEditingController _lastNameController;
|
||||
late TextEditingController _dobController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialiser les contrôleurs avec les données du widget
|
||||
_firstNameController = TextEditingController(text: widget.childData.firstName);
|
||||
_lastNameController = TextEditingController(text: widget.childData.lastName);
|
||||
_dobController = TextEditingController(text: widget.childData.dob);
|
||||
|
||||
// Ajouter des listeners pour mettre à jour les données sources via les callbacks
|
||||
_firstNameController.addListener(() => widget.onFirstNameChanged(_firstNameController.text));
|
||||
_lastNameController.addListener(() => widget.onLastNameChanged(_lastNameController.text));
|
||||
// Pour dob, la mise à jour se fait via _selectDate, pas besoin de listener ici
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _ChildCardWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
// Mettre à jour les contrôleurs si les données externes changent
|
||||
// (peut arriver si on recharge l'état global)
|
||||
if (widget.childData.firstName != _firstNameController.text) {
|
||||
_firstNameController.text = widget.childData.firstName;
|
||||
}
|
||||
if (widget.childData.lastName != _lastNameController.text) {
|
||||
_lastNameController.text = widget.childData.lastName;
|
||||
}
|
||||
if (widget.childData.dob != _dobController.text) {
|
||||
_dobController.text = widget.childData.dob;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
_dobController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final File? currentChildImage = widget.childData.imageFile;
|
||||
// Utiliser la couleur de la carte de childData pour l'ombre si besoin, ou directement pour le fond
|
||||
final Color baseCardColorForShadow = widget.childData.cardColor == CardColorVertical.lavender
|
||||
? Colors.purple.shade200
|
||||
: (widget.childData.cardColor == CardColorVertical.pink ? Colors.pink.shade200 : Colors.grey.shade200); // Placeholder pour autres couleurs
|
||||
final Color initialPhotoShadow = baseCardColorForShadow.withAlpha(90);
|
||||
final Color hoverPhotoShadow = baseCardColorForShadow.withAlpha(130);
|
||||
|
||||
return Container(
|
||||
width: 345.0 * 1.1, // 379.5
|
||||
height: 570.0 * 1.2, // 684.0
|
||||
padding: const EdgeInsets.all(22.0 * 1.1), // 24.2
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(image: AssetImage(widget.childData.cardColor.path), fit: BoxFit.cover),
|
||||
borderRadius: BorderRadius.circular(20 * 1.1), // 22
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
HoverReliefWidget(
|
||||
onPressed: widget.onPickImage,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
initialShadowColor: initialPhotoShadow,
|
||||
hoverShadowColor: hoverPhotoShadow,
|
||||
child: SizedBox(
|
||||
height: 200.0,
|
||||
width: 200.0,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.0 * 1.1), // 5.5
|
||||
child: currentChildImage != null
|
||||
? ClipRRect(borderRadius: BorderRadius.circular(10 * 1.1), child: kIsWeb ? Image.network(currentChildImage.path, fit: BoxFit.cover) : Image.file(currentChildImage, fit: BoxFit.cover))
|
||||
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12.0 * 1.1), // Augmenté pour plus d'espace après la photo
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text('Enfant à naître ?', style: GoogleFonts.merienda(fontSize: 16 * 1.1, fontWeight: FontWeight.w600)),
|
||||
Switch(value: widget.childData.isUnbornChild, onChanged: widget.onToggleIsUnborn, activeColor: Theme.of(context).primaryColor),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 9.0 * 1.1), // 9.9
|
||||
CustomAppTextField(
|
||||
controller: _firstNameController,
|
||||
labelText: 'Prénom',
|
||||
hintText: 'Facultatif si à naître',
|
||||
isRequired: !widget.childData.isUnbornChild,
|
||||
fieldHeight: 55.0 * 1.1, // 60.5
|
||||
),
|
||||
const SizedBox(height: 6.0 * 1.1), // 6.6
|
||||
CustomAppTextField(
|
||||
controller: _lastNameController,
|
||||
labelText: 'Nom',
|
||||
hintText: 'Nom de l\'enfant',
|
||||
enabled: true,
|
||||
fieldHeight: 55.0 * 1.1, // 60.5
|
||||
),
|
||||
const SizedBox(height: 9.0 * 1.1), // 9.9
|
||||
CustomAppTextField(
|
||||
controller: _dobController,
|
||||
labelText: widget.childData.isUnbornChild ? 'Date prévisionnelle de naissance' : 'Date de naissance',
|
||||
hintText: 'JJ/MM/AAAA',
|
||||
readOnly: true,
|
||||
onTap: widget.onDateSelect,
|
||||
suffixIcon: Icons.calendar_today,
|
||||
fieldHeight: 55.0 * 1.1, // 60.5
|
||||
),
|
||||
const SizedBox(height: 11.0 * 1.1), // 12.1
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
AppCustomCheckbox(
|
||||
label: 'Consentement photo',
|
||||
value: widget.childData.photoConsent,
|
||||
onChanged: widget.onTogglePhotoConsent,
|
||||
checkboxSize: 22.0 * 1.1, // 24.2
|
||||
),
|
||||
const SizedBox(height: 6.0 * 1.1), // 6.6
|
||||
AppCustomCheckbox(
|
||||
label: 'Naissance multiple',
|
||||
value: widget.childData.multipleBirth,
|
||||
onChanged: widget.onToggleMultipleBirth,
|
||||
checkboxSize: 22.0 * 1.1, // 24.2
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (widget.canBeRemoved)
|
||||
Positioned(
|
||||
top: -5, right: -5,
|
||||
child: InkWell(
|
||||
onTap: widget.onRemove,
|
||||
customBorder: const CircleBorder(),
|
||||
child: Image.asset(
|
||||
'images/red_cross2.png',
|
||||
width: 36,
|
||||
height: 36,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:p_tits_pas/widgets/custom_decorated_text_field.dart'; // Import du nouveau widget
|
||||
import 'dart:math' as math; // Pour la rotation du chevron
|
||||
import 'package:p_tits_pas/widgets/app_custom_checkbox.dart'; // Import de la checkbox personnalisée
|
||||
// import 'package:p_tits_pas/models/placeholder_registration_data.dart'; // Remplacé
|
||||
import '../../../models/parent_user_registration_data.dart'; // Import du vrai modèle
|
||||
import '../../../utils/data_generator.dart'; // Import du générateur
|
||||
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
||||
|
||||
class ParentRegisterStep4Screen extends StatefulWidget {
|
||||
final UserRegistrationData registrationData; // Accepte les données
|
||||
|
||||
const ParentRegisterStep4Screen({super.key, required this.registrationData});
|
||||
|
||||
@override
|
||||
State<ParentRegisterStep4Screen> createState() => _ParentRegisterStep4ScreenState();
|
||||
}
|
||||
|
||||
class _ParentRegisterStep4ScreenState extends State<ParentRegisterStep4Screen> {
|
||||
late UserRegistrationData _registrationData; // État local
|
||||
final _motivationController = TextEditingController();
|
||||
bool _cguAccepted = true; // Pour le test, CGU acceptées par défaut
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registrationData = widget.registrationData;
|
||||
_motivationController.text = DataGenerator.motivation(); // Générer la motivation
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_motivationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showCGUModal() {
|
||||
// Un long texte Lorem Ipsum pour simuler les CGU
|
||||
const String loremIpsumText = '''
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
|
||||
|
||||
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna.
|
||||
|
||||
Aliquam convallis sollicitudin purus. Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet.
|
||||
|
||||
Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi. Proin porttitor, orci nec nonummy molestie, enim est eleifend mi, non fermentum diam nisl sit amet erat. Duis semper. Duis arcu massa, scelerisque vitae, consequat in, pretium a, enim. Pellentesque congue. Ut in risus volutpat libero pharetra tempor. Cras vestibulum bibendum augue. Praesent egestas leo in pede. Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam nibh. Mauris ac mauris sed pede pellentesque fermentum. Maecenas adipiscing ante non diam sodales hendrerit.
|
||||
|
||||
Ut velit mauris, egestas sed, gravida nec, ornare ut, mi. Aenean ut orci vel massa suscipit pulvinar. Nulla sollicitudin. Fusce varius, ligula non tempus aliquam, nunc turpis ullamcorper nibh, in tempus sapien eros vitae ligula. Pellentesque rhoncus nunc et augue. Integer id felis. Curabitur aliquet pellentesque diam. Integer quis metus vitae elit lobortis egestas. Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Morbi vel erat non mauris convallis vehicula. Nulla et sapien. Integer tortor tellus, aliquam faucibus, convallis id, congue eu, quam. Mauris ullamcorper felis vitae erat. Proin feugiat, augue non elementum posuere, metus purus iaculis lectus, et tristique ligula justo vitae magna. Etiam et felis dolor.
|
||||
|
||||
Praesent aliquam, enim at fermentum mollis, ligula massa adipiscing nisl, ac euismod nibh nisl eu lectus. Fusce vulputate sem at sapien. Vivamus leo. Aliquam euismod libero eu enim. Nulla nec felis sed leo placerat imperdiet. Aenean suscipit nulla in justo. Suspendisse cursus rutrum augue. Nulla tincidunt tincidunt mi. Curabitur iaculis, lorem vel rhoncus faucibus, felis magna fermentum augue, et ultricies lacus lorem varius purus. Curabitur eu amet. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
|
||||
|
||||
Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
|
||||
''';
|
||||
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false, // L'utilisateur doit utiliser le bouton
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Conditions Générales d\'Utilisation',
|
||||
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: SizedBox(
|
||||
width: MediaQuery.of(dialogContext).size.width * 0.7, // 70% de la largeur de l'écran
|
||||
height: MediaQuery.of(dialogContext).size.height * 0.6, // 60% de la hauteur de l'écran
|
||||
child: SingleChildScrollView(
|
||||
child: Text(
|
||||
loremIpsumText,
|
||||
style: GoogleFonts.merienda(fontSize: 13),
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
),
|
||||
),
|
||||
actionsPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0),
|
||||
actionsAlignment: MainAxisAlignment.center,
|
||||
actions: <Widget>[
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(dialogContext).primaryColor,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 15),
|
||||
),
|
||||
child: Text(
|
||||
'Valider et Accepter',
|
||||
style: GoogleFonts.merienda(fontSize: 15, color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop(); // Ferme la modale
|
||||
setState(() {
|
||||
_cguAccepted = true; // Met à jour l'état
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final cardWidth = screenSize.width * 0.6; // Largeur de la carte (60% de l'écran)
|
||||
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
|
||||
final cardHeight = cardWidth / imageAspectRatio;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat),
|
||||
),
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 50.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Étape 4/5',
|
||||
style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Motivation de votre demande',
|
||||
style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Container(
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(CardColorHorizontal.green.path),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(40.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomDecoratedTextField(
|
||||
controller: _motivationController,
|
||||
hintText: 'Écrivez ici pour motiver votre demande...',
|
||||
fieldHeight: cardHeight * 0.6,
|
||||
maxLines: 10,
|
||||
expandDynamically: true,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (!_cguAccepted) {
|
||||
_showCGUModal();
|
||||
}
|
||||
},
|
||||
child: AppCustomCheckbox(
|
||||
label: 'J\'accepte les conditions générales d\'utilisation',
|
||||
value: _cguAccepted,
|
||||
onChanged: (newValue) {
|
||||
if (!_cguAccepted) {
|
||||
_showCGUModal();
|
||||
} else {
|
||||
setState(() => _cguAccepted = false);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Chevrons de navigation
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
left: 40,
|
||||
child: IconButton(
|
||||
icon: Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi), child: Image.asset('assets/images/chevron_right.png', height: 40)),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
tooltip: 'Retour',
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
right: 40,
|
||||
child: IconButton(
|
||||
icon: Image.asset('assets/images/chevron_right.png', height: 40),
|
||||
onPressed: _cguAccepted
|
||||
? () {
|
||||
_registrationData.updateMotivation(_motivationController.text);
|
||||
_registrationData.acceptCGU();
|
||||
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
'/parent-register/step5',
|
||||
arguments: _registrationData
|
||||
);
|
||||
}
|
||||
: null,
|
||||
tooltip: 'Suivant',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:p_tits_pas/widgets/Summary.dart';
|
||||
import '../../../models/parent_user_registration_data.dart'; // Utilisation du vrai modèle
|
||||
import '../../../widgets/image_button.dart'; // Import du ImageButton
|
||||
import '../../../models/card_assets.dart'; // Import des enums de cartes
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import '../../../widgets/custom_decorated_text_field.dart'; // Import du CustomDecoratedTextField
|
||||
|
||||
// Nouvelle méthode helper pour afficher un champ de type "lecture seule" stylisé
|
||||
Widget _buildDisplayFieldValue(BuildContext context, String label, String value, {bool multiLine = false, double fieldHeight = 50.0, double labelFontSize = 18.0}) {
|
||||
const FontWeight labelFontWeight = FontWeight.w600;
|
||||
|
||||
// Ne pas afficher le label si labelFontSize est 0 ou si label est vide
|
||||
bool showLabel = label.isNotEmpty && labelFontSize > 0;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showLabel)
|
||||
Text(label, style: GoogleFonts.merienda(fontSize: labelFontSize, fontWeight: labelFontWeight)),
|
||||
if (showLabel)
|
||||
const SizedBox(height: 4),
|
||||
// Utiliser Expanded si multiLine et pas de hauteur fixe, sinon Container
|
||||
multiLine && fieldHeight == null
|
||||
? Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
image: const DecorationImage(
|
||||
image: AssetImage('assets/images/input_field_bg.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView( // Pour le défilement si le texte dépasse
|
||||
child: Text(
|
||||
value.isNotEmpty ? value : '-',
|
||||
style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0), // Garder une taille de texte par défaut si label caché
|
||||
maxLines: null, // Permettre un nombre illimité de lignes
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
width: double.infinity,
|
||||
height: multiLine ? null : fieldHeight,
|
||||
constraints: multiLine ? BoxConstraints(minHeight: fieldHeight) : null,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 12.0),
|
||||
decoration: BoxDecoration(
|
||||
image: const DecorationImage(
|
||||
image: AssetImage('assets/images/input_field_bg.png'),
|
||||
fit: BoxFit.fill,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
value.isNotEmpty ? value : '-',
|
||||
style: GoogleFonts.merienda(fontSize: labelFontSize > 0 ? labelFontSize : 18.0),
|
||||
maxLines: multiLine ? null : 1,
|
||||
overflow: multiLine ? TextOverflow.visible : TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class ParentRegisterStep5Screen extends StatelessWidget {
|
||||
final UserRegistrationData registrationData;
|
||||
|
||||
const ParentRegisterStep5Screen({super.key, required this.registrationData});
|
||||
|
||||
// Méthode pour construire la carte Parent 1
|
||||
Widget _buildParent1Card(BuildContext context, ParentData data) {
|
||||
const double verticalSpacing = 28.0; // Espacement vertical augmenté
|
||||
const double labelFontSize = 22.0; // Taille de label augmentée
|
||||
|
||||
List<Widget> details = [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: verticalSpacing),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: verticalSpacing),
|
||||
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize),
|
||||
];
|
||||
return _SummaryCard(
|
||||
backgroundImagePath: CardColorHorizontal.peach.path,
|
||||
title: 'Parent Principal',
|
||||
content: details,
|
||||
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step1', arguments: registrationData),
|
||||
);
|
||||
}
|
||||
|
||||
// Méthode pour construire la carte Parent 2
|
||||
Widget _buildParent2Card(BuildContext context, ParentData data) {
|
||||
const double verticalSpacing = 28.0;
|
||||
const double labelFontSize = 22.0;
|
||||
List<Widget> details = [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Nom:", data.lastName, labelFontSize: labelFontSize)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Prénom:", data.firstName, labelFontSize: labelFontSize)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: verticalSpacing),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Téléphone:", data.phone, labelFontSize: labelFontSize)),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(child: _buildDisplayFieldValue(context, "Email:", data.email, multiLine: true, labelFontSize: labelFontSize)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: verticalSpacing),
|
||||
_buildDisplayFieldValue(context, "Adresse:", "${data.address}\n${data.postalCode} ${data.city}".trim(), multiLine: true, fieldHeight: 80, labelFontSize: labelFontSize),
|
||||
];
|
||||
return SummaryCard(
|
||||
backgroundImagePath: CardColorHorizontal.blue.path,
|
||||
title: 'Deuxième Parent',
|
||||
content: details,
|
||||
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step2', arguments: registrationData),
|
||||
);
|
||||
}
|
||||
|
||||
// Méthode pour construire les cartes Enfants
|
||||
List<Widget> _buildChildrenCards(BuildContext context, List<ChildData> children) {
|
||||
return children.asMap().entries.map((entry) {
|
||||
int index = entry.key;
|
||||
ChildData child = entry.value;
|
||||
|
||||
CardColorHorizontal cardColorHorizontal = CardColorHorizontal.values.firstWhere(
|
||||
(e) => e.name == child.cardColor.name,
|
||||
orElse: () => CardColorHorizontal.lavender,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 2.0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(cardColorHorizontal.path),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Titre centré dans la carte
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Enfant ${index + 1}' + (child.isUnbornChild ? ' (à naître)' : ''),
|
||||
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
'/parent-register/step3',
|
||||
arguments: registrationData,
|
||||
);
|
||||
},
|
||||
tooltip: 'Modifier',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// IMAGE SANS CADRE BLANC, PREND LA HAUTEUR
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: (child.imageFile != null)
|
||||
? (kIsWeb
|
||||
? Image.network(child.imageFile!.path, fit: BoxFit.cover)
|
||||
: Image.file(child.imageFile!, fit: BoxFit.cover))
|
||||
: Image.asset('assets/images/photo.png', fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 32),
|
||||
// INFOS À DROITE (2/3)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildDisplayFieldValue(context, 'Prénom :', child.firstName, labelFontSize: 22.0),
|
||||
const SizedBox(height: 12),
|
||||
_buildDisplayFieldValue(context, 'Nom :', child.lastName, labelFontSize: 22.0),
|
||||
const SizedBox(height: 12),
|
||||
_buildDisplayFieldValue(context, child.isUnbornChild ? 'Date de naissance :' : 'Date de naissance :', child.dob, labelFontSize: 22.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
// Ligne des consentements
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: child.photoConsent,
|
||||
onChanged: null,
|
||||
),
|
||||
Text('Consentement photo', style: GoogleFonts.merienda(fontSize: 16)),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 32),
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: child.multipleBirth,
|
||||
onChanged: null,
|
||||
),
|
||||
Text('Naissance multiple', style: GoogleFonts.merienda(fontSize: 16)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Méthode pour construire la carte Motivation
|
||||
Widget _buildMotivationCard(BuildContext context, String motivation) {
|
||||
return _SummaryCard(
|
||||
backgroundImagePath: CardColorHorizontal.green.path,
|
||||
title: 'Votre Motivation',
|
||||
content: [
|
||||
Expanded(
|
||||
child: CustomDecoratedTextField(
|
||||
controller: TextEditingController(text: motivation),
|
||||
hintText: 'Aucune motivation renseignée.',
|
||||
fieldHeight: 200,
|
||||
maxLines: 10,
|
||||
expandDynamically: true,
|
||||
readOnly: true,
|
||||
fontSize: 18.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
onEdit: () => Navigator.of(context).pushNamed('/parent-register/step4', arguments: registrationData),
|
||||
);
|
||||
}
|
||||
|
||||
// Helper pour afficher une ligne de détail (police et agencement amélioré)
|
||||
Widget _buildDetailRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"$label: ",
|
||||
style: GoogleFonts.merienda(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value.isNotEmpty ? value : '-',
|
||||
style: GoogleFonts.merienda(fontSize: 18),
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final cardWidth = screenSize.width / 2.0; // Largeur de la carte (50% de l'écran)
|
||||
final double imageAspectRatio = 2.0; // Ratio corrigé (1024/512 = 2.0)
|
||||
final cardHeight = cardWidth / imageAspectRatio;
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Image.asset('assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeatY),
|
||||
),
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 40.0), // Padding horizontal supprimé ici
|
||||
child: Padding( // Ajout du Padding horizontal externe
|
||||
padding: EdgeInsets.symmetric(horizontal: screenSize.width / 4.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text('Étape 5/5', style: GoogleFonts.merienda(fontSize: 16, color: Colors.black54)),
|
||||
const SizedBox(height: 20),
|
||||
Text('Récapitulatif de votre demande', style: GoogleFonts.merienda(fontSize: 22, fontWeight: FontWeight.bold, color: Colors.black87), textAlign: TextAlign.center),
|
||||
const SizedBox(height: 30),
|
||||
|
||||
_buildParent1Card(context, registrationData.parent1),
|
||||
const SizedBox(height: 20),
|
||||
if (registrationData.parent2 != null) ...[
|
||||
_buildParent2Card(context, registrationData.parent2!),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
..._buildChildrenCards(context, registrationData.children),
|
||||
_buildMotivationCard(context, registrationData.motivationText),
|
||||
const SizedBox(height: 40),
|
||||
ImageButton(
|
||||
bg: 'assets/images/btn_green.png',
|
||||
text: 'Soumettre ma demande',
|
||||
textColor: const Color(0xFF2D6A4F),
|
||||
width: 350,
|
||||
height: 50,
|
||||
fontSize: 18,
|
||||
onPressed: () {
|
||||
print("Données finales: ${registrationData.parent1.firstName}, Enfant(s): ${registrationData.children.length}");
|
||||
_showConfirmationModal(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: screenSize.height / 2 - 20,
|
||||
left: 40,
|
||||
child: IconButton(
|
||||
icon: Transform.flip(flipX: true, child: Image.asset('assets/images/chevron_right.png', height: 40)),
|
||||
onPressed: () => Navigator.pop(context), // Retour à l'étape 4
|
||||
tooltip: 'Retour',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showConfirmationModal(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Demande enregistrée',
|
||||
style: GoogleFonts.merienda(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: Text(
|
||||
'Votre dossier a bien été pris en compte. Un gestionnaire le validera bientôt.',
|
||||
style: GoogleFonts.merienda(fontSize: 14),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: Text('OK', style: GoogleFonts.merienda(fontWeight: FontWeight.bold)),
|
||||
onPressed: () {
|
||||
Navigator.of(dialogContext).pop(); // Ferme la modale
|
||||
// TODO: Naviguer vers l'écran de connexion ou tableau de bord
|
||||
Navigator.of(context).pushNamedAndRemoveUntil('/login', (Route<dynamic> route) => false);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Widget générique _SummaryCard (ajusté)
|
||||
class _SummaryCard extends StatelessWidget {
|
||||
final String backgroundImagePath;
|
||||
final String title;
|
||||
final List<Widget> content;
|
||||
final VoidCallback onEdit;
|
||||
|
||||
const _SummaryCard({
|
||||
super.key,
|
||||
required this.backgroundImagePath,
|
||||
required this.title,
|
||||
required this.content,
|
||||
required this.onEdit,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AspectRatio(
|
||||
aspectRatio: 2.0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(backgroundImagePath),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
|
||||
onPressed: onEdit,
|
||||
tooltip: 'Modifier',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: content,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/controllers/parent_dashboard_controller.dart';
|
||||
import 'package:p_tits_pas/services/dashboardService.dart';
|
||||
import 'package:p_tits_pas/widgets/app_footer.dart';
|
||||
import 'package:p_tits_pas/widgets/dashbord_parent/app_layout.dart';
|
||||
import 'package:p_tits_pas/widgets/dashbord_parent/children_sidebar.dart';
|
||||
import 'package:p_tits_pas/widgets/dashbord_parent/dashboard_app_bar.dart';
|
||||
import 'package:p_tits_pas/widgets/dashbord_parent/wid_dashbord.dart';
|
||||
import 'package:p_tits_pas/widgets/main_content_area.dart';
|
||||
import 'package:p_tits_pas/widgets/messaging_sidebar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class ParentDashboardScreen extends StatefulWidget {
|
||||
const ParentDashboardScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ParentDashboardScreen> createState() => _ParentDashboardScreenState();
|
||||
}
|
||||
|
||||
class _ParentDashboardScreenState extends State<ParentDashboardScreen> {
|
||||
int selectedIndex = 0;
|
||||
|
||||
void onTabChange(int index) {
|
||||
setState(() {
|
||||
selectedIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Initialiser les données du dashboard
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<ParentDashboardController>().initDashboard();
|
||||
});
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
switch (selectedIndex) {
|
||||
case 0:
|
||||
return Dashbord_body();
|
||||
case 1:
|
||||
return const Center(child: Text("🔍 Trouver une nounou"));
|
||||
case 2:
|
||||
return const Center(child: Text("⚙️ Paramètres"));
|
||||
default:
|
||||
return const Center(child: Text("Page non trouvée"));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider(
|
||||
create: (context) => ParentDashboardController(DashboardService())..initDashboard(),
|
||||
child: Scaffold(
|
||||
appBar: PreferredSize(preferredSize: const Size.fromHeight(60.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
child: DashboardAppBar(
|
||||
selectedIndex: selectedIndex,
|
||||
onTabChange: onTabChange,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded (child: _getBody(),
|
||||
),
|
||||
const AppFooter(),
|
||||
],
|
||||
),
|
||||
)
|
||||
// body: _buildResponsiveBody(context, controller),
|
||||
// footer: const AppFooter(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResponsiveBody(BuildContext context, ParentDashboardController controller) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 768) {
|
||||
// Layout mobile : colonnes empilées
|
||||
return _buildMobileLayout(controller);
|
||||
} else if (constraints.maxWidth < 1024) {
|
||||
// Layout tablette : 2 colonnes
|
||||
return _buildTabletLayout(controller);
|
||||
} else {
|
||||
// Layout desktop : 3 colonnes
|
||||
return _buildDesktopLayout(controller);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDesktopLayout(ParentDashboardController controller) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Sidebar gauche - Enfants
|
||||
SizedBox(
|
||||
width: 280,
|
||||
child: ChildrenSidebar(
|
||||
children: controller.children,
|
||||
selectedChildId: controller.selectedChildId,
|
||||
onChildSelected: controller.selectChild,
|
||||
onAddChild: controller.showAddChildModal,
|
||||
),
|
||||
),
|
||||
|
||||
// Contenu central
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: MainContentArea(
|
||||
selectedChild: controller.selectedChild,
|
||||
selectedAssistant: controller.selectedAssistant,
|
||||
events: controller.upcomingEvents,
|
||||
contracts: controller.contracts,
|
||||
),
|
||||
),
|
||||
|
||||
// Sidebar droite - Messagerie
|
||||
SizedBox(
|
||||
width: 320,
|
||||
child: MessagingSidebar(
|
||||
conversations: controller.conversations,
|
||||
notifications: controller.notifications,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabletLayout(ParentDashboardController controller) {
|
||||
return Row(
|
||||
children: [
|
||||
// Sidebar enfants plus étroite
|
||||
SizedBox(
|
||||
width: 240,
|
||||
child: ChildrenSidebar(
|
||||
children: controller.children,
|
||||
selectedChildId: controller.selectedChildId,
|
||||
onChildSelected: controller.selectChild,
|
||||
onAddChild: controller.showAddChildModal,
|
||||
isCompact: true,
|
||||
),
|
||||
),
|
||||
|
||||
// Contenu principal avec messagerie intégrée
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: MainContentArea(
|
||||
selectedChild: controller.selectedChild,
|
||||
selectedAssistant: controller.selectedAssistant,
|
||||
events: controller.upcomingEvents,
|
||||
contracts: controller.contracts,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: MessagingSidebar(
|
||||
conversations: controller.conversations,
|
||||
notifications: controller.notifications,
|
||||
isCompact: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileLayout(ParentDashboardController controller) {
|
||||
return DefaultTabController(
|
||||
length: 4,
|
||||
child: Column(
|
||||
children: [
|
||||
// Navigation par onglets sur mobile
|
||||
Container(
|
||||
color: Theme.of(context).primaryColor.withOpacity(0.1),
|
||||
child: const TabBar(
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(text: 'Enfants', icon: Icon(Icons.child_care)),
|
||||
Tab(text: 'Planning', icon: Icon(Icons.calendar_month)),
|
||||
Tab(text: 'Contrats', icon: Icon(Icons.description)),
|
||||
Tab(text: 'Messages', icon: Icon(Icons.message)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
// Onglet Enfants
|
||||
ChildrenSidebar(
|
||||
children: controller.children,
|
||||
selectedChildId: controller.selectedChildId,
|
||||
onChildSelected: controller.selectChild,
|
||||
onAddChild: controller.showAddChildModal,
|
||||
isMobile: true,
|
||||
),
|
||||
|
||||
// Onglet Planning
|
||||
MainContentArea(
|
||||
selectedChild: controller.selectedChild,
|
||||
selectedAssistant: controller.selectedAssistant,
|
||||
events: controller.upcomingEvents,
|
||||
contracts: controller.contracts,
|
||||
showOnlyCalendar: true,
|
||||
),
|
||||
|
||||
// Onglet Contrats
|
||||
MainContentArea(
|
||||
selectedChild: controller.selectedChild,
|
||||
selectedAssistant: controller.selectedAssistant,
|
||||
events: controller.upcomingEvents,
|
||||
contracts: controller.contracts,
|
||||
showOnlyContracts: true,
|
||||
),
|
||||
|
||||
// Onglet Messages
|
||||
MessagingSidebar(
|
||||
conversations: controller.conversations,
|
||||
notifications: controller.notifications,
|
||||
isMobile: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FindNannyScreen extends StatelessWidget {
|
||||
const FindNannyScreen({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Trouver une nounou"),
|
||||
),
|
||||
body: Center(
|
||||
child: const Text("Contenu de la page Trouver une nounou"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
class ApiConfig {
|
||||
// static const String baseUrl = 'http://localhost:3000/api/v1/';
|
||||
static const String baseUrl = 'https://ynov.ptits-pas.fr/api/v1';
|
||||
|
||||
// Auth endpoints
|
||||
static const String login = '/auth/login';
|
||||
static const String register = '/auth/register';
|
||||
static const String refreshToken = '/auth/refresh';
|
||||
|
||||
// Users endpoints
|
||||
static const String users = '/users';
|
||||
static const String userProfile = '/users/profile';
|
||||
static const String userChildren = '/users/children';
|
||||
|
||||
// Dashboard endpoints
|
||||
static const String dashboard = '/dashboard';
|
||||
static const String events = '/events';
|
||||
static const String contracts = '/contracts';
|
||||
static const String conversations = '/conversations';
|
||||
static const String notifications = '/notifications';
|
||||
|
||||
// Headers
|
||||
static Map<String, String> get headers => {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
|
||||
static Map<String, String> authHeaders(String token) => {
|
||||
...headers,
|
||||
'Authorization': 'Bearer $token',
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class TokenService {
|
||||
// static const _storage = FlutterSecureStorage();
|
||||
static const _tokenKey = 'access_token';
|
||||
static const String _refreshTokenKey = 'refresh_token';
|
||||
static const _roleKey = 'user_role';
|
||||
|
||||
// Stockage du token
|
||||
static Future<void> saveToken(String token) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_tokenKey, token);
|
||||
}
|
||||
|
||||
// Stockage du refresh token
|
||||
static Future<void> saveRefreshToken(String refreshToken) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_refreshTokenKey, refreshToken);
|
||||
}
|
||||
|
||||
// Stockage du rôle
|
||||
static Future<void> saveRole(String role) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_roleKey, role);
|
||||
}
|
||||
|
||||
// Récupération du token
|
||||
static Future<String?> getToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_tokenKey);
|
||||
}
|
||||
|
||||
// Récupération du refresh token
|
||||
static Future<String?> getRefreshToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_refreshTokenKey);
|
||||
}
|
||||
|
||||
// Récupération du rôle
|
||||
static Future<String?> getRole() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(_roleKey);
|
||||
}
|
||||
|
||||
// Suppression du token
|
||||
static Future<void> deleteToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_tokenKey);
|
||||
}
|
||||
|
||||
// Suppression du refresh token
|
||||
static Future<void> deleteRefreshToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_refreshTokenKey);
|
||||
}
|
||||
|
||||
|
||||
// Suppression du rôle
|
||||
static Future<void> deleteRole() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_roleKey);
|
||||
}
|
||||
|
||||
// Nettoyage complet
|
||||
static Future<void> clearAll() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_tokenKey);
|
||||
await prefs.remove(_refreshTokenKey);
|
||||
await prefs.remove(_roleKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import 'package:p_tits_pas/models/m_dashbord/assistant_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/child_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/contract_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/conversation_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/event_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/notification_model.dart';
|
||||
|
||||
class DashboardService {
|
||||
// URL de base de l'API
|
||||
static const String _baseUrl = 'YOUR_API_BASE_URL';
|
||||
|
||||
// Récupérer la liste des enfants
|
||||
Future<List<ChildModel>> getChildren() async {
|
||||
try {
|
||||
// TODO: Implémenter l'appel API
|
||||
// Exemple de mock data pour le développement
|
||||
return [
|
||||
ChildModel(
|
||||
id: '1',
|
||||
firstName: 'Emma',
|
||||
birthDate: DateTime(2020, 5, 15),
|
||||
photoUrl: 'assets/images/child1.jpg',
|
||||
status: ChildStatus.onHoliday,
|
||||
),
|
||||
ChildModel(
|
||||
id: '2',
|
||||
firstName: 'Lucas',
|
||||
birthDate: DateTime(2021, 3, 10),
|
||||
photoUrl: 'assets/images/child2.jpg',
|
||||
status: ChildStatus.searching,
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération des enfants: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer l'assistante maternelle pour un enfant
|
||||
Future<AssistantModel> getAssistantForChild(String childId) async {
|
||||
try {
|
||||
// TODO: Implémenter l'appel API
|
||||
return AssistantModel(
|
||||
id: 'am1',
|
||||
firstName: 'Marie',
|
||||
lastName: 'Dupont',
|
||||
hourlyRate: 10.0,
|
||||
dailyFees: 80.0,
|
||||
status: AssistantStatus.available,
|
||||
photoUrl: 'assets/images/assistant1.jpg',
|
||||
address: '123 rue des Lilas',
|
||||
phone: '0123456789',
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération de l\'assistante: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les événements pour un enfant
|
||||
Future<List<EventModel>> getEventsForChild(String childId) async {
|
||||
try {
|
||||
// TODO: Implémenter l'appel API
|
||||
return [
|
||||
EventModel(
|
||||
id: 'evt1',
|
||||
title: 'Rendez-vous médical',
|
||||
startDate: DateTime.now().add(const Duration(days: 2)),
|
||||
type: EventType.parentVacation,
|
||||
status: EventStatus.pending,
|
||||
description: 'Visite de routine',
|
||||
childId: childId,
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération des événements: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer tous les événements à venir
|
||||
Future<List<EventModel>> getUpcomingEvents() async {
|
||||
try {
|
||||
// TODO: Implémenter l'appel API
|
||||
return [
|
||||
EventModel(
|
||||
id: 'evt1',
|
||||
title: 'Activité peinture',
|
||||
startDate: DateTime.now().add(const Duration(days: 1)),
|
||||
endDate: DateTime.now().add(const Duration(days: 1, hours: 2)),
|
||||
type: EventType.parentVacation,
|
||||
status: EventStatus.pending,
|
||||
description: 'Atelier créatif',
|
||||
childId: '1',
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération des événements: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les contrats
|
||||
Future<List<ContractModel>> getContracts() async {
|
||||
try {
|
||||
// TODO: Implémenter l'appel API
|
||||
return [
|
||||
ContractModel(
|
||||
id: 'contract1',
|
||||
childId: '1',
|
||||
assistantId: 'am1',
|
||||
startDate: DateTime(2023, 9, 1),
|
||||
endDate: DateTime(2024, 8, 31),
|
||||
status: ContractStatus.pending,
|
||||
hourlyRate: 10.0,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération des contrats: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les contrats pour un enfant spécifique
|
||||
Future<List<ContractModel>> getContractsForChild(String childId) async {
|
||||
try {
|
||||
// TODO: Implémenter l'appel API
|
||||
return [
|
||||
ContractModel(
|
||||
id: 'contract1',
|
||||
childId: childId,
|
||||
assistantId: 'am1',
|
||||
startDate: DateTime(2023, 9, 1),
|
||||
endDate: DateTime(2024, 8, 31),
|
||||
status: ContractStatus.active,
|
||||
hourlyRate: 10.0,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération des contrats: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les conversations
|
||||
Future<List<ConversationModel>> getConversations() async {
|
||||
try {
|
||||
// TODO: Implémenter l'appel API
|
||||
return [
|
||||
ConversationModel(
|
||||
id: 'conv1',
|
||||
title: 'Conversation avec Marie Dupont',
|
||||
participantIds: ['am1'],
|
||||
messages: [
|
||||
MessageModel(
|
||||
id: 'msg1',
|
||||
content: 'Bonjour, comment ça va ?',
|
||||
senderId: 'am1',
|
||||
sentAt: DateTime.now().subtract(const Duration(hours: 2)),
|
||||
status: MessageStatus.read,
|
||||
),
|
||||
MessageModel(
|
||||
id: 'msg2',
|
||||
content: 'Tout va bien, merci !',
|
||||
senderId: 'parent1',
|
||||
sentAt: DateTime.now().subtract(const Duration(hours: 1, minutes: 30)),
|
||||
status: MessageStatus.read,
|
||||
),
|
||||
],
|
||||
lastMessageAt: DateTime.now().subtract(const Duration(hours: 2)),
|
||||
unreadCount: 2,
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération des conversations: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les notifications
|
||||
Future<List<NotificationModel>> getNotifications() async {
|
||||
try {
|
||||
// TODO: Implémenter l'appel API
|
||||
return [
|
||||
NotificationModel(
|
||||
id: 'notif1',
|
||||
title: 'Nouveau message',
|
||||
createdAt: DateTime.now(),
|
||||
isRead: false,
|
||||
type: NotificationType.contractPending,
|
||||
content: 'Votre contrat est en attente',
|
||||
),
|
||||
];
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération des notifications: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Marquer une notification comme lue
|
||||
Future<void> markNotificationAsRead(String notificationId) async {
|
||||
try {
|
||||
// TODO: Implémenter l'appel API
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors du marquage de la notification: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class NavigationService {
|
||||
static void handleLoginSuccess(BuildContext context, String role) {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
Navigator.pushReplacementNamed(context, '/admin_dashboard');
|
||||
break;
|
||||
case 'gestionnaire':
|
||||
Navigator.pushReplacementNamed(context, '/gestionnaire_dashboard');
|
||||
break;
|
||||
case 'parent':
|
||||
Navigator.pushReplacementNamed(context, '/parent-dashboard');
|
||||
break;
|
||||
case 'assistante_maternelle':
|
||||
Navigator.pushReplacementNamed(context, '/assistante_maternelle_dashboard');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:p_tits_pas/widgets/custom_app_text_field.dart';
|
||||
|
||||
class ModularFormField {
|
||||
final String label;
|
||||
final String hint;
|
||||
final TextEditingController controller;
|
||||
final TextInputType? keyboardType;
|
||||
final bool isPassword;
|
||||
final String? Function(String?)? validator;
|
||||
final bool isRequired;
|
||||
final int flex;
|
||||
|
||||
ModularFormField({
|
||||
required this.label,
|
||||
required this.hint,
|
||||
required this.controller,
|
||||
this.keyboardType,
|
||||
this.isPassword = false,
|
||||
this.validator,
|
||||
this.isRequired = false,
|
||||
this.flex = 1,
|
||||
});
|
||||
}
|
||||
|
||||
class ModularForm extends StatelessWidget {
|
||||
final List<List<ModularFormField>> fieldGroups;
|
||||
final GlobalKey<FormState> formKey;
|
||||
final double? width;
|
||||
final EdgeInsets padding;
|
||||
final String? title;
|
||||
final VoidCallback? onSubmit;
|
||||
final String submitLabel;
|
||||
|
||||
const ModularForm({
|
||||
super.key,
|
||||
required this.fieldGroups,
|
||||
required this.formKey,
|
||||
this.width,
|
||||
this.padding = const EdgeInsets.all(20),
|
||||
this.title,
|
||||
this.onSubmit,
|
||||
this.submitLabel = "Suivant",
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: width ?? MediaQuery.of(context).size.width * 0.6,
|
||||
padding: padding,
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (title != null) ...[
|
||||
Text(
|
||||
title!,
|
||||
style: GoogleFonts.merienda(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
...fieldGroups.map((group) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: group.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final field = entry.value;
|
||||
|
||||
return [
|
||||
Expanded(
|
||||
flex: field.flex,
|
||||
child: CustomAppTextField(
|
||||
controller: field.controller,
|
||||
labelText: field.label,
|
||||
hintText: field.hint,
|
||||
obscureText: field.isPassword,
|
||||
keyboardType: field.keyboardType ?? TextInputType.text,
|
||||
validator: field.validator,
|
||||
style: CustomAppTextFieldStyle.beige,
|
||||
fieldWidth: double.infinity, // CORRECTION PRINCIPALE
|
||||
),
|
||||
),
|
||||
// Ajouter un espaceur entre les champs (sauf pour le dernier)
|
||||
if (index < group.length - 1)
|
||||
const Expanded(
|
||||
flex: 1,
|
||||
child: SizedBox(), // Espacement de 4% comme dans l'original
|
||||
),
|
||||
];
|
||||
}).expand((element) => element).toList(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
if (onSubmit != null)
|
||||
Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: onSubmit,
|
||||
child: Text(submitLabel),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class SummaryCard extends StatelessWidget {
|
||||
final String backgroundImagePath;
|
||||
final String title;
|
||||
final List<Widget> content;
|
||||
final VoidCallback onEdit;
|
||||
|
||||
const SummaryCard({
|
||||
super.key,
|
||||
required this.backgroundImagePath,
|
||||
required this.title,
|
||||
required this.content,
|
||||
required this.onEdit,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AspectRatio(
|
||||
aspectRatio: 2.0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 25.0),
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: AssetImage(backgroundImagePath),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: GoogleFonts.merienda(fontSize: 28, fontWeight: FontWeight.w600),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit, color: Colors.black54, size: 28),
|
||||
onPressed: onEdit,
|
||||
tooltip: 'Modifier',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: content,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AssistanteMaternelleManagementWidget extends StatelessWidget {
|
||||
const AssistanteMaternelleManagementWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final assistantes = [
|
||||
{
|
||||
"nom": "Marie Dupont",
|
||||
"numeroAgrement": "AG123456",
|
||||
"zone": "Paris 14",
|
||||
"capacite": 3,
|
||||
},
|
||||
{
|
||||
"nom": "Claire Martin",
|
||||
"numeroAgrement": "AG654321",
|
||||
"zone": "Lyon 7",
|
||||
"capacite": 2,
|
||||
},
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🔎 Zone de filtre
|
||||
_buildFilterSection(),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 📋 Liste des assistantes
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: assistantes.length,
|
||||
itemBuilder: (context, index) {
|
||||
final assistante = assistantes[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.face),
|
||||
title: Text(assistante['nom'].toString()),
|
||||
subtitle: Text(
|
||||
"N° Agrément : ${assistante['numeroAgrement']}\nZone : ${assistante['zone']} | Capacité : ${assistante['capacite']}"),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: () {
|
||||
// TODO: Ajouter modification
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () {
|
||||
// TODO: Ajouter suppression
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterSection() {
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Zone géographique",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (value) {
|
||||
// TODO: Ajouter logique de filtrage par zone
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Capacité minimum",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
// TODO: Ajouter logique de filtrage par capacité
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DashboardAppBarAdmin extends StatelessWidget implements PreferredSizeWidget {
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onTabChange;
|
||||
|
||||
const DashboardAppBarAdmin({Key? key, required this.selectedIndex, required this.onTabChange}) : super(key: key);
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight + 10);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = MediaQuery.of(context).size.width < 768;
|
||||
return AppBar(
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
title: Row(
|
||||
children: [
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.19),
|
||||
const Text(
|
||||
"P'tit Pas",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF9CC5C0),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
|
||||
|
||||
// Navigation principale
|
||||
_buildNavItem(context, 'Gestionnaires', 0),
|
||||
const SizedBox(width: 24),
|
||||
_buildNavItem(context, 'Parents', 1),
|
||||
const SizedBox(width: 24),
|
||||
_buildNavItem(context, 'Assistantes maternelles', 2),
|
||||
const SizedBox(width: 24),
|
||||
_buildNavItem(context, 'Administrateurs', 3),
|
||||
],
|
||||
),
|
||||
actions: isMobile
|
||||
? [_buildMobileMenu(context)]
|
||||
: [
|
||||
// Nom de l'utilisateur
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Admin',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Bouton déconnexion
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: TextButton(
|
||||
onPressed: () => _handleLogout(context),
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF9CC5C0),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
child: const Text('Se déconnecter'),
|
||||
),
|
||||
),
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem(BuildContext context, String title, int index) {
|
||||
final bool isActive = index == selectedIndex;
|
||||
return InkWell(
|
||||
onTap: () => onTabChange(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? const Color(0xFF9CC5C0) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: isActive ? null : Border.all(color: Colors.black26),
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isActive ? Colors.white : Colors.black,
|
||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildMobileMenu(BuildContext context) {
|
||||
return PopupMenuButton<int>(
|
||||
icon: const Icon(Icons.menu, color: Colors.white),
|
||||
onSelected: (value) {
|
||||
if (value == 4) {
|
||||
_handleLogout(context);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(value: 0, child: Text("Gestionnaires")),
|
||||
const PopupMenuItem(value: 1, child: Text("Parents")),
|
||||
const PopupMenuItem(value: 2, child: Text("Assistantes maternelles")),
|
||||
const PopupMenuItem(value: 3, child: Text("Administrateurs")),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem(value: 4, child: Text("Se déconnecter")),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _handleLogout(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Déconnexion'),
|
||||
content: const Text('Êtes-vous sûr de vouloir vous déconnecter ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Implémenter la logique de déconnexion
|
||||
},
|
||||
child: const Text('Déconnecter'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class GestionnaireCard extends StatelessWidget {
|
||||
final String name;
|
||||
final String email;
|
||||
|
||||
const GestionnaireCard({
|
||||
Key? key,
|
||||
required this.name,
|
||||
required this.email,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 🔹 Infos principales
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(name, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
Text(email, style: const TextStyle(color: Colors.grey)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 🔹 Attribution à des RPE (dropdown fictif ici)
|
||||
Row(
|
||||
children: [
|
||||
const Text("RPE attribué : "),
|
||||
const SizedBox(width: 8),
|
||||
DropdownButton<String>(
|
||||
value: "RPE 1",
|
||||
items: const [
|
||||
DropdownMenuItem(value: "RPE 1", child: Text("RPE 1")),
|
||||
DropdownMenuItem(value: "RPE 2", child: Text("RPE 2")),
|
||||
DropdownMenuItem(value: "RPE 3", child: Text("RPE 3")),
|
||||
],
|
||||
onChanged: (value) {},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 🔹 Boutons d'action
|
||||
Row(
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
// Réinitialisation mot de passe
|
||||
},
|
||||
icon: const Icon(Icons.lock_reset),
|
||||
label: const Text("Réinitialiser MDP"),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
// Suppression du compte
|
||||
},
|
||||
icon: const Icon(Icons.delete, color: Colors.red),
|
||||
label: const Text("Supprimer", style: TextStyle(color: Colors.red)),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/widgets/admin/gestionnaire_card.dart';
|
||||
|
||||
class GestionnaireManagementWidget extends StatelessWidget {
|
||||
const GestionnaireManagementWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 🔹 Barre du haut avec bouton
|
||||
Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: "Rechercher un gestionnaire...",
|
||||
prefixIcon: Icon(Icons.search),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Rediriger vers la page de création
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text("Créer un gestionnaire"),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 🔹 Liste des gestionnaires
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: 5, // À remplacer par liste dynamique
|
||||
itemBuilder: (context, index) {
|
||||
return GestionnaireCard(
|
||||
name: "Dupont $index",
|
||||
email: "dupont$index@mail.com",
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ParentManagementWidget extends StatelessWidget {
|
||||
const ParentManagementWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 🔁 Simulation de données parents
|
||||
final parents = [
|
||||
{
|
||||
"nom": "Jean Dupuis",
|
||||
"email": "jean.dupuis@email.com",
|
||||
"statut": "Actif",
|
||||
"enfants": 2,
|
||||
},
|
||||
{
|
||||
"nom": "Lucie Morel",
|
||||
"email": "lucie.morel@email.com",
|
||||
"statut": "En attente",
|
||||
"enfants": 1,
|
||||
},
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
_buildSearchSection(),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: parents.length,
|
||||
itemBuilder: (context, index) {
|
||||
final parent = parents[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.person_outline),
|
||||
title: Text(parent['nom'].toString()),
|
||||
subtitle: Text(
|
||||
"${parent['email']}\nStatut : ${parent['statut']} | Enfants : ${parent['enfants']}",
|
||||
),
|
||||
isThreeLine: true,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.visibility),
|
||||
tooltip: "Voir dossier",
|
||||
onPressed: () {
|
||||
// TODO: Voir le statut du dossier
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
tooltip: "Modifier",
|
||||
onPressed: () {
|
||||
// TODO: Modifier parent
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
tooltip: "Supprimer",
|
||||
onPressed: () {
|
||||
// TODO: Supprimer compte
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchSection() {
|
||||
return Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Nom du parent",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (value) {
|
||||
// TODO: Ajouter logique de recherche
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 220,
|
||||
child: DropdownButtonFormField<String>(
|
||||
decoration: const InputDecoration(
|
||||
labelText: "Statut",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: const [
|
||||
DropdownMenuItem(value: "Actif", child: Text("Actif")),
|
||||
DropdownMenuItem(value: "En attente", child: Text("En attente")),
|
||||
DropdownMenuItem(value: "Supprimé", child: Text("Supprimé")),
|
||||
],
|
||||
onChanged: (value) {
|
||||
// TODO: Ajouter logique de filtrage
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/child_model.dart';
|
||||
import 'package:p_tits_pas/services/bug_report_service.dart';
|
||||
|
||||
class AppFooter extends StatelessWidget {
|
||||
const AppFooter({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
// color: Colors.white,
|
||||
border: Border(
|
||||
top: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 768) {
|
||||
return _buildMobileFooter(context);
|
||||
} else {
|
||||
return _buildDesktopFooter(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDesktopFooter(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildFooterLink(context, 'Contact support', () => _handleContactSupport(context)),
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
|
||||
// _buildFooterDivider(),
|
||||
_buildFooterLink(context, 'Signaler un bug', () => _handleReportBug(context)),
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
|
||||
// _buildFooterDivider(),
|
||||
_buildFooterLink(context, 'Mentions légales', () => _handleLegalNotices(context)),
|
||||
// _buildFooterDivider(),
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
|
||||
_buildFooterLink(context, 'Politique de confidentialité', () => _handlePrivacyPolicy(context)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileFooter(BuildContext context) {
|
||||
return PopupMenuButton<String>(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Icon(Icons.info_outline, size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text('Informations'),
|
||||
Icon(Icons.keyboard_arrow_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(value: 'support', child: Text('Contact support')),
|
||||
const PopupMenuItem(value: 'bug', child: Text('Signaler un bug')),
|
||||
const PopupMenuItem(value: 'legal', child: Text('Mentions légales')),
|
||||
const PopupMenuItem(value: 'privacy', child: Text('Politique de confidentialité')),
|
||||
],
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'support':
|
||||
_handleContactSupport(context);
|
||||
break;
|
||||
case 'bug':
|
||||
_handleReportBug(context);
|
||||
break;
|
||||
case 'legal':
|
||||
_handleLegalNotices(context);
|
||||
break;
|
||||
case 'privacy':
|
||||
_handlePrivacyPolicy(context);
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooterLink(BuildContext context, String text, VoidCallback onTap) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleReportBug(BuildContext context) {
|
||||
final TextEditingController controller = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(
|
||||
'Signaler un bug',
|
||||
style: GoogleFonts.merienda(),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Décrivez le problème rencontré...',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Annuler',
|
||||
style: GoogleFonts.merienda(),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
if (controller.text.trim().isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Veuillez décrire le problème',
|
||||
style: GoogleFonts.merienda(),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await BugReportService.sendReport(controller.text);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Rapport envoyé avec succès',
|
||||
style: GoogleFonts.merienda(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'Erreur lors de l\'envoi du rapport',
|
||||
style: GoogleFonts.merienda(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
'Envoyer',
|
||||
style: GoogleFonts.merienda(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleLegalNotices(BuildContext context) {
|
||||
// Handle legal notices action
|
||||
Navigator.pushNamed(context, '/legal');
|
||||
}
|
||||
|
||||
void _handlePrivacyPolicy(BuildContext context) {
|
||||
// Handle privacy policy action
|
||||
Navigator.pushNamed(context, '/privacy');
|
||||
}
|
||||
|
||||
void _handleContactSupport(BuildContext context) {
|
||||
// Handle contact support action
|
||||
// Navigator.pushNamed(context, '/support');
|
||||
}
|
||||
|
||||
Widget _buildFooterDivider() {
|
||||
return Divider(
|
||||
color: Colors.grey[300],
|
||||
thickness: 1,
|
||||
height: 40,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Childrensidebarwidget extends StatelessWidget{
|
||||
final void Function(String childId) onChildSelected;
|
||||
|
||||
const Childrensidebarwidget({
|
||||
Key? key,
|
||||
required this.onChildSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = [
|
||||
{'id': '1', 'name': 'Léna', 'photo': null, 'status': 'Actif'},
|
||||
{'id': '2', 'name': 'Noé', 'photo': null, 'status': 'Inactif'},
|
||||
];
|
||||
|
||||
return Container(
|
||||
color: const Color(0xFFF7F7F7),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Avatar parent + bouton
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const CircleAvatar(radius: 24, child: Icon(Icons.person)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
// Naviguer vers ajout d'enfant
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text("Mes enfants", style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
// Liste des enfants
|
||||
...children.map((child) {
|
||||
return GestureDetector(
|
||||
onTap: () => onChildSelected(child['id']!),
|
||||
child: Card(
|
||||
color: child['status'] == 'Actif' ? Colors.teal.shade50 : Colors.white,
|
||||
child: ListTile(
|
||||
leading: const CircleAvatar(child: Icon(Icons.child_care)),
|
||||
title: Text(child['name']!),
|
||||
subtitle: Text(child['status']!),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppLayout extends StatelessWidget {
|
||||
final PreferredSizeWidget appBar;
|
||||
final Widget body;
|
||||
final Widget? footer;
|
||||
|
||||
const AppLayout({
|
||||
Key? key,
|
||||
required this.appBar,
|
||||
required this.body,
|
||||
this.footer,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F7FA),
|
||||
appBar: appBar,
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(child: body),
|
||||
if (footer != null) footer!,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/child_model.dart';
|
||||
|
||||
class ChildrenSidebar extends StatelessWidget {
|
||||
final List<ChildModel> children;
|
||||
final String? selectedChildId;
|
||||
final Function(String) onChildSelected;
|
||||
final VoidCallback onAddChild;
|
||||
final bool isCompact;
|
||||
final bool isMobile;
|
||||
|
||||
const ChildrenSidebar({
|
||||
Key? key,
|
||||
required this.children,
|
||||
this.selectedChildId,
|
||||
required this.onChildSelected,
|
||||
required this.onAddChild,
|
||||
this.isCompact = false,
|
||||
this.isMobile = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(isMobile ? 16 : 24),
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeader(context),
|
||||
const SizedBox(height: 20),
|
||||
_buildAddChildButton(context),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(child: _buildChildrenList()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
// UserAvatar(
|
||||
// size: isCompact ? 40 : 60,
|
||||
// name: 'Emma Dupont', // TODO: Récupérer depuis le contexte utilisateur
|
||||
// ),
|
||||
if (!isCompact) ...[
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Text(
|
||||
'Emma Dupont',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Icon(Icons.keyboard_arrow_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAddChildButton(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: onAddChild,
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(isCompact ? 'Ajouter' : 'Ajouter un enfant'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: isCompact ? 8 : 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChildrenList() {
|
||||
if (children.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Aucun enfant ajouté',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
itemCount: children.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final child = children[index];
|
||||
final isSelected = child.id == selectedChildId;
|
||||
|
||||
return _buildChildCard(context, child, isSelected);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChildCard(BuildContext context, ChildModel child, bool isSelected) {
|
||||
return InkWell(
|
||||
onTap: () => onChildSelected(child.id),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0xFF9CC5C0).withOpacity(0.1) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected ? const Color(0xFF9CC5C0) : Colors.grey.shade300,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// UserAvatar(
|
||||
// // size: isCompact ? 32 : 40,
|
||||
// // name: child.fullName,
|
||||
// // imageUrl: child.photoUrl,
|
||||
// ),
|
||||
if (!isCompact) ...[
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
child.firstName,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_buildChildStatus(child.status),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildChildStatus(ChildStatus status) {
|
||||
String label;
|
||||
Color color;
|
||||
|
||||
switch (status) {
|
||||
case ChildStatus.withAssistant:
|
||||
label = 'En garde';
|
||||
color = Colors.green;
|
||||
break;
|
||||
case ChildStatus.available:
|
||||
label = 'Disponible';
|
||||
color = Colors.blue;
|
||||
break;
|
||||
case ChildStatus.onHoliday:
|
||||
label = 'En vacances';
|
||||
color = Colors.orange;
|
||||
break;
|
||||
case ChildStatus.sick:
|
||||
label = 'Malade';
|
||||
color = Colors.red;
|
||||
break;
|
||||
case ChildStatus.searching:
|
||||
label = 'Recherche AM';
|
||||
color = Colors.purple;
|
||||
break;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final int selectedIndex;
|
||||
final ValueChanged<int> onTabChange;
|
||||
|
||||
const DashboardAppBar({Key? key, required this.selectedIndex, required this.onTabChange}) : super(key: key);
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight + 10);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isMobile = MediaQuery.of(context).size.width < 768;
|
||||
return AppBar(
|
||||
// backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
// Logo de la ville
|
||||
// Container(
|
||||
// height: 32,
|
||||
// width: 32,
|
||||
// decoration: BoxDecoration(
|
||||
// color: Colors.white,
|
||||
// borderRadius: BorderRadius.circular(8),
|
||||
// ),
|
||||
// child: const Icon(
|
||||
// Icons.location_city,
|
||||
// color: Color(0xFF9CC5C0),
|
||||
// size: 20,
|
||||
// ),
|
||||
// ),
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.19),
|
||||
const Text(
|
||||
"P'tit Pas",
|
||||
style: TextStyle(
|
||||
color: Color(0xFF9CC5C0),
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
|
||||
|
||||
// Navigation principale
|
||||
_buildNavItem(context, 'Mon tableau de bord', 0),
|
||||
const SizedBox(width: 24),
|
||||
_buildNavItem(context, 'Trouver une nounou', 1),
|
||||
const SizedBox(width: 24),
|
||||
_buildNavItem(context, 'Paramètres', 2),
|
||||
],
|
||||
),
|
||||
actions: isMobile
|
||||
? [_buildMobileMenu(context)]
|
||||
: [
|
||||
// Nom de l'utilisateur
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Jean Dupont',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Bouton déconnexion
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: TextButton(
|
||||
onPressed: () => _handleLogout(context),
|
||||
style: TextButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF9CC5C0),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
child: const Text('Se déconnecter'),
|
||||
),
|
||||
),
|
||||
SizedBox(width: MediaQuery.of(context).size.width * 0.1),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNavItem(BuildContext context, String title, int index) {
|
||||
final bool isActive = index == selectedIndex;
|
||||
return InkWell(
|
||||
onTap: () => onTabChange(index),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? const Color(0xFF9CC5C0) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: isActive ? null : Border.all(color: Colors.black26),
|
||||
),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: isActive ? Colors.white : Colors.black,
|
||||
fontWeight: isActive ? FontWeight.w600 : FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildMobileMenu(BuildContext context) {
|
||||
return PopupMenuButton<int>(
|
||||
icon: const Icon(Icons.menu, color: Colors.white),
|
||||
onSelected: (value) {
|
||||
if (value == 3) {
|
||||
_handleLogout(context);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(value: 0, child: Text("Mon tableau de bord")),
|
||||
const PopupMenuItem(value: 1, child: Text("Trouver une nounou")),
|
||||
const PopupMenuItem(value: 2, child: Text("Paramètres")),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem(value: 3, child: Text("Se déconnecter")),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _handleLogout(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Déconnexion'),
|
||||
content: const Text('Êtes-vous sûr de vouloir vous déconnecter ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
// TODO: Implémenter la logique de déconnexion
|
||||
},
|
||||
child: const Text('Déconnecter'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/widgets/dashbord_parent/ChildrenSidebarwidget.dart';
|
||||
import 'package:p_tits_pas/widgets/dashbord_parent/children_sidebar.dart';
|
||||
import 'package:p_tits_pas/widgets/dashbord_parent/wid_mainContentArea.dart';
|
||||
import 'package:p_tits_pas/widgets/messaging_sidebar.dart';
|
||||
|
||||
Widget Dashbord_body() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 1️⃣ Colonne de gauche : enfants
|
||||
SizedBox(
|
||||
width: 250,
|
||||
child: Childrensidebarwidget(
|
||||
onChildSelected: (childId) {
|
||||
// Met à jour l'enfant sélectionné
|
||||
// Tu peux stocker cet ID dans un state `selectedChildId`
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: WMainContentArea(
|
||||
// Passe l’enfant sélectionné si besoin
|
||||
),
|
||||
),
|
||||
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/widgets/messaging_sidebar.dart';
|
||||
|
||||
class WMainContentArea extends StatelessWidget {
|
||||
const WMainContentArea({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 🔷 Informations assistante maternelle (ligne complète)
|
||||
Card(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundImage: AssetImage("assets/images/am_photo.jpg"), // à adapter
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Text("Julie Dupont", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 4),
|
||||
Text("Taux horaire : 10€/h"),
|
||||
Text("Frais journaliers : 5€"),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Ouvrir le contrat
|
||||
},
|
||||
child: const Text("Voir le contrat"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// 🔷 Deux colonnes : planning + messagerie
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
// 📆 Planning de garde
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Text("Planning de garde", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text("Composant calendrier à intégrer ici"),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// 💬 Messagerie
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: MessagingSidebar(
|
||||
conversations: [],
|
||||
notifications: [],
|
||||
isCompact: false,
|
||||
isMobile: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/assistant_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/child_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/contract_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/event_model.dart';
|
||||
|
||||
class MainContentArea extends StatelessWidget {
|
||||
final ChildModel? selectedChild;
|
||||
final AssistantModel? selectedAssistant;
|
||||
final List<EventModel> events;
|
||||
final List<ContractModel> contracts;
|
||||
final bool showOnlyCalendar;
|
||||
final bool showOnlyContracts;
|
||||
|
||||
const MainContentArea({
|
||||
Key? key,
|
||||
this.selectedChild,
|
||||
this.selectedAssistant,
|
||||
required this.events,
|
||||
required this.contracts,
|
||||
this.showOnlyCalendar = false,
|
||||
this.showOnlyContracts = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!showOnlyCalendar && !showOnlyContracts) ...[
|
||||
if (selectedAssistant != null) _buildAssistantProfile(),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
if (showOnlyContracts || (!showOnlyCalendar && !showOnlyContracts)) ...[
|
||||
_buildContractsSection(),
|
||||
if (!showOnlyContracts) const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
if (showOnlyCalendar || (!showOnlyCalendar && !showOnlyContracts)) ...[
|
||||
Expanded(child: _buildCalendarSection()),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAssistantProfile() {
|
||||
if (selectedAssistant == null) {
|
||||
return _buildSearchAssistantCard();
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF9CC5C0),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
selectedAssistant!.fullName,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Taux horaire : ${selectedAssistant!.hourlyRateFormatted}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Text(
|
||||
'Frais journaliers : ${selectedAssistant!.dailyFeesFormatted}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// TODO: Navigation vers le contrat
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF9CC5C0),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Voir le contrat'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchAssistantCard() {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.search,
|
||||
size: 48,
|
||||
color: Colors.grey.shade400,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Aucune assistante maternelle assignée',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Trouvez une assistante maternelle pour votre enfant',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// TODO: Navigation vers la recherche
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF9CC5C0),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Rechercher une assistante maternelle'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCalendarSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Planning de garde pour ${selectedChild?.firstName ?? "votre enfant"}',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// TODO: Mode sélection de plage
|
||||
},
|
||||
child: const Text('Mode sélection de plage'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: _buildCalendar(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCalendar() {
|
||||
// Placeholder pour le calendrier - sera développé dans FRONT-11
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Calendrier à implémenter\n(FRONT-11)',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContractsSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Contrats',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (contracts.isEmpty)
|
||||
const Text(
|
||||
'Aucun contrat en cours',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
)
|
||||
else
|
||||
...contracts.map((contract) => _buildContractItem(contract)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContractItem(ContractModel contract) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: _getContractStatusColor(contract.status),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(contract.statusLabel),
|
||||
),
|
||||
if (contract.needsSignature)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// TODO: Action signature
|
||||
},
|
||||
child: const Text('Signer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _getContractStatusColor(ContractStatus status) {
|
||||
switch (status) {
|
||||
case ContractStatus.draft:
|
||||
return Colors.grey;
|
||||
case ContractStatus.pending:
|
||||
return Colors.orange;
|
||||
case ContractStatus.active:
|
||||
return Colors.green;
|
||||
case ContractStatus.ended:
|
||||
return Colors.blue;
|
||||
case ContractStatus.cancelled:
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/conversation_model.dart';
|
||||
import 'package:p_tits_pas/models/m_dashbord/notification_model.dart';
|
||||
|
||||
class MessagingSidebar extends StatelessWidget {
|
||||
final List<ConversationModel> conversations;
|
||||
final List<NotificationModel> notifications;
|
||||
final bool isCompact;
|
||||
final bool isMobile;
|
||||
|
||||
const MessagingSidebar({
|
||||
Key? key,
|
||||
required this.conversations,
|
||||
required this.notifications,
|
||||
this.isCompact = false,
|
||||
this.isMobile = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.all(isMobile ? 16 : 20),
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildMessagingHeader(),
|
||||
const SizedBox(height: 20),
|
||||
Expanded(
|
||||
child: _buildMessagingContent(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildContactRPEButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessagingHeader() {
|
||||
return const Text(
|
||||
'Messagerie avec Emma Dupont',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessagingContent() {
|
||||
return Column(
|
||||
children: [
|
||||
// Messages existants
|
||||
Expanded(
|
||||
child: _buildMessagesList(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Zone de saisie
|
||||
_buildMessageInput(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessagesList() {
|
||||
if (conversations.isEmpty) {
|
||||
return const Center(
|
||||
child: Text(
|
||||
'Aucun message',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Pour la démo, on affiche quelques messages fictifs
|
||||
return ListView(
|
||||
children: [
|
||||
_buildMessageBubble(
|
||||
'Bonjour, Emma a bien dormi aujourd\'hui.',
|
||||
isFromCurrentUser: false,
|
||||
timestamp: DateTime.now().subtract(const Duration(hours: 2)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildMessageBubble(
|
||||
'Merci pour l\'information. Elle a bien mangé ?',
|
||||
isFromCurrentUser: true,
|
||||
timestamp: DateTime.now().subtract(const Duration(hours: 1)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageBubble(String content, {required bool isFromCurrentUser, required DateTime timestamp}) {
|
||||
return Align(
|
||||
alignment: isFromCurrentUser ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 250),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isFromCurrentUser
|
||||
? const Color(0xFF9CC5C0)
|
||||
: Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
content,
|
||||
style: TextStyle(
|
||||
color: isFromCurrentUser ? Colors.white : Colors.black87,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_formatTimestamp(timestamp),
|
||||
style: TextStyle(
|
||||
color: isFromCurrentUser
|
||||
? Colors.white.withOpacity(0.8)
|
||||
: Colors.grey.shade600,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageInput() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Expanded(
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Écrivez votre message...',
|
||||
border: InputBorder.none,
|
||||
isDense: true,
|
||||
),
|
||||
maxLines: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: const Color(0xFF9CC5C0),
|
||||
child: IconButton(
|
||||
iconSize: 16,
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () {
|
||||
// TODO: Envoyer le message
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.send,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContactRPEButton() {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
// TODO: Contacter le RPE
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Contacter le Relais Petite Enfance',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatTimestamp(DateTime timestamp) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(timestamp);
|
||||
|
||||
if (difference.inMinutes < 1) {
|
||||
return 'À l\'instant';
|
||||
} else if (difference.inHours < 1) {
|
||||
return '${difference.inMinutes}m';
|
||||
} else if (difference.inDays < 1) {
|
||||
return '${difference.inHours}h';
|
||||
} else {
|
||||
return '${timestamp.day}/${timestamp.month}';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user