diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index e202d52..673b26f 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,12 +1,27 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'screens/auth/login_screen.dart'; +import 'screens/legal/legal_page.dart'; +import 'screens/legal/privacy_page.dart'; void main() => runApp(const PtiPasApp()); -final _router = GoRouter(routes: [ - GoRoute(path: '/', builder: (_, __) => const LoginPage()), -]); +final _router = GoRouter( + routes: [ + GoRoute( + path: '/', + builder: (_, __) => const LoginPage(), + ), + GoRoute( + path: '/legal', + builder: (_, __) => const LegalPage(), + ), + GoRoute( + path: '/privacy', + builder: (_, __) => const PrivacyPage(), + ), + ], +); class PtiPasApp extends StatelessWidget { const PtiPasApp({super.key}); @@ -17,6 +32,13 @@ class PtiPasApp extends StatelessWidget { title: 'P\'titsPas', routerConfig: _router, debugShowCheckedModeBanner: false, + theme: ThemeData( + fontFamily: 'Merienda', + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(0xFF8AD0C8), + brightness: Brightness.light, + ), + ), ); } } \ No newline at end of file diff --git a/frontend/lib/screens/auth/login_screen.dart b/frontend/lib/screens/auth/login_screen.dart index 20db6a9..f3d944a 100644 --- a/frontend/lib/screens/auth/login_screen.dart +++ b/frontend/lib/screens/auth/login_screen.dart @@ -2,10 +2,49 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:url_launcher/url_launcher.dart'; +import 'package:p_tits_pas/services/bug_report_service.dart'; +import 'package:go_router/go_router.dart'; -class LoginPage extends StatelessWidget { +class LoginPage extends StatefulWidget { const LoginPage({super.key}); + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + String? _validateEmail(String? value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer votre email'; + } + if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { + return 'Veuillez entrer un email valide'; + } + return null; + } + + String? _validatePassword(String? value) { + if (value == null || value.isEmpty) { + return 'Veuillez entrer votre mot de passe'; + } + if (value.length < 6) { + return 'Le mot de passe doit contenir au moins 6 caractères'; + } + return null; + } + @override Widget build(BuildContext context) { return Scaffold( @@ -55,33 +94,23 @@ class LoginPage extends StatelessWidget { Positioned( right: 0, bottom: 0, - width: w * 0.5, // Moitié droite de l'écran - height: h * 0.5, // Moitié basse de l'écran + width: w * 0.6, // 60% de la largeur de l'écran + height: h * 0.5, // 50% de la hauteur de l'écran child: Padding( padding: EdgeInsets.all(w * 0.02), // 2% de padding - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Labels au-dessus des champs - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - 'Email', - style: GoogleFonts.merienda( - fontSize: 20, - color: Colors.black87, - fontWeight: FontWeight.w600, - ), - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 20), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Labels au-dessus des champs + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( child: Text( - 'Mot de passe', + 'Email', style: GoogleFonts.merienda( fontSize: 20, color: Colors.black87, @@ -89,48 +118,157 @@ class LoginPage extends StatelessWidget { ), ), ), - ), - ], - ), - const SizedBox(height: 10), - // Champs côte à côte - Row( - children: [ - Expanded( - child: _ImageTextField( - bg: 'assets/images/field_email.png', - width: 400, - height: 80, - hint: 'Email', + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 20), + child: Text( + 'Mot de passe', + style: GoogleFonts.merienda( + fontSize: 20, + color: Colors.black87, + fontWeight: FontWeight.w600, + ), + ), + ), ), - ), - const SizedBox(width: 20), - Expanded( - child: _ImageTextField( - bg: 'assets/images/field_password.png', - width: 400, - height: 80, - hint: 'Mot de passe', - obscure: true, - ), - ), - ], - ), - const Spacer(), - // Bouton centré - Center( - child: _ImageButton( - bg: 'assets/images/btn_green.png', - width: 300, - height: 60, - text: 'Se connecter', - textColor: const Color(0xFF8AD0C8), // Vert harmonieux - onPressed: () { - // TODO: Implémenter la logique de connexion - }, + ], ), + const SizedBox(height: 10), + // Champs côte à côte + Row( + children: [ + Expanded( + child: _ImageTextField( + bg: 'assets/images/field_email.png', + width: 400, + height: 53, + hint: 'Email', + controller: _emailController, + validator: _validateEmail, + ), + ), + const SizedBox(width: 20), + Expanded( + child: _ImageTextField( + bg: 'assets/images/field_password.png', + width: 400, + height: 53, + hint: 'Mot de passe', + obscure: true, + controller: _passwordController, + validator: _validatePassword, + ), + ), + ], + ), + const SizedBox(height: 20), // Réduit l'espacement + // Bouton centré + Center( + child: _ImageButton( + bg: 'assets/images/btn_green.png', + width: 300, + height: 40, + text: 'Se connecter', + textColor: const Color(0xFF2D6A4F), + onPressed: () { + if (_formKey.currentState?.validate() ?? false) { + // TODO: Implémenter la logique de connexion + } + }, + ), + ), + const SizedBox(height: 10), + // Lien mot de passe oublié + Center( + child: TextButton( + onPressed: () { + // TODO: Implémenter la logique de récupération de mot de passe + }, + child: Text( + 'Mot de passe oublié ?', + style: GoogleFonts.merienda( + fontSize: 14, + color: const Color(0xFF2D6A4F), + decoration: TextDecoration.underline, + ), + ), + ), + ), + const SizedBox(height: 10), + // Lien de création de compte + Center( + child: TextButton( + onPressed: () { + Navigator.pushNamed(context, '/parent-register'); + }, + child: Text( + 'Créer un compte', + style: GoogleFonts.merienda( + fontSize: 16, + color: const Color(0xFF2D6A4F), + decoration: TextDecoration.underline, + ), + ), + ), + ), + const SizedBox(height: 20), // Réduit l'espacement en bas + ], + ), + ), + ), + ), + // Pied de page + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + decoration: BoxDecoration( + color: Colors.transparent, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _FooterLink( + text: 'Contact support', + onTap: () async { + final Uri emailLaunchUri = Uri( + scheme: 'mailto', + path: 'support@supernounou.local', + ); + if (await canLaunchUrl(emailLaunchUri)) { + await launchUrl(emailLaunchUri); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Impossible d\'ouvrir le client mail', + style: GoogleFonts.merienda(), + ), + ), + ); + } + }, + ), + _FooterLink( + text: 'Signaler un bug', + onTap: () { + _showBugReportDialog(context); + }, + ), + _FooterLink( + text: 'Mentions légales', + onTap: () { + Navigator.pushNamed(context, '/legal'); + }, + ), + _FooterLink( + text: 'Politique de confidentialité', + onTap: () { + Navigator.pushNamed(context, '/privacy'); + }, ), - const SizedBox(height: 40), ], ), ), @@ -150,6 +288,89 @@ class LoginPage extends StatelessWidget { ); } + void _showBugReportDialog(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(), + ), + ), + ], + ), + ); + } + Future _getImageDimensions() async { final image = Image.asset('assets/images/river_logo_desktop.png'); final completer = Completer(); @@ -181,12 +402,17 @@ class _ImageTextField extends StatelessWidget { final double height; final String hint; final bool obscure; + final TextEditingController? controller; + final String? Function(String?)? validator; + const _ImageTextField({ required this.bg, required this.width, required this.height, required this.hint, this.obscure = false, + this.controller, + this.validator, }); @override @@ -200,24 +426,30 @@ class _ImageTextField extends StatelessWidget { fit: BoxFit.fill, ), ), - child: TextField( + child: TextFormField( + controller: controller, obscureText: obscure, textAlign: TextAlign.left, style: GoogleFonts.merienda( - fontSize: height * 0.25, // Réduction de la taille de la police + fontSize: height * 0.25, color: Colors.black87, ), + validator: validator, decoration: InputDecoration( border: InputBorder.none, hintText: hint, hintStyle: GoogleFonts.merienda( - fontSize: height * 0.25, // Même taille pour le placeholder + fontSize: height * 0.25, color: Colors.black38, ), contentPadding: EdgeInsets.symmetric( horizontal: width * 0.1, vertical: height * 0.3, ), + errorStyle: GoogleFonts.merienda( + fontSize: height * 0.2, + color: Colors.red, + ), ), ), ); @@ -255,14 +487,50 @@ class _ImageButton extends StatelessWidget { fit: BoxFit.fill, ), ), - child: TextButton( - onPressed: onPressed, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onPressed, + child: Center( + child: Text( + text, + style: GoogleFonts.merienda( + fontSize: height * 0.4, + color: textColor, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ); + } +} + +// ─────────────────────────────────────────────────────────────── +// Lien du pied de page +// ─────────────────────────────────────────────────────────────── +class _FooterLink extends StatelessWidget { + final String text; + final VoidCallback onTap; + + const _FooterLink({ + required this.text, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( text, style: GoogleFonts.merienda( - fontSize: height * 0.3, - color: textColor, - fontWeight: FontWeight.w600, + fontSize: 14, + color: Colors.black87, + decoration: TextDecoration.underline, ), ), ), diff --git a/frontend/lib/screens/legal/legal_page.dart b/frontend/lib/screens/legal/legal_page.dart new file mode 100644 index 0000000..fd3b5a6 --- /dev/null +++ b/frontend/lib/screens/legal/legal_page.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class LegalPage extends StatelessWidget { + const LegalPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + 'Mentions légales', + style: GoogleFonts.merienda(), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Éditeur', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + 'P\'titsPas est une application développée pour les collectivités locales.', + style: GoogleFonts.merienda(), + ), + const SizedBox(height: 32), + Text( + 'Hébergeur', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + 'Les données sont hébergées sur des serveurs sécurisés en France.', + style: GoogleFonts.merienda(), + ), + const SizedBox(height: 32), + Text( + 'Responsable du traitement', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + 'Le responsable du traitement des données est la collectivité locale utilisatrice de l\'application.', + style: GoogleFonts.merienda(), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/screens/legal/privacy_page.dart b/frontend/lib/screens/legal/privacy_page.dart new file mode 100644 index 0000000..377b91a --- /dev/null +++ b/frontend/lib/screens/legal/privacy_page.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class PrivacyPage extends StatelessWidget { + const PrivacyPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + 'Politique de confidentialité', + style: GoogleFonts.merienda(), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Protection des données personnelles', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + 'P\'titsPas s\'engage à protéger vos données personnelles conformément au RGPD.', + style: GoogleFonts.merienda(), + ), + const SizedBox(height: 32), + Text( + 'Données collectées', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + 'Les données collectées sont nécessaires au bon fonctionnement de l\'application et à la gestion des contrats de garde d\'enfants.', + style: GoogleFonts.merienda(), + ), + const SizedBox(height: 32), + Text( + 'Vos droits', + style: GoogleFonts.merienda( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + 'Vous disposez d\'un droit d\'accès, de rectification, d\'effacement et de portabilité de vos données.', + style: GoogleFonts.merienda(), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/frontend/lib/services/bug_report_service.dart b/frontend/lib/services/bug_report_service.dart new file mode 100644 index 0000000..7dc9af4 --- /dev/null +++ b/frontend/lib/services/bug_report_service.dart @@ -0,0 +1,28 @@ +import 'package:http/http.dart' as http; +import 'dart:convert'; + +class BugReportService { + static const String _apiUrl = 'https://api.supernounou.local/bug-reports'; + + static Future sendReport(String description) async { + try { + final response = await http.post( + Uri.parse(_apiUrl), + headers: { + 'Content-Type': 'application/json', + }, + body: jsonEncode({ + 'description': description, + 'timestamp': DateTime.now().toIso8601String(), + 'platform': 'web', // TODO: Ajouter la détection de la plateforme + }), + ); + + if (response.statusCode != 200) { + throw Exception('Erreur lors de l\'envoi du rapport'); + } + } catch (e) { + rethrow; + } + } +} \ No newline at end of file diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 1df4fe8..c700298 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -161,7 +161,7 @@ packages: source: hosted version: "6.2.1" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f @@ -525,6 +525,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" vector_math: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 6b49c14..ff10e21 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -15,6 +15,8 @@ dependencies: shared_preferences: ^2.2.2 image_picker: ^1.0.7 js: ^0.6.7 + url_launcher: ^6.2.4 + http: ^1.2.0 dev_dependencies: flutter_test: diff --git a/frontend/windows/flutter/generated_plugin_registrant.cc b/frontend/windows/flutter/generated_plugin_registrant.cc index 77ab7a0..043a96f 100644 --- a/frontend/windows/flutter/generated_plugin_registrant.cc +++ b/frontend/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/frontend/windows/flutter/generated_plugins.cmake b/frontend/windows/flutter/generated_plugins.cmake index a423a02..a95e267 100644 --- a/frontend/windows/flutter/generated_plugins.cmake +++ b/frontend/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST