From 9f874f30e7c0568e91f108ebfed048f9276d84c8 Mon Sep 17 00:00:00 2001 From: Hanim Date: Thu, 28 Aug 2025 12:58:44 +0200 Subject: [PATCH] feat: Add dashboard layout with sidebar and main content area - Implemented AppFooter widget for mobile and desktop views. - Created ChildrenSidebar widget to display children's information. - Developed AppLayout to manage app structure with optional footer. - Added ChildrenSidebar for selecting children and displaying their status. - Introduced DashboardAppBar for navigation and user actions. - Built WMainContentArea for displaying assistant details and calendar. - Created MainContentArea to manage contracts and events display. - Implemented MessagingSidebar for messaging functionality. - Updated widget tests to reflect new structure and imports. --- .../parent_dashboard_controller.dart | 138 ++++++++ frontend/lib/main.dart | 5 +- .../models/m_dashbord/assistant_model.dart | 51 +++ .../lib/models/m_dashbord/child_model.dart | 58 ++++ .../lib/models/m_dashbord/contract_model.dart | 65 ++++ .../models/m_dashbord/conversation_model.dart | 46 +++ .../lib/models/m_dashbord/event_model.dart | 66 ++++ .../models/m_dashbord/notification_model.dart | 42 +++ frontend/lib/navigation/app_router.dart | 13 +- frontend/lib/screens/home/home_screen.dart | 2 +- .../parent_screen/ParentDashboardScreen.dart | 242 +++++++++++++ .../home/parent_screen/find_nanny.dart | 17 + frontend/lib/services/dashboardService.dart | 202 +++++++++++ frontend/lib/widgets/app_footer.dart | 209 +++++++++++ .../ChildrenSidebarwidget.dart | 58 ++++ .../widgets/dashbord_parent/app_layout.dart | 28 ++ .../dashbord_parent/children_sidebar.dart | 203 +++++++++++ .../dashbord_parent/dashboard_app_bar.dart | 156 +++++++++ .../widgets/dashbord_parent/wid_dashbord.dart | 31 ++ .../dashbord_parent/wid_mainContentArea.dart | 94 +++++ frontend/lib/widgets/main_content_area.dart | 326 ++++++++++++++++++ frontend/lib/widgets/messaging_sidebar.dart | 207 +++++++++++ frontend/test/widget_test.dart | 2 +- 23 files changed, 2251 insertions(+), 10 deletions(-) create mode 100644 frontend/lib/controllers/parent_dashboard_controller.dart create mode 100644 frontend/lib/models/m_dashbord/assistant_model.dart create mode 100644 frontend/lib/models/m_dashbord/child_model.dart create mode 100644 frontend/lib/models/m_dashbord/contract_model.dart create mode 100644 frontend/lib/models/m_dashbord/conversation_model.dart create mode 100644 frontend/lib/models/m_dashbord/event_model.dart create mode 100644 frontend/lib/models/m_dashbord/notification_model.dart create mode 100644 frontend/lib/screens/home/parent_screen/ParentDashboardScreen.dart create mode 100644 frontend/lib/screens/home/parent_screen/find_nanny.dart create mode 100644 frontend/lib/services/dashboardService.dart create mode 100644 frontend/lib/widgets/app_footer.dart create mode 100644 frontend/lib/widgets/dashbord_parent/ChildrenSidebarwidget.dart create mode 100644 frontend/lib/widgets/dashbord_parent/app_layout.dart create mode 100644 frontend/lib/widgets/dashbord_parent/children_sidebar.dart create mode 100644 frontend/lib/widgets/dashbord_parent/dashboard_app_bar.dart create mode 100644 frontend/lib/widgets/dashbord_parent/wid_dashbord.dart create mode 100644 frontend/lib/widgets/dashbord_parent/wid_mainContentArea.dart create mode 100644 frontend/lib/widgets/main_content_area.dart create mode 100644 frontend/lib/widgets/messaging_sidebar.dart diff --git a/frontend/lib/controllers/parent_dashboard_controller.dart b/frontend/lib/controllers/parent_dashboard_controller.dart new file mode 100644 index 0000000..143d84a --- /dev/null +++ b/frontend/lib/controllers/parent_dashboard_controller.dart @@ -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 _children = []; + String? _selectedChildId; + AssistantModel? _selectedAssistant; + List _upcomingEvents = []; + List _contracts = []; + List _conversations = []; + List _notifications = []; + + // État de chargement + bool _isLoading = false; + String? _error; + + // Getters + List get children => _children; + String? get selectedChildId => _selectedChildId; + ChildModel? get selectedChild => _children.where((c) => c.id == _selectedChildId).firstOrNull; + AssistantModel? get selectedAssistant => _selectedAssistant; + List get upcomingEvents => _upcomingEvents; + List get contracts => _contracts; + List get conversations => _conversations; + List get notifications => _notifications; + bool get isLoading => _isLoading; + String? get error => _error; + + // Initialisation du dashboard + Future 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 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 _loadChildren() async { + _children = await _dashboardService.getChildren(); + notifyListeners(); + } + + Future _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 _loadUpcomingEvents() async { + _upcomingEvents = await _dashboardService.getUpcomingEvents(); + notifyListeners(); + } + + Future _loadContracts() async { + _contracts = await _dashboardService.getContracts(); + notifyListeners(); + } + + Future _loadConversations() async { + _conversations = await _dashboardService.getConversations(); + notifyListeners(); + } + + Future _loadNotifications() async { + _notifications = await _dashboardService.getNotifications(); + notifyListeners(); + } + + // Méthodes d'action + Future 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 refreshDashboard() async { + await initDashboard(); + } +} \ No newline at end of file diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 7cf07f5..56be658 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,10 +1,7 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; // Import pour la localisation -// import 'package:provider/provider.dart'; // Supprimer Provider +import 'package:flutter_localizations/flutter_localizations.dart'; import 'navigation/app_router.dart'; -// import 'theme/app_theme.dart'; // Supprimer AppTheme -// import 'theme/theme_provider.dart'; // Supprimer ThemeProvider void main() { runApp(const MyApp()); // Exécution simple diff --git a/frontend/lib/models/m_dashbord/assistant_model.dart b/frontend/lib/models/m_dashbord/assistant_model.dart new file mode 100644 index 0000000..bc880c3 --- /dev/null +++ b/frontend/lib/models/m_dashbord/assistant_model.dart @@ -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 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, +} \ No newline at end of file diff --git a/frontend/lib/models/m_dashbord/child_model.dart b/frontend/lib/models/m_dashbord/child_model.dart new file mode 100644 index 0000000..2732b1c --- /dev/null +++ b/frontend/lib/models/m_dashbord/child_model.dart @@ -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 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 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 +} \ No newline at end of file diff --git a/frontend/lib/models/m_dashbord/contract_model.dart b/frontend/lib/models/m_dashbord/contract_model.dart new file mode 100644 index 0000000..9b65ce8 --- /dev/null +++ b/frontend/lib/models/m_dashbord/contract_model.dart @@ -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? 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 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, +} \ No newline at end of file diff --git a/frontend/lib/models/m_dashbord/conversation_model.dart b/frontend/lib/models/m_dashbord/conversation_model.dart new file mode 100644 index 0000000..059d57a --- /dev/null +++ b/frontend/lib/models/m_dashbord/conversation_model.dart @@ -0,0 +1,46 @@ +class ConversationModel { + final String id; + final String title; + final List participantIds; + final List 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, +} \ No newline at end of file diff --git a/frontend/lib/models/m_dashbord/event_model.dart b/frontend/lib/models/m_dashbord/event_model.dart new file mode 100644 index 0000000..39702ed --- /dev/null +++ b/frontend/lib/models/m_dashbord/event_model.dart @@ -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 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, +} \ No newline at end of file diff --git a/frontend/lib/models/m_dashbord/notification_model.dart b/frontend/lib/models/m_dashbord/notification_model.dart new file mode 100644 index 0000000..09d1ecf --- /dev/null +++ b/frontend/lib/models/m_dashbord/notification_model.dart @@ -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? 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 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 +} \ No newline at end of file diff --git a/frontend/lib/navigation/app_router.dart b/frontend/lib/navigation/app_router.dart index 3fd8b6c..5992db3 100644 --- a/frontend/lib/navigation/app_router.dart +++ b/frontend/lib/navigation/app_router.dart @@ -4,6 +4,8 @@ import 'package:p_tits_pas/screens/auth/am/am_register_step1_sceen.dart'; import 'package:p_tits_pas/screens/auth/am/am_register_step2_sceen.dart'; import 'package:p_tits_pas/screens/auth/am/am_register_step3_sceen.dart'; import 'package:p_tits_pas/screens/auth/am/am_register_step4_sceen.dart'; +import 'package:p_tits_pas/screens/home/parent_screen/ParentDashboardScreen.dart'; +import 'package:p_tits_pas/screens/home/parent_screen/find_nanny.dart'; import 'package:p_tits_pas/screens/legal/legal_page.dart'; import 'package:p_tits_pas/screens/legal/privacy_page.dart'; import '../screens/auth/login_screen.dart'; @@ -13,7 +15,6 @@ import '../screens/auth/parent/parent_register_step2_screen.dart'; import '../screens/auth/parent/parent_register_step3_screen.dart'; import '../screens/auth/parent/parent_register_step4_screen.dart'; import '../screens/auth/parent/parent_register_step5_screen.dart'; -import '../screens/home/home_screen.dart'; import '../models/parent_user_registration_data.dart'; class AppRouter { @@ -31,7 +32,8 @@ class AppRouter { static const String amRegisterStep2 = '/am-register/step2'; static const String amRegisterStep3 = '/am-register/step3'; static const String amRegisterStep4 = '/am-register/step4'; - static const String home = '/home'; + static const String parentDashboard = '/parent-dashboard'; + static const String findNanny = '/find-nanny'; static Route generateRoute(RouteSettings settings) { Widget screen; @@ -119,8 +121,11 @@ class AppRouter { } slideTransition = true; break; - case home: - screen = const HomeScreen(); + case parentDashboard: + screen = const ParentDashboardScreen(); + break; + case findNanny: + screen = const FindNannyScreen(); break; default: screen = Scaffold( diff --git a/frontend/lib/screens/home/home_screen.dart b/frontend/lib/screens/home/home_screen.dart index 9a0c6ba..ad836ca 100644 --- a/frontend/lib/screens/home/home_screen.dart +++ b/frontend/lib/screens/home/home_screen.dart @@ -14,4 +14,4 @@ class HomeScreen extends StatelessWidget { ), ); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/lib/screens/home/parent_screen/ParentDashboardScreen.dart b/frontend/lib/screens/home/parent_screen/ParentDashboardScreen.dart new file mode 100644 index 0000000..81deb26 --- /dev/null +++ b/frontend/lib/screens/home/parent_screen/ParentDashboardScreen.dart @@ -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 createState() => _ParentDashboardScreenState(); +} + +class _ParentDashboardScreenState extends State { + 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().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, + ), + ], + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/home/parent_screen/find_nanny.dart b/frontend/lib/screens/home/parent_screen/find_nanny.dart new file mode 100644 index 0000000..6955ac7 --- /dev/null +++ b/frontend/lib/screens/home/parent_screen/find_nanny.dart @@ -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"), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/services/dashboardService.dart b/frontend/lib/services/dashboardService.dart new file mode 100644 index 0000000..31bc45d --- /dev/null +++ b/frontend/lib/services/dashboardService.dart @@ -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> 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 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> 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> 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> 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> 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> 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> 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 markNotificationAsRead(String notificationId) async { + try { + // TODO: Implémenter l'appel API + } catch (e) { + throw Exception('Erreur lors du marquage de la notification: $e'); + } + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/app_footer.dart b/frontend/lib/widgets/app_footer.dart new file mode 100644 index 0000000..a4559d2 --- /dev/null +++ b/frontend/lib/widgets/app_footer.dart @@ -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( + 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, + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/dashbord_parent/ChildrenSidebarwidget.dart b/frontend/lib/widgets/dashbord_parent/ChildrenSidebarwidget.dart new file mode 100644 index 0000000..de46d41 --- /dev/null +++ b/frontend/lib/widgets/dashbord_parent/ChildrenSidebarwidget.dart @@ -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() + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/dashbord_parent/app_layout.dart b/frontend/lib/widgets/dashbord_parent/app_layout.dart new file mode 100644 index 0000000..d1dbd45 --- /dev/null +++ b/frontend/lib/widgets/dashbord_parent/app_layout.dart @@ -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!, + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/dashbord_parent/children_sidebar.dart b/frontend/lib/widgets/dashbord_parent/children_sidebar.dart new file mode 100644 index 0000000..0dd681f --- /dev/null +++ b/frontend/lib/widgets/dashbord_parent/children_sidebar.dart @@ -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 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, + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/dashbord_parent/dashboard_app_bar.dart b/frontend/lib/widgets/dashbord_parent/dashboard_app_bar.dart new file mode 100644 index 0000000..c89fd6e --- /dev/null +++ b/frontend/lib/widgets/dashbord_parent/dashboard_app_bar.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; + +class DashboardAppBar extends StatelessWidget implements PreferredSizeWidget { + final int selectedIndex; + final ValueChanged 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( + 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'), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/dashbord_parent/wid_dashbord.dart b/frontend/lib/widgets/dashbord_parent/wid_dashbord.dart new file mode 100644 index 0000000..6ed87cf --- /dev/null +++ b/frontend/lib/widgets/dashbord_parent/wid_dashbord.dart @@ -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 + ), + ), + + ], + ); +} diff --git a/frontend/lib/widgets/dashbord_parent/wid_mainContentArea.dart b/frontend/lib/widgets/dashbord_parent/wid_mainContentArea.dart new file mode 100644 index 0000000..6bc8d07 --- /dev/null +++ b/frontend/lib/widgets/dashbord_parent/wid_mainContentArea.dart @@ -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, + ), + ), + ], + ), + ) + ], + ), + ); + } +} diff --git a/frontend/lib/widgets/main_content_area.dart b/frontend/lib/widgets/main_content_area.dart new file mode 100644 index 0000000..6b8c0ac --- /dev/null +++ b/frontend/lib/widgets/main_content_area.dart @@ -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 events; + final List 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; + } + } +} \ No newline at end of file diff --git a/frontend/lib/widgets/messaging_sidebar.dart b/frontend/lib/widgets/messaging_sidebar.dart new file mode 100644 index 0000000..a417be4 --- /dev/null +++ b/frontend/lib/widgets/messaging_sidebar.dart @@ -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 conversations; + final List 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}'; + } + } +} \ No newline at end of file diff --git a/frontend/test/widget_test.dart b/frontend/test/widget_test.dart index a6ee807..a26aed4 100644 --- a/frontend/test/widget_test.dart +++ b/frontend/test/widget_test.dart @@ -7,8 +7,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:p_tits_pas/main.dart'; -import 'package:petitspas/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async {