From 02754f35061243f2e4a51bdafa326aef77ae79eb Mon Sep 17 00:00:00 2001 From: Van Leemput Dayron Date: Sat, 4 Oct 2025 17:42:21 +0200 Subject: [PATCH] Refactor signup fields and update home page titles; add trip service and model --- lib/components/create_trip_content.dart | 448 ++++++++++++++++++++++++ lib/components/home/home_content.dart | 202 ++++------- lib/models/trip.dart | 181 ++++++++++ lib/pages/home.dart | 11 +- lib/pages/signup.dart | 30 +- lib/services/trip_service.dart | 108 ++++++ 6 files changed, 830 insertions(+), 150 deletions(-) create mode 100644 lib/models/trip.dart create mode 100644 lib/services/trip_service.dart diff --git a/lib/components/create_trip_content.dart b/lib/components/create_trip_content.dart index e69de29..d29ae11 100644 --- a/lib/components/create_trip_content.dart +++ b/lib/components/create_trip_content.dart @@ -0,0 +1,448 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../providers/user_provider.dart'; + +class CreateTripContent extends StatefulWidget { + const CreateTripContent({super.key}); + + @override + State createState() => _CreateTripContentState(); +} + +class _CreateTripContentState extends State { + final _formKey = GlobalKey(); + final _titleController = TextEditingController(); + final _descriptionController = TextEditingController(); + final _locationController = TextEditingController(); + final _budgetController = TextEditingController(); + + DateTime? _startDate; + DateTime? _endDate; + bool _isLoading = false; + + // Liste des participants (emails) + final List _participants = []; + final _participantController = TextEditingController(); + + @override + void dispose() { + _titleController.dispose(); + _descriptionController.dispose(); + _locationController.dispose(); + _budgetController.dispose(); + _participantController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Créer un voyage'), + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, + actions: [ + TextButton( + onPressed: _isLoading ? null : _saveTrip, + child: Text( + 'Sauvegarder', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Titre du voyage + _buildSectionTitle('Informations générales'), + SizedBox(height: 16), + + TextFormField( + controller: _titleController, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Titre requis'; + } + return null; + }, + decoration: InputDecoration( + labelText: 'Titre du voyage *', + hintText: 'ex: Voyage à Paris', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: Icon(Icons.travel_explore), + ), + ), + + SizedBox(height: 16), + + // Description + TextFormField( + controller: _descriptionController, + maxLines: 3, + decoration: InputDecoration( + labelText: 'Description', + hintText: 'Décrivez votre voyage...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: Icon(Icons.description), + ), + ), + + SizedBox(height: 16), + + // Destination + TextFormField( + controller: _locationController, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Destination requise'; + } + return null; + }, + decoration: InputDecoration( + labelText: 'Destination *', + hintText: 'ex: Paris, France', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: Icon(Icons.location_on), + ), + ), + + SizedBox(height: 24), + + // Dates + _buildSectionTitle('Dates du voyage'), + SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: _buildDateField( + label: 'Date de début *', + date: _startDate, + onTap: () => _selectStartDate(context), + ), + ), + SizedBox(width: 16), + Expanded( + child: _buildDateField( + label: 'Date de fin *', + date: _endDate, + onTap: () => _selectEndDate(context), + ), + ), + ], + ), + + SizedBox(height: 24), + + // Budget + _buildSectionTitle('Budget'), + SizedBox(height: 16), + + TextFormField( + controller: _budgetController, + keyboardType: TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + labelText: 'Budget estimé', + hintText: 'ex: 1200.50', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: Icon(Icons.euro), + suffixText: '€', + ), + ), + + SizedBox(height: 24), + + // Participants + _buildSectionTitle('Participants'), + SizedBox(height: 8), + Text( + 'Ajoutez les emails des personnes que vous souhaitez inviter', + style: TextStyle(color: Colors.grey[600], fontSize: 14), + ), + SizedBox(height: 16), + + // Champ d'ajout de participant + Row( + children: [ + Expanded( + child: TextFormField( + controller: _participantController, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + labelText: 'Email du participant', + hintText: 'ex: ami@email.com', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: Icon(Icons.person_add), + ), + ), + ), + SizedBox(width: 8), + ElevatedButton( + onPressed: _addParticipant, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: EdgeInsets.all(16), + ), + child: Icon(Icons.add), + ), + ], + ), + + SizedBox(height: 16), + + // Liste des participants ajoutés + if (_participants.isNotEmpty) ...[ + Text( + 'Participants ajoutés (${_participants.length})', + style: TextStyle(fontWeight: FontWeight.w500), + ), + SizedBox(height: 8), + Container( + width: double.infinity, + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[300]!), + borderRadius: BorderRadius.circular(12), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: _participants + .map( + (email) => Chip( + label: Text(email, style: TextStyle(fontSize: 12)), + deleteIcon: Icon(Icons.close, size: 18), + onDeleted: () => _removeParticipant(email), + backgroundColor: Theme.of( + context, + ).colorScheme.primary.withAlpha(25), + ), + ) + .toList(), + ), + ), + ], + + SizedBox(height: 32), + + // Bouton de création + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: _isLoading ? null : _saveTrip, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: _isLoading + ? CircularProgressIndicator(color: Colors.white) + : Text( + 'Créer le voyage', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + + SizedBox(height: 20), + ], + ), + ), + ), + ); + } + + Widget _buildSectionTitle(String title) { + return Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.grey[700], + ), + ); + } + + Widget _buildDateField({ + required String label, + required DateTime? date, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + child: Container( + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[400]!), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + SizedBox(height: 8), + Row( + children: [ + Icon(Icons.calendar_today, size: 16, color: Colors.grey[600]), + SizedBox(width: 8), + Text( + date != null + ? '${date.day}/${date.month}/${date.year}' + : 'Sélectionner', + style: TextStyle( + fontSize: 16, + color: date != null ? Colors.black : Colors.grey[500], + ), + ), + ], + ), + ], + ), + ), + ); + } + + Future _selectStartDate(BuildContext context) async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _startDate ?? DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(Duration(days: 365 * 2)), + ); + if (picked != null) { + setState(() { + _startDate = picked; + if (_endDate != null && _endDate!.isBefore(picked)) { + _endDate = null; + } + }); + } + } + + Future _selectEndDate(BuildContext context) async { + if (_startDate == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Veuillez d\'abord sélectionner la date de début'), + ), + ); + return; + } + + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _endDate ?? _startDate!.add(Duration(days: 1)), + firstDate: _startDate!, + lastDate: DateTime.now().add(Duration(days: 365 * 2)), + ); + if (picked != null) { + setState(() { + _endDate = picked; + }); + } + } + + void _addParticipant() { + final email = _participantController.text.trim(); + if (email.isEmpty) return; + + // Validation email simple + final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(email)) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Email invalide'))); + return; + } + + // Vérifier si l'email existe déjà + if (_participants.contains(email)) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Ce participant est déjà ajouté'))); + return; + } + + setState(() { + _participants.add(email); + _participantController.clear(); + }); + } + + void _removeParticipant(String email) { + setState(() { + _participants.remove(email); + }); + } + + Future _saveTrip() async { + if (!_formKey.currentState!.validate()) { + return; + } + + if (_startDate == null || _endDate == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Veuillez sélectionner les dates')), + ); + return; + } + + setState(() { + _isLoading = true; + }); + + try { + // TODO: Implémenter la sauvegarde du voyage + await Future.delayed(Duration(seconds: 2)); // Simulation + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Voyage créé avec succès !'), + backgroundColor: Colors.green, + ), + ); + + Navigator.pop(context); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erreur lors de la création: $e'), + backgroundColor: Colors.red, + ), + ); + } finally { + setState(() { + _isLoading = false; + }); + } + } +} diff --git a/lib/components/home/home_content.dart b/lib/components/home/home_content.dart index ab904eb..81c66d0 100644 --- a/lib/components/home/home_content.dart +++ b/lib/components/home/home_content.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:travel_mate/components/create_trip_content.dart'; import '../../providers/user_provider.dart'; class HomeContent extends StatelessWidget { @@ -7,73 +8,83 @@ class HomeContent extends StatelessWidget { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, userProvider, child) { - final user = userProvider.currentUser; + return Scaffold( + body: Consumer( + builder: (context, userProvider, child) { + final user = userProvider.currentUser; - return SingleChildScrollView( - // Ajout du scroll - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header de bienvenue - Text( - 'Bonjour ${user?.prenom ?? 'Voyageur'} !', - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), - ), - SizedBox(height: 8), - Text( - 'Vos voyages en cours', - style: TextStyle(fontSize: 16, color: Colors.grey[600]), - ), - SizedBox(height: 20), + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header de bienvenue + Text( + 'Bonjour ${user?.prenom ?? 'Voyageur'} !', + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), + ), + SizedBox(height: 8), + Text( + 'Vos voyages en cours', + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + ), + SizedBox(height: 20), - // Liste des groupes/voyages - _buildTravelCard( - context, - title: 'Voyage à Paris', - location: 'Paris, France', - dates: '15-20 Juin 2024', - participants: ['Alice', 'Bob', 'Charlie'], - budget: 1200.50, - imageUrl: - 'https://images.unsplash.com/photo-1502602898536-47ad22581b52', - color: Colors.blue, - ), + // Liste des groupes/voyages + _buildTravelCard( + context, + title: 'Voyage à Paris', + location: 'Paris, France', + dates: '15-20 Juin 2024', + participants: ['Alice', 'Bob', 'Charlie'], + budget: 1200.50, + imageUrl: + 'https://images.unsplash.com/photo-1502602898536-47ad22581b52', + color: Colors.blue, + ), - _buildTravelCard( - context, - title: 'Road Trip Espagne', - location: 'Madrid, Espagne', - dates: '10-18 Août 2024', - participants: ['Marie', 'Paul', 'Sophie', 'Thomas'], - budget: 850.75, - imageUrl: - 'https://images.unsplash.com/photo-1539037116277-4db20889f2d4', - color: Colors.orange, - ), + _buildTravelCard( + context, + title: 'Road Trip Espagne', + location: 'Madrid, Espagne', + dates: '10-18 Août 2024', + participants: ['Marie', 'Paul', 'Sophie', 'Thomas'], + budget: 850.75, + imageUrl: + 'https://images.unsplash.com/photo-1539037116277-4db20889f2d4', + color: Colors.orange, + ), - _buildTravelCard( - context, - title: 'Weekend à Amsterdam', - location: 'Amsterdam, Pays-Bas', - dates: '3-5 Septembre 2024', - participants: ['Emma', 'Lucas'], - budget: 450.25, - imageUrl: - 'https://images.unsplash.com/photo-1534351450181-ea9f78427fe8', - color: Colors.green, - ), + _buildTravelCard( + context, + title: 'Weekend à Amsterdam', + location: 'Amsterdam, Pays-Bas', + dates: '3-5 Septembre 2024', + participants: ['Emma', 'Lucas'], + budget: 450.25, + imageUrl: + 'https://images.unsplash.com/photo-1534351450181-ea9f78427fe8', + color: Colors.green, + ), - SizedBox(height: 20), + // Espacement en bas pour éviter que le FAB cache le contenu + SizedBox(height: 80), + ], + ), + ); + }, + ), - // Bouton pour créer un nouveau voyage - _buildCreateTravelCard(context), - ], - ), - ); - }, + // FloatingActionButton intégré directement dans HomeContent + floatingActionButton: FloatingActionButton( + onPressed: () { + _showCreateTravelDialog(context); + }, + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, + child: Icon(Icons.add), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ); } @@ -301,49 +312,6 @@ class HomeContent extends StatelessWidget { ); } - Widget _buildCreateTravelCard(BuildContext context) { - return Card( - elevation: 2, - margin: EdgeInsets.only(bottom: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: BorderSide( - color: Colors.grey[300]!, - width: 2, - style: BorderStyle.solid, - ), - ), - child: InkWell( - onTap: () { - _showCreateTravelDialog(context); - }, - borderRadius: BorderRadius.circular(12), - child: Container( - padding: EdgeInsets.all(24), - child: Column( - children: [ - Icon(Icons.add_circle_outline, size: 48, color: Colors.grey[400]), - SizedBox(height: 12), - Text( - 'Créer un nouveau voyage', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.grey[600], - ), - ), - SizedBox(height: 4), - Text( - 'Organisez votre prochaine aventure', - style: TextStyle(fontSize: 14, color: Colors.grey[500]), - ), - ], - ), - ), - ), - ); - } - void _showTravelDetails(BuildContext context, String title) { Navigator.push( context, @@ -359,29 +327,9 @@ class HomeContent extends StatelessWidget { } void _showCreateTravelDialog(BuildContext context) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text('Créer un voyage'), - content: Text( - 'Fonctionnalité à implémenter pour créer un nouveau voyage.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text('Fermer'), - ), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - // TODO: Naviguer vers le formulaire de création - }, - child: Text('Créer'), - ), - ], - ); - }, + Navigator.push( + context, + MaterialPageRoute(builder: (context) => CreateTripContent()), ); } } diff --git a/lib/models/trip.dart b/lib/models/trip.dart new file mode 100644 index 0000000..dc40d73 --- /dev/null +++ b/lib/models/trip.dart @@ -0,0 +1,181 @@ +import 'dart:convert'; + +class Trip { + final String? id; + final String title; + final String description; + final String location; + final DateTime startDate; + final DateTime endDate; + final double? budget; + final List participants; + final String createdBy; // ID de l'utilisateur créateur + final DateTime createdAt; + final DateTime updatedAt; + final String status; // 'draft', 'active', 'completed', 'cancelled' + + Trip({ + this.id, + required this.title, + required this.description, + required this.location, + required this.startDate, + required this.endDate, + this.budget, + required this.participants, + required this.createdBy, + required this.createdAt, + required this.updatedAt, + this.status = 'draft', + }); + + // Constructeur pour créer un Trip depuis un Map (utile pour Firebase) + factory Trip.fromMap(Map map) { + return Trip( + id: map['id'], + title: map['title'] ?? '', + description: map['description'] ?? '', + location: map['location'] ?? '', + startDate: DateTime.fromMillisecondsSinceEpoch(map['startDate']), + endDate: DateTime.fromMillisecondsSinceEpoch(map['endDate']), + budget: map['budget']?.toDouble(), + participants: List.from(map['participants'] ?? []), + createdBy: map['createdBy'] ?? '', + createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']), + updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt']), + status: map['status'] ?? 'draft', + ); + } + + // Constructeur pour créer un Trip depuis JSON + factory Trip.fromJson(String jsonStr) { + Map map = json.decode(jsonStr); + return Trip.fromMap(map); + } + + // Méthode pour convertir un Trip en Map (utile pour Firebase) + Map toMap() { + return { + 'id': id, + 'title': title, + 'description': description, + 'location': location, + 'startDate': startDate.millisecondsSinceEpoch, + 'endDate': endDate.millisecondsSinceEpoch, + 'budget': budget, + 'participants': participants, + 'createdBy': createdBy, + 'createdAt': createdAt.millisecondsSinceEpoch, + 'updatedAt': updatedAt.millisecondsSinceEpoch, + 'status': status, + }; + } + + // Méthode pour convertir un Trip en JSON + String toJson() { + return json.encode(toMap()); + } + + // Méthode pour créer une copie avec des modifications + Trip copyWith({ + String? id, + String? title, + String? description, + String? location, + DateTime? startDate, + DateTime? endDate, + double? budget, + List? participants, + String? createdBy, + DateTime? createdAt, + DateTime? updatedAt, + String? status, + }) { + return Trip( + id: id ?? this.id, + title: title ?? this.title, + description: description ?? this.description, + location: location ?? this.location, + startDate: startDate ?? this.startDate, + endDate: endDate ?? this.endDate, + budget: budget ?? this.budget, + participants: participants ?? this.participants, + createdBy: createdBy ?? this.createdBy, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + status: status ?? this.status, + ); + } + + // Méthode pour obtenir la durée du voyage en jours + int get durationInDays { + return endDate.difference(startDate).inDays + 1; + } + + // Méthode pour vérifier si le voyage est en cours + bool get isActive { + final now = DateTime.now(); + return status == 'active' && + now.isAfter(startDate) && + now.isBefore(endDate.add(Duration(days: 1))); + } + + // Méthode pour vérifier si le voyage est à venir + bool get isUpcoming { + final now = DateTime.now(); + return status == 'active' && now.isBefore(startDate); + } + + // Méthode pour vérifier si le voyage est terminé + bool get isCompleted { + final now = DateTime.now(); + return status == 'completed' || + (status == 'active' && now.isAfter(endDate)); + } + + // Méthode pour obtenir le budget par participant + double? get budgetPerParticipant { + if (budget == null || participants.isEmpty) return null; + return budget! / (participants.length + 1); // +1 pour le créateur + } + + // Méthode pour obtenir le nombre total de participants (incluant le créateur) + int get totalParticipants { + return participants.length + 1; // +1 pour le créateur + } + + // Méthode pour formater les dates + String get formattedDates { + return '${startDate.day}/${startDate.month}/${startDate.year} - ${endDate.day}/${endDate.month}/${endDate.year}'; + } + + // Méthode pour obtenir le statut formaté + String get formattedStatus { + switch (status) { + case 'draft': + return 'Brouillon'; + case 'active': + return 'Actif'; + case 'completed': + return 'Terminé'; + case 'cancelled': + return 'Annulé'; + default: + return 'Inconnu'; + } + } + + @override + String toString() { + return 'Trip(id: $id, title: $title, location: $location, dates: $formattedDates, participants: ${participants.length})'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is Trip && other.id == id; + } + + @override + int get hashCode => id.hashCode; +} diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 5d9947f..ce4df11 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -27,10 +27,10 @@ class _HomePageState extends State { ]; final List titles = [ - 'Accueil', // 0 + 'Mes voyages', // 0 'Paramètres', // 1 'Carte', // 2 - 'Mes groupes', // 3 + 'Mes chats', // 3 'Comptes', // 4 ]; @@ -55,10 +55,9 @@ class _HomePageState extends State { ), ListTile( leading: Icon(Icons.home), - title: Text("Accueil"), + title: Text("Mes voyages"), selected: _currentIndex == 0, onTap: () { - print('Clicked Accueil - Setting index to 0'); // Debug setState(() { _currentIndex = 0; }); @@ -70,7 +69,6 @@ class _HomePageState extends State { title: Text("Paramètres"), selected: _currentIndex == 1, onTap: () { - print('Clicked Paramètres - Setting index to 1'); // Debug setState(() { _currentIndex = 1; }); @@ -82,7 +80,6 @@ class _HomePageState extends State { title: Text("Carte"), selected: _currentIndex == 2, onTap: () { - print('Clicked Carte - Setting index to 2'); // Debug setState(() { _currentIndex = 2; }); @@ -94,7 +91,6 @@ class _HomePageState extends State { title: Text("Mes groupes"), selected: _currentIndex == 3, onTap: () { - print('Clicked Groupes - Setting index to 3'); // Debug setState(() { _currentIndex = 3; }); @@ -106,7 +102,6 @@ class _HomePageState extends State { title: Text("Comptes"), selected: _currentIndex == 4, onTap: () { - print('Clicked Comptes - Setting index to 4'); // Debug setState(() { _currentIndex = 4; }); diff --git a/lib/pages/signup.dart b/lib/pages/signup.dart index e80551e..981686f 100644 --- a/lib/pages/signup.dart +++ b/lib/pages/signup.dart @@ -202,21 +202,6 @@ class _SignUpPageState extends State { const SizedBox(height: 40), - // Champ Nom - TextFormField( - controller: _nomController, - validator: (value) => _validateField(value, 'Nom'), - decoration: InputDecoration( - labelText: 'Nom', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - prefixIcon: Icon(Icons.person), - ), - ), - - const SizedBox(height: 20), - // Champ Prénom TextFormField( controller: _prenomController, @@ -232,6 +217,21 @@ class _SignUpPageState extends State { const SizedBox(height: 20), + // Champ Nom + TextFormField( + controller: _nomController, + validator: (value) => _validateField(value, 'Nom'), + decoration: InputDecoration( + labelText: 'Nom', + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + prefixIcon: Icon(Icons.person), + ), + ), + + const SizedBox(height: 20), + // Champ Email TextFormField( controller: _emailController, diff --git a/lib/services/trip_service.dart b/lib/services/trip_service.dart new file mode 100644 index 0000000..1deb7ac --- /dev/null +++ b/lib/services/trip_service.dart @@ -0,0 +1,108 @@ +import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/trip.dart'; + +class TripService { + static const String _tripsKey = 'trips_data'; + + // Charger tous les voyages + Future> loadTrips() async { + try { + final prefs = await SharedPreferences.getInstance(); + final tripsJson = prefs.getString(_tripsKey); + + if (tripsJson == null || tripsJson.isEmpty) return []; + + final List jsonList = json.decode(tripsJson); + return jsonList.map((json) => Trip.fromMap(json)).toList(); + } catch (e) { + print('Erreur lors du chargement des voyages: $e'); + return []; + } + } + + // Sauvegarder tous les voyages + Future saveTrips(List trips) async { + try { + final prefs = await SharedPreferences.getInstance(); + final jsonList = trips.map((trip) => trip.toMap()).toList(); + await prefs.setString(_tripsKey, json.encode(jsonList)); + } catch (e) { + print('Erreur lors de la sauvegarde des voyages: $e'); + throw Exception('Erreur de sauvegarde'); + } + } + + // Ajouter un nouveau voyage + Future addTrip(Trip trip) async { + try { + final trips = await loadTrips(); + + // Générer un ID unique + final newTrip = trip.copyWith( + id: DateTime.now().millisecondsSinceEpoch.toString(), + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ); + + trips.add(newTrip); + await saveTrips(trips); + return true; + } catch (e) { + print('Erreur lors de l\'ajout du voyage: $e'); + return false; + } + } + + // Obtenir les voyages d'un utilisateur + Future> getTripsByUser(String userId) async { + try { + final trips = await loadTrips(); + return trips + .where( + (trip) => + trip.createdBy == userId || trip.participants.contains(userId), + ) + .toList(); + } catch (e) { + print('Erreur lors de la récupération des voyages: $e'); + return []; + } + } + + // Mettre à jour un voyage + Future updateTrip(Trip updatedTrip) async { + try { + final trips = await loadTrips(); + final index = trips.indexWhere((trip) => trip.id == updatedTrip.id); + + if (index != -1) { + trips[index] = updatedTrip.copyWith(updatedAt: DateTime.now()); + await saveTrips(trips); + return true; + } + return false; + } catch (e) { + print('Erreur lors de la mise à jour du voyage: $e'); + return false; + } + } + + // Supprimer un voyage + Future deleteTrip(String tripId) async { + try { + final trips = await loadTrips(); + final initialLength = trips.length; + trips.removeWhere((trip) => trip.id == tripId); + + if (trips.length < initialLength) { + await saveTrips(trips); + return true; + } + return false; + } catch (e) { + print('Erreur lors de la suppression du voyage: $e'); + return false; + } + } +}