diff --git a/frontend/assets/images/bg_ivoire_pill.png b/frontend/assets/images/bg_ivoire_pill.png new file mode 100644 index 0000000..6604289 Binary files /dev/null and b/frontend/assets/images/bg_ivoire_pill.png differ diff --git a/frontend/assets/images/bg_lavender_pill.png b/frontend/assets/images/bg_lavender_pill.png new file mode 100644 index 0000000..69ad8c4 Binary files /dev/null and b/frontend/assets/images/bg_lavender_pill.png differ diff --git a/frontend/assets/images/bg_peach_pill.png b/frontend/assets/images/bg_peach_pill.png new file mode 100644 index 0000000..87b1764 Binary files /dev/null and b/frontend/assets/images/bg_peach_pill.png differ diff --git a/frontend/assets/images/bg_turquoise_pill.png b/frontend/assets/images/bg_turquoise_pill.png new file mode 100644 index 0000000..fae2e35 Binary files /dev/null and b/frontend/assets/images/bg_turquoise_pill.png differ diff --git a/frontend/assets/images/bg_yellow_pill.png b/frontend/assets/images/bg_yellow_pill.png new file mode 100644 index 0000000..c964ab2 Binary files /dev/null and b/frontend/assets/images/bg_yellow_pill.png differ diff --git a/frontend/assets/images/pencil_line_grey.png b/frontend/assets/images/pencil_line_grey.png new file mode 100644 index 0000000..bf434ec Binary files /dev/null and b/frontend/assets/images/pencil_line_grey.png differ diff --git a/frontend/assets/images/pencil_line_grey_v.png b/frontend/assets/images/pencil_line_grey_v.png new file mode 100644 index 0000000..a692c36 Binary files /dev/null and b/frontend/assets/images/pencil_line_grey_v.png differ diff --git a/frontend/lib/screens/home/parent_screen/ParentDashboardScreen.dart b/frontend/lib/screens/home/parent_screen/ParentDashboardScreen.dart index c71e4dc..543689c 100644 --- a/frontend/lib/screens/home/parent_screen/ParentDashboardScreen.dart +++ b/frontend/lib/screens/home/parent_screen/ParentDashboardScreen.dart @@ -1,41 +1,26 @@ import 'package:flutter/material.dart'; -import 'package:p_tits_pas/controllers/parent_dashboard_controller.dart'; import 'package:p_tits_pas/models/user.dart'; import 'package:p_tits_pas/services/auth_service.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/children_sidebar.dart'; -import 'package:p_tits_pas/widgets/dashbord_parent/wid_dashbord.dart'; -import 'package:p_tits_pas/widgets/dashboard/dashboard_bandeau.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'; +import 'package:p_tits_pas/widgets/quotidien/quotidien_shell.dart'; +import 'package:p_tits_pas/widgets/quotidien/quotidien_theme.dart'; +/// Tableau de bord parent — coquille 3 colonnes quotidien (#166). +/// Métier cartes / blog / messagerie : tickets C/D/E. class ParentDashboardScreen extends StatefulWidget { - const ParentDashboardScreen({Key? key}) : super(key: key); + const ParentDashboardScreen({super.key}); @override State createState() => _ParentDashboardScreenState(); } class _ParentDashboardScreenState extends State { - int selectedIndex = 0; + QuotidienNavSection _section = QuotidienNavSection.liaison; AppUser? _user; - void onTabChange(int index) { - setState(() { - selectedIndex = index; - }); - } - @override void initState() { super.initState(); _loadUser(); - // Initialiser les données du dashboard - WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().initDashboard(); - }); } Future _loadUser() async { @@ -43,222 +28,56 @@ class _ParentDashboardScreenState extends State { if (mounted) setState(() => _user = user); } - 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")); - } + String get _displayName { + final n = _user?.fullName.trim() ?? ''; + if (n.isNotEmpty) return n; + final email = _user?.email.trim() ?? ''; + if (email.isNotEmpty) return email.split('@').first; + return 'Parent'; + } + + void _soon(String label) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$label — à venir')), + ); } @override Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (context) => ParentDashboardController(DashboardService())..initDashboard(), - child: Scaffold( - appBar: PreferredSize( - preferredSize: const Size.fromHeight(60.0), - child: DashboardBandeau( - tabItems: const [ - DashboardTabItem(label: 'Mon tableau de bord'), - DashboardTabItem(label: 'Trouver une nounou'), - DashboardTabItem(label: 'Paramètres'), - ], - selectedTabIndex: selectedIndex, - onTabSelected: onTabChange, - userDisplayName: _user?.fullName.isNotEmpty == true - ? _user!.fullName - : 'Parent', - userEmail: _user?.email, - userRole: _user?.role, - onProfileTap: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Modification du profil – à venir')), - ); - }, - onSettingsTap: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Paramètres – à venir')), - ); - }, - onLogout: () {}, - showLogoutConfirmation: true, - ), - ), - body: Column( - children: [ - Expanded(child: _getBody()), - const AppFooter(), - ], + return QuotidienShell( + selectedSection: _section, + onSectionSelected: (s) => setState(() => _section = s), + userDisplayName: _displayName, + userEmail: _user?.email, + onProfileTap: () => _soon('Profil'), + onSearchAmTap: () => _soon('Recherche AM'), + onSettingsTap: () => _soon('Paramètres'), + leftColumn: const QuotidienColumnPlaceholder( + title: 'Cartes', + subtitle: + 'Couple enfant–nounou et flux de cartes\n(à brancher — tickets #167 / #173).', + icon: Icons.style_outlined, ), - ), - ); - } - - 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, - ), - ], - ), - ), - ], + centerColumn: const QuotidienColumnPlaceholder( + title: 'Blog', + subtitle: + 'Fil du quotidien (affichage par défaut)\n(à brancher — ticket #178).', + icon: Icons.auto_stories_outlined, + ), + rightColumn: const QuotidienColumnPlaceholder( + title: 'Messagerie', + subtitle: + 'Mess. AM · Mess. RPE\n(à brancher — ticket #184).', + icon: Icons.chat_bubble_outline, + ), + agendaBody: const QuotidienStubPage( + title: 'Agenda', + message: 'Agenda — contenu à venir (stub #187).', + ), + contratBody: const QuotidienStubPage( + title: 'Contrat', + message: 'Contrat — contenu à venir (stub #187).', ), ); } -} \ No newline at end of file +} diff --git a/frontend/lib/widgets/app_footer.dart b/frontend/lib/widgets/app_footer.dart index b87f82b..ab38f72 100644 --- a/frontend/lib/widgets/app_footer.dart +++ b/frontend/lib/widgets/app_footer.dart @@ -5,7 +5,11 @@ 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); + /// Ligne grise droite au-dessus du footer. À désactiver quand l'écran + /// fournit déjà son propre séparateur (ex. trait crayon du quotidien). + final bool showTopBorder; + + const AppFooter({Key? key, this.showTopBorder = true}) : super(key: key); @override Widget build(BuildContext context) { @@ -13,9 +17,9 @@ class AppFooter extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), decoration: BoxDecoration( // color: Colors.white, - border: Border( - top: BorderSide(color: Colors.grey.shade300), - ), + border: showTopBorder + ? Border(top: BorderSide(color: Colors.grey.shade300)) + : null, ), child: LayoutBuilder( builder: (context, constraints) { diff --git a/frontend/lib/widgets/image_button.dart b/frontend/lib/widgets/image_button.dart index 9d63bad..d206eae 100644 --- a/frontend/lib/widgets/image_button.dart +++ b/frontend/lib/widgets/image_button.dart @@ -10,6 +10,14 @@ class ImageButton extends StatelessWidget { final VoidCallback onPressed; final double fontSize; // Ajout pour la flexibilité + /// Forme utilisée pour le focus / hover / splash. Les fonds « dessinés » + /// sont des pastilles : le stadium suit leur contour au lieu d'un rectangle. + final OutlinedBorder shape; + + /// Opacité du fond seul (le texte reste net) : permet un état « inactif » + /// plus clair sans changer d'asset. + final double bgOpacity; + const ImageButton({ super.key, required this.bg, @@ -19,6 +27,8 @@ class ImageButton extends StatelessWidget { required this.textColor, required this.onPressed, this.fontSize = 16, // Valeur par défaut + this.shape = const StadiumBorder(), + this.bgOpacity = 1.0, }); @override @@ -36,14 +46,16 @@ class ImageButton extends StatelessWidget { style: TextButton.styleFrom( padding: EdgeInsets.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: - const RoundedRectangleBorder(borderRadius: BorderRadius.zero), + shape: shape, ), child: Ink( + // Pas de découpe du PNG : le trait « dessiné » déborde un peu du + // stadium, on ne rogne que le focus / hover / splash. decoration: BoxDecoration( image: DecorationImage( image: AssetImage(bg), fit: BoxFit.fill, + opacity: bgOpacity, ), ), child: Center( diff --git a/frontend/lib/widgets/quotidien/quotidien_bandeau.dart b/frontend/lib/widgets/quotidien/quotidien_bandeau.dart new file mode 100644 index 0000000..16e0399 --- /dev/null +++ b/frontend/lib/widgets/quotidien/quotidien_bandeau.dart @@ -0,0 +1,282 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:p_tits_pas/services/auth_service.dart'; +import 'package:p_tits_pas/widgets/image_button.dart'; +import 'package:p_tits_pas/widgets/quotidien/quotidien_theme.dart'; + +/// Bandeau quotidien pastel : logo · pastilles Cahier de liaison / Agenda / +/// Contrat · menu user. +/// Réutilisable parent (#166) et AM (#169). +class QuotidienBandeau extends StatelessWidget { + final QuotidienNavSection selectedSection; + final ValueChanged onSectionSelected; + final String userDisplayName; + final String? userEmail; + final VoidCallback? onProfileTap; + final VoidCallback? onSearchAmTap; + final VoidCallback? onSettingsTap; + final VoidCallback? onLogout; + + const QuotidienBandeau({ + super.key, + required this.selectedSection, + required this.onSectionSelected, + required this.userDisplayName, + this.userEmail, + this.onProfileTap, + this.onSearchAmTap, + this.onSettingsTap, + this.onLogout, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + child: Row( + children: [ + Image.asset( + QuotidienTheme.logoAsset, + height: 44, + fit: BoxFit.contain, + ), + const Spacer(), + _NavPills( + selected: selectedSection, + onSelected: onSectionSelected, + ), + const Spacer(), + _UserMenu( + displayName: userDisplayName, + email: userEmail, + onProfileTap: onProfileTap, + onSearchAmTap: onSearchAmTap, + onSettingsTap: onSettingsTap, + onLogout: onLogout, + ), + ], + ), + ); + } +} + +class _NavPills extends StatelessWidget { + final QuotidienNavSection selected; + final ValueChanged onSelected; + + const _NavPills({ + required this.selected, + required this.onSelected, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + _pill( + label: 'Cahier de liaison', + section: QuotidienNavSection.liaison, + ), + const SizedBox(width: 10), + _pill( + label: 'Agenda', + section: QuotidienNavSection.agenda, + ), + const SizedBox(width: 10), + _pill( + label: 'Contrat', + section: QuotidienNavSection.contrat, + ), + ], + ); + } + + Widget _pill({ + required String label, + required QuotidienNavSection section, + }) { + final active = selected == section; + // Même famille que l’inscription : fonds « dessinés » (pas Material). + // Les deux pastilles ont la même silhouette (ratio ~4:1) : la largeur + // est dérivée de la hauteur pour ne jamais déformer le PNG. + // Chaque section garde sa couleur ; l'inactive est juste plus claire. + return ImageButton( + bg: QuotidienTheme.pillAssetFor(section), + bgOpacity: active ? 1.0 : QuotidienTheme.pillInactiveOpacity, + width: _pillHeight * QuotidienTheme.pillAspectRatio, + height: _pillHeight, + text: label, + textColor: QuotidienTheme.ink, + fontSize: 14, + onPressed: () => onSelected(section), + ); + } + + static const double _pillHeight = 46; +} + +class _UserMenu extends StatelessWidget { + final String displayName; + final String? email; + final VoidCallback? onProfileTap; + final VoidCallback? onSearchAmTap; + final VoidCallback? onSettingsTap; + final VoidCallback? onLogout; + + const _UserMenu({ + required this.displayName, + this.email, + this.onProfileTap, + this.onSearchAmTap, + this.onSettingsTap, + this.onLogout, + }); + + @override + Widget build(BuildContext context) { + final shortName = displayName.trim().isEmpty ? 'Compte' : displayName.trim(); + const double pillHeight = 46; + const double pillWidth = pillHeight * QuotidienTheme.pillAspectRatio; + return PopupMenuButton( + tooltip: 'Menu utilisateur', + offset: const Offset(0, pillHeight + 4), + color: QuotidienTheme.ivory, + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: QuotidienTheme.lavender.withOpacity(0.6)), + ), + onSelected: (value) async { + switch (value) { + case 'profile': + onProfileTap?.call(); + break; + case 'search_am': + onSearchAmTap?.call(); + break; + case 'settings': + onSettingsTap?.call(); + break; + case 'logout': + await _confirmLogout(context); + break; + } + }, + itemBuilder: (context) => [ + if (email != null && email!.trim().isNotEmpty) + PopupMenuItem( + enabled: false, + child: Text( + email!, + style: TextStyle(color: QuotidienTheme.muted, fontSize: 12), + ), + ), + const PopupMenuDivider(), + const PopupMenuItem( + value: 'profile', + child: ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + leading: Icon(Icons.person_outline, size: 20), + title: Text('Profil'), + ), + ), + const PopupMenuItem( + value: 'search_am', + child: ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + leading: Icon(Icons.search, size: 20), + title: Text('Recherche AM'), + ), + ), + const PopupMenuItem( + value: 'settings', + child: ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + leading: Icon(Icons.settings_outlined, size: 20), + title: Text('Paramètres'), + ), + ), + const PopupMenuDivider(), + const PopupMenuItem( + value: 'logout', + child: ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + leading: Icon(Icons.logout, size: 20), + title: Text('Déconnexion'), + ), + ), + ], + // Même pastille « dessinée » que la nav, teinte violet pastel charte. + child: Container( + width: pillWidth, + height: pillHeight, + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage(QuotidienTheme.pillLavenderAsset), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + const Icon( + Icons.person_outline, + size: 20, + color: QuotidienTheme.ink, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + shortName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: GoogleFonts.merienda( + fontSize: 13, + fontWeight: FontWeight.bold, + color: QuotidienTheme.ink, + ), + ), + ), + const Icon( + Icons.keyboard_arrow_down, + size: 18, + color: QuotidienTheme.ink, + ), + ], + ), + ), + ); + } + + Future _confirmLogout(BuildContext context) async { + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Déconnexion'), + content: const Text('Voulez-vous vraiment vous déconnecter ?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: const Text('Annuler'), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(true), + child: const Text('Déconnexion'), + ), + ], + ), + ); + if (ok != true) return; + onLogout?.call(); + await AuthService.logout(); + if (context.mounted) { + context.go('/login'); + } + } +} diff --git a/frontend/lib/widgets/quotidien/quotidien_shell.dart b/frontend/lib/widgets/quotidien/quotidien_shell.dart new file mode 100644 index 0000000..05345da --- /dev/null +++ b/frontend/lib/widgets/quotidien/quotidien_shell.dart @@ -0,0 +1,320 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:p_tits_pas/widgets/app_footer.dart'; +import 'package:p_tits_pas/widgets/quotidien/quotidien_bandeau.dart'; +import 'package:p_tits_pas/widgets/quotidien/quotidien_theme.dart'; + +/// Coquille « Cahier de liaison » quotidien 3 colonnes + bandeau (#166). +/// L’AM (#169) réutilise ce widget en injectant ses slots. +class QuotidienShell extends StatelessWidget { + final QuotidienNavSection selectedSection; + final ValueChanged onSectionSelected; + final String userDisplayName; + final String? userEmail; + final VoidCallback? onProfileTap; + final VoidCallback? onSearchAmTap; + final VoidCallback? onSettingsTap; + final VoidCallback? onLogout; + + /// Colonne gauche (couple + cartes + actions). + final Widget leftColumn; + + /// Colonne milieu (blog). + final Widget centerColumn; + + /// Colonne droite (messagerie). + final Widget rightColumn; + + /// Corps affiché hors Cahier de liaison (Agenda / Contrat stubs). + final Widget? agendaBody; + final Widget? contratBody; + + const QuotidienShell({ + super.key, + required this.selectedSection, + required this.onSectionSelected, + required this.userDisplayName, + required this.leftColumn, + required this.centerColumn, + required this.rightColumn, + this.userEmail, + this.onProfileTap, + this.onSearchAmTap, + this.onSettingsTap, + this.onLogout, + this.agendaBody, + this.contratBody, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: QuotidienTheme.ivory, + body: Container( + decoration: QuotidienTheme.paperBackground(), + child: Column( + children: [ + QuotidienBandeau( + selectedSection: selectedSection, + onSectionSelected: onSectionSelected, + userDisplayName: userDisplayName, + userEmail: userEmail, + onProfileTap: onProfileTap, + onSearchAmTap: onSearchAmTap, + onSettingsTap: onSettingsTap, + onLogout: onLogout, + ), + const QuotidienPencilDivider(), + Expanded(child: _bodyForSection(context)), + const QuotidienPencilDivider(), + const AppFooter(showTopBorder: false), + ], + ), + ), + ); + } + + Widget _bodyForSection(BuildContext context) { + switch (selectedSection) { + case QuotidienNavSection.liaison: + return _ThreeColumns( + left: leftColumn, + center: centerColumn, + right: rightColumn, + ); + case QuotidienNavSection.agenda: + return agendaBody ?? + const QuotidienStubPage( + title: 'Agenda', + message: 'Page Agenda — à venir.', + ); + case QuotidienNavSection.contrat: + return contratBody ?? + const QuotidienStubPage( + title: 'Contrat', + message: 'Page Contrat — à venir.', + ); + } + } +} + +/// Trait « crayon gris » dessiné à la main : sépare bandeau / corps / footer +/// (horizontal) et les 3 colonnes (vertical). Le PNG (2400×28) est étiré dans +/// le sens du trait seulement : sur un trait, c'est invisible. +class QuotidienPencilDivider extends StatelessWidget { + final Axis axis; + + /// Épaisseur de la zone du trait (hauteur si horizontal, largeur sinon). + final double thickness; + final EdgeInsets padding; + + const QuotidienPencilDivider({ + super.key, + this.axis = Axis.horizontal, + this.thickness = 14, + this.padding = const EdgeInsets.symmetric(horizontal: 24), + }); + + const QuotidienPencilDivider.vertical({ + super.key, + this.thickness = 14, + this.padding = const EdgeInsets.symmetric(vertical: 16), + }) : axis = Axis.vertical; + + @override + Widget build(BuildContext context) { + final horizontal = axis == Axis.horizontal; + return Padding( + padding: padding, + child: SizedBox( + width: horizontal ? double.infinity : thickness, + height: horizontal ? thickness : double.infinity, + child: Image.asset( + horizontal + ? QuotidienTheme.pencilLineAsset + : QuotidienTheme.pencilLineVerticalAsset, + fit: BoxFit.fill, + filterQuality: FilterQuality.medium, + ), + ), + ); + } +} + +class _ThreeColumns extends StatelessWidget { + final Widget left; + final Widget center; + final Widget right; + + const _ThreeColumns({ + required this.left, + required this.center, + required this.right, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final wide = constraints.maxWidth >= 900; + if (!wide) { + // Socle mobile temporaire (#188 = swipe dédié) : pile verticale. + return ListView( + padding: const EdgeInsets.fromLTRB(12, 4, 12, 12), + children: [ + _ColumnPanel(child: left), + const QuotidienPencilDivider( + padding: EdgeInsets.symmetric(horizontal: 32, vertical: 4), + ), + _ColumnPanel(child: center), + const QuotidienPencilDivider( + padding: EdgeInsets.symmetric(horizontal: 32, vertical: 4), + ), + _ColumnPanel(child: right), + ], + ); + } + return Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded(child: _ColumnPanel(child: left)), + const QuotidienPencilDivider.vertical(), + Expanded(child: _ColumnPanel(child: center)), + const QuotidienPencilDivider.vertical(), + Expanded(child: _ColumnPanel(child: right)), + ], + ), + ); + }, + ); + } +} + +/// Panneau colonne (carte pastel semi-transparente). +class _ColumnPanel extends StatelessWidget { + final Widget child; + + const _ColumnPanel({required this.child}); + + @override + Widget build(BuildContext context) { + return Container( + // Pas de bordure Material : la séparation est assurée par les traits + // crayon, on garde juste un léger fond. + decoration: BoxDecoration( + color: QuotidienTheme.columnCardFill.withOpacity(0.82), + borderRadius: BorderRadius.circular(16), + ), + clipBehavior: Clip.antiAlias, + child: child, + ); + } +} + +/// Placeholder de colonne métier (branchable par tickets C/D/E). +class QuotidienColumnPlaceholder extends StatelessWidget { + final String title; + final String subtitle; + final IconData icon; + + const QuotidienColumnPlaceholder({ + super.key, + required this.title, + required this.subtitle, + required this.icon, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Icon(icon, color: QuotidienTheme.lavender, size: 22), + const SizedBox(width: 8), + Text( + title, + style: GoogleFonts.merienda( + fontSize: 18, + fontWeight: FontWeight.w600, + color: QuotidienTheme.ink, + ), + ), + ], + ), + const SizedBox(height: 12), + Expanded( + child: Center( + child: Text( + subtitle, + textAlign: TextAlign.center, + style: GoogleFonts.merriweather( + fontSize: 13, + color: QuotidienTheme.muted, + height: 1.4, + ), + ), + ), + ), + ], + ), + ); + } +} + +/// Stub pleine page (Agenda / Contrat) — contenu riche hors #166. +class QuotidienStubPage extends StatelessWidget { + final String title; + final String message; + + const QuotidienStubPage({ + super.key, + required this.title, + required this.message, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Container( + margin: const EdgeInsets.all(24), + padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 32), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.85), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade300), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title, + style: GoogleFonts.merienda( + fontSize: 22, + fontWeight: FontWeight.w600, + color: QuotidienTheme.ink, + ), + ), + const SizedBox(height: 12), + Text( + message, + textAlign: TextAlign.center, + style: GoogleFonts.merriweather( + fontSize: 14, + color: QuotidienTheme.muted, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/lib/widgets/quotidien/quotidien_theme.dart b/frontend/lib/widgets/quotidien/quotidien_theme.dart new file mode 100644 index 0000000..2eb3a5f --- /dev/null +++ b/frontend/lib/widgets/quotidien/quotidien_theme.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +/// Couleurs / tokens du quotidien parent–AM (#166) — lignée papier / pastel. +/// Pas le violet Material du dashboard staff. +abstract final class QuotidienTheme { + static const Color ink = Color(0xFF2F2F2F); + static const Color ivory = Color(0xFFFFFEF9); + static const Color turquoise = Color(0xFF8AD0C8); + static const Color lavender = Color(0xFFC6A3D8); + static const Color coral = Color(0xFFF4A28C); + static const Color softGreenPill = Color(0xFFB8D9A8); + static const Color columnCardFill = Color(0xFFF7F3EA); + static const Color muted = Color(0xFF6B6B6B); + + static const String paperAsset = 'assets/images/paper2.png'; + static const String logoAsset = 'assets/images/logo.png'; + + /// Pastilles « dessinées » du bandeau : même silhouette (1190×299), une + /// couleur charte par section ; l'inactive est la même, plus transparente. + static const double pillInactiveOpacity = 0.45; + static const String pillIvoryAsset = 'assets/images/bg_ivoire_pill.png'; + static const String pillYellowAsset = 'assets/images/bg_yellow_pill.png'; + static const String pillPeachAsset = 'assets/images/bg_peach_pill.png'; + static const String pillTurquoiseAsset = + 'assets/images/bg_turquoise_pill.png'; + static const String pillLavenderAsset = + 'assets/images/bg_lavender_pill.png'; + + /// Trait crayon gris (2400×28, transparent) : séparateurs bandeau / corps / + /// footer. Version verticale (28×2400) entre les colonnes. + static const String pencilLineAsset = 'assets/images/pencil_line_grey.png'; + static const String pencilLineVerticalAsset = + 'assets/images/pencil_line_grey_v.png'; + + static String pillAssetFor(QuotidienNavSection section) { + switch (section) { + case QuotidienNavSection.liaison: + return pillYellowAsset; + case QuotidienNavSection.agenda: + return pillPeachAsset; + case QuotidienNavSection.contrat: + return pillTurquoiseAsset; + } + } + + /// Largeur / hauteur des PNG de pastille, à respecter pour ne pas déformer. + static const double pillAspectRatio = 1190 / 298; + + static BoxDecoration paperBackground() { + return const BoxDecoration( + image: DecorationImage( + image: AssetImage(paperAsset), + fit: BoxFit.cover, + repeat: ImageRepeat.repeat, + ), + ); + } + +} + +/// Sections du bandeau quotidien (Cahier de liaison / Agenda / Contrat). +/// `liaison` = le hub « Cahier de liaison » (cartes · blog · messagerie). +enum QuotidienNavSection { + liaison, + agenda, + contrat, +}