Modifications des permissions, ajout Presta OK, vue calendrier ok

This commit is contained in:
2025-05-24 23:50:07 +02:00
parent 249a6d6074
commit 851b891a8a
18 changed files with 1077 additions and 304 deletions

9
.cursor/rules/rules1.mdc Normal file
View File

@ -0,0 +1,9 @@
---
description: To remember for every prompts
globs:
alwaysApply: false
---
Le plus important : Repondre en français. Toujours appliquer les modification au code sauf si le message commence par "QUESTION :"
Projet d'ERP pour une entreprise d'événemetiel. Flutter, Dart

View File

@ -15,6 +15,7 @@ import 'providers/local_user_provider.dart';
import 'services/user_service.dart'; import 'services/user_service.dart';
import 'pages/auth/reset_password_page.dart'; import 'pages/auth/reset_password_page.dart';
import 'config/env.dart'; import 'config/env.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -81,6 +82,15 @@ class MyApp extends StatelessWidget {
), ),
), ),
), ),
locale: const Locale('fr', 'FR'),
supportedLocales: const [
Locale('fr', 'FR'),
],
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: const AutoLoginWrapper(), home: const AutoLoginWrapper(),
routes: { routes: {
'/login': (context) => const LoginPage(), '/login': (context) => const LoginPage(),

View File

@ -1,100 +1,28 @@
enum Permission { import 'package:cloud_firestore/cloud_firestore.dart';
// Permissions liées aux prestations
viewAllEvents, // Voir toutes les prestations
viewAssignedEvents, // Voir uniquement les prestations assignées
editEvents, // Modifier les prestations
deleteEvents, // Supprimer les prestations
assignCrew, // Assigner des membres d'équipe aux prestations
// Permissions liées aux finances class RoleModel {
viewPrices, // Voir les prix final String id;
editPrices, // Modifier les prix
viewQuotes, // Voir les devis
createQuotes, // Créer des devis
editQuotes, // Modifier les devis
viewInvoices, // Voir les factures
createInvoices, // Créer des factures
editInvoices, // Modifier les factures
// Permissions liées aux utilisateurs
viewUsers, // Voir les utilisateurs
editUsers, // Modifier les utilisateurs
deleteUsers, // Supprimer les utilisateurs
// Permissions liées aux clients
viewClients, // Voir les clients
editClients, // Modifier les clients
deleteClients, // Supprimer les clients
}
class Role {
final String name; final String name;
final Set<Permission> permissions; final List<String> permissions;
const Role({ RoleModel({
required this.id,
required this.name, required this.name,
required this.permissions, required this.permissions,
}); });
bool hasPermission(Permission permission) => permissions.contains(permission); factory RoleModel.fromMap(Map<String, dynamic> map, String id) {
return RoleModel(
bool hasAllPermissions(List<Permission> requiredPermissions) { id: id,
return requiredPermissions name: map['name'] ?? '',
.every((permission) => permissions.contains(permission)); permissions: List<String>.from(map['permissions'] ?? []),
);
} }
bool hasAnyPermission(List<Permission> requiredPermissions) { Map<String, dynamic> toMap() {
return requiredPermissions return {
.any((permission) => permissions.contains(permission)); 'name': name,
'permissions': permissions,
};
} }
} }
class Roles {
static const admin = Role(
name: 'ADMIN',
permissions: {
// Toutes les permissions pour l'administrateur
Permission.viewAllEvents,
Permission.viewAssignedEvents,
Permission.editEvents,
Permission.deleteEvents,
Permission.assignCrew,
Permission.viewPrices,
Permission.editPrices,
Permission.viewQuotes,
Permission.createQuotes,
Permission.editQuotes,
Permission.viewInvoices,
Permission.createInvoices,
Permission.editInvoices,
Permission.viewUsers,
Permission.editUsers,
Permission.deleteUsers,
Permission.viewClients,
Permission.editClients,
Permission.deleteClients,
},
);
static const crew = Role(
name: 'CREW',
permissions: {
// Permissions limitées pour l'équipe
Permission.viewAssignedEvents,
Permission.viewClients,
},
);
static Role fromString(String roleName) {
switch (roleName.toUpperCase()) {
case 'ADMIN':
return admin;
case 'CREW':
return crew;
default:
return crew; // Par défaut, on donne les permissions minimales
}
}
static List<Role> values = [admin, crew];
}

View File

@ -1,3 +1,5 @@
import 'package:cloud_firestore/cloud_firestore.dart';
class UserModel { class UserModel {
final String uid; final String uid;
final String firstName; final String firstName;
@ -19,11 +21,20 @@ class UserModel {
// Convertit une Map (Firestore) en UserModel // Convertit une Map (Firestore) en UserModel
factory UserModel.fromMap(Map<String, dynamic> data, String uid) { factory UserModel.fromMap(Map<String, dynamic> data, String uid) {
String roleString;
final roleField = data['role'];
if (roleField is String) {
roleString = roleField;
} else if (roleField is DocumentReference) {
roleString = roleField.id;
} else {
roleString = 'USER';
}
return UserModel( return UserModel(
uid: uid, uid: uid,
firstName: data['firstName'] ?? '', firstName: data['firstName'] ?? '',
lastName: data['lastName'] ?? '', lastName: data['lastName'] ?? '',
role: data['role'] ?? 'USER', role: roleString,
profilePhotoUrl: data['profilePhotoUrl'] ?? '', profilePhotoUrl: data['profilePhotoUrl'] ?? '',
email: data['email'] ?? '', email: data['email'] ?? '',
phoneNumber: data['phoneNumber'] ?? '', phoneNumber: data['phoneNumber'] ?? '',

View File

@ -11,24 +11,29 @@ class EventProvider with ChangeNotifier {
bool get isLoading => _isLoading; bool get isLoading => _isLoading;
// Récupérer les événements pour un utilisateur spécifique // Récupérer les événements pour un utilisateur spécifique
Future<void> loadUserEvents(String userId) async { Future<void> loadUserEvents(String userId,
{bool canViewAllEvents = false}) async {
_isLoading = true; _isLoading = true;
notifyListeners(); notifyListeners();
try { try {
print('Loading events for user: $userId'); print(
'Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)');
// Récupérer uniquement les événements où l'utilisateur est dans la workforce QuerySnapshot eventsSnapshot;
final eventsSnapshot = await _firestore if (canViewAllEvents) {
.collection('events') eventsSnapshot = await _firestore.collection('events').get();
.where('workforce', arrayContains: userId) } else {
.get(); eventsSnapshot = await _firestore
.collection('events')
.where('workforce', arrayContains: userId)
.get();
}
print('Found ${eventsSnapshot.docs.length} events for user'); print('Found ${eventsSnapshot.docs.length} events for user');
_events = eventsSnapshot.docs.map((doc) { _events = eventsSnapshot.docs.map((doc) {
print('Event data: ${doc.data()}'); print('Event data: ${doc.data()}');
return EventModel.fromMap(doc.data(), doc.id); return EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
}).toList(); }).toList();
print('Parsed ${_events.length} events'); print('Parsed ${_events.length} events');

View File

@ -3,10 +3,12 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import '../models/user_model.dart'; import '../models/user_model.dart';
import '../models/role_model.dart';
import '../utils/firebase_storage_manager.dart'; import '../utils/firebase_storage_manager.dart';
class LocalUserProvider with ChangeNotifier { class LocalUserProvider with ChangeNotifier {
UserModel? _currentUser; UserModel? _currentUser;
RoleModel? _currentRole;
final FirebaseAuth _auth = FirebaseAuth.instance; final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseStorageManager _storageManager = FirebaseStorageManager(); final FirebaseStorageManager _storageManager = FirebaseStorageManager();
@ -19,6 +21,8 @@ class LocalUserProvider with ChangeNotifier {
String? get profilePhotoUrl => _currentUser?.profilePhotoUrl; String? get profilePhotoUrl => _currentUser?.profilePhotoUrl;
String? get email => _currentUser?.email; String? get email => _currentUser?.email;
String? get phoneNumber => _currentUser?.phoneNumber; String? get phoneNumber => _currentUser?.phoneNumber;
RoleModel? get currentRole => _currentRole;
List<String> get permissions => _currentRole?.permissions ?? [];
/// Charge les données de l'utilisateur actuel /// Charge les données de l'utilisateur actuel
Future<void> loadUserData() async { Future<void> loadUserData() async {
@ -47,6 +51,7 @@ class LocalUserProvider with ChangeNotifier {
setUser(UserModel.fromMap(userData, userDoc.id)); setUser(UserModel.fromMap(userData, userDoc.id));
print('User data loaded successfully'); print('User data loaded successfully');
await loadRole();
} else { } else {
print('No user document found in Firestore'); print('No user document found in Firestore');
// Créer un document utilisateur par défaut // Créer un document utilisateur par défaut
@ -73,6 +78,7 @@ class LocalUserProvider with ChangeNotifier {
setUser(defaultUser); setUser(defaultUser);
print('Default user document created'); print('Default user document created');
await loadRole();
} }
} catch (e) { } catch (e) {
print('Error loading user data: $e'); print('Error loading user data: $e');
@ -154,4 +160,24 @@ class LocalUserProvider with ChangeNotifier {
await _auth.signOut(); await _auth.signOut();
clearUser(); clearUser();
} }
Future<void> loadRole() async {
if (_currentUser == null) return;
final roleId = _currentUser!.role;
if (roleId.isEmpty) return;
try {
final doc = await _firestore.collection('roles').doc(roleId).get();
if (doc.exists) {
_currentRole =
RoleModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
notifyListeners();
}
} catch (e) {
print('Error loading role: $e');
}
}
bool hasPermission(String permission) {
return _currentRole?.permissions.contains(permission) ?? false;
}
} }

View File

@ -4,6 +4,8 @@ import '../services/user_service.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/providers/local_user_provider.dart';
class UsersProvider with ChangeNotifier { class UsersProvider with ChangeNotifier {
final UserService _userService; final UserService _userService;
@ -75,7 +77,8 @@ class UsersProvider with ChangeNotifier {
await _userService.resetPassword(email); await _userService.resetPassword(email);
} }
Future<void> createUserWithEmailInvite(UserModel user) async { Future<void> createUserWithEmailInvite(
BuildContext context, UserModel user) async {
String? authUid; String? authUid;
try { try {
@ -87,14 +90,12 @@ class UsersProvider with ChangeNotifier {
throw Exception('Aucun utilisateur connecté'); throw Exception('Aucun utilisateur connecté');
} }
// Vérifier le rôle de l'utilisateur actuel // Vérifier la permission via le provider
final currentUserDoc = final localUserProvider =
await _firestore.collection('users').doc(currentUser.uid).get(); Provider.of<LocalUserProvider>(context, listen: false);
print('Current user role: ${currentUserDoc.data()?['role']}'); if (!localUserProvider.hasPermission('add_user')) {
if (currentUserDoc.data()?['role'] != 'ADMIN') {
throw Exception( throw Exception(
'Seuls les administrateurs peuvent créer des utilisateurs'); 'Vous n\'avez pas la permission de créer des utilisateurs');
} }
try { try {

View File

@ -5,7 +5,7 @@ import 'package:em2rp/providers/local_user_provider.dart';
class PermissionGate extends StatelessWidget { class PermissionGate extends StatelessWidget {
final Widget child; final Widget child;
final List<Permission> requiredPermissions; final List<String> requiredPermissions;
final bool requireAll; final bool requireAll;
final Widget? fallback; final Widget? fallback;
@ -26,10 +26,9 @@ class PermissionGate extends StatelessWidget {
return fallback ?? const SizedBox.shrink(); return fallback ?? const SizedBox.shrink();
} }
final userRole = Roles.fromString(currentUser.role);
final hasPermission = requireAll final hasPermission = requireAll
? userRole.hasAllPermissions(requiredPermissions) ? hasAllPermissions(localUserProvider, requiredPermissions)
: userRole.hasAnyPermission(requiredPermissions); : hasAnyPermission(localUserProvider, requiredPermissions);
if (hasPermission) { if (hasPermission) {
return child; return child;
@ -39,4 +38,18 @@ class PermissionGate extends StatelessWidget {
}, },
); );
} }
bool hasAllPermissions(LocalUserProvider provider, List<String> permissions) {
for (final perm in permissions) {
if (!provider.hasPermission(perm)) return false;
}
return true;
}
bool hasAnyPermission(LocalUserProvider provider, List<String> permissions) {
for (final perm in permissions) {
if (provider.hasPermission(perm)) return true;
}
return false;
}
} }

View File

@ -27,32 +27,36 @@ class LoginViewModel extends ChangeNotifier {
notifyListeners(); notifyListeners();
try { try {
print('User signed in'); // --- Étape 1: Connecter l'utilisateur dans Firebase Auth ---
// Appelle la méthode du provider qui gère la connexion Auth ET le chargement des données utilisateur
// Attendre que les données utilisateur soient chargées await localAuthProvider.signInWithEmailAndPassword(
await localAuthProvider.loadUserData(); emailController.text,
passwordController.text,
);
// Vérifier si le contexte est toujours valide // Vérifier si le contexte est toujours valide
if (context.mounted) { if (context.mounted) {
// Vérifier si l'utilisateur a bien été chargé // Vérifier si l'utilisateur a bien été chargé dans le provider
if (localAuthProvider.currentUser != null) { if (localAuthProvider.currentUser != null) {
// Utiliser pushReplacementNamed pour une transition propre // Utiliser pushReplacementNamed pour une transition propre
Navigator.of(context, rootNavigator: true) Navigator.of(context, rootNavigator: true)
.pushReplacementNamed('/calendar'); .pushReplacementNamed('/calendar');
} else { } else {
errorMessage = 'Erreur lors du chargement des données utilisateur'; errorMessage =
'Erreur inattendue après connexion: Données utilisateur non chargées.';
isLoading = false; isLoading = false;
notifyListeners(); notifyListeners();
} }
} }
} on FirebaseAuthException catch (e) { } on FirebaseAuthException catch (e) {
// Gestion spécifique des erreurs d'authentification (email/mot de passe incorrects, etc.)
isLoading = false; isLoading = false;
errorMessage = errorMessage =
e.message ?? 'Une erreur est survenue lors de la connexion'; e.message ?? 'Une erreur est survenue lors de la connexion Firebase.';
notifyListeners(); notifyListeners();
} catch (e) { } catch (e) {
// Gestion des autres erreurs potentielles (ex: erreur lors de loadUserData)
isLoading = false; isLoading = false;
errorMessage = 'Une erreur inattendue est survenue'; errorMessage = 'Une erreur inattendue est survenue: ${e.toString()}';
notifyListeners(); notifyListeners();
} }
} }

View File

@ -1,15 +1,16 @@
import 'package:em2rp/providers/local_user_provider.dart'; import 'package:em2rp/providers/local_user_provider.dart';
import 'package:em2rp/providers/event_provider.dart'; import 'package:em2rp/providers/event_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:em2rp/widgets/custom_app_bar.dart'; import 'package:em2rp/views/widgets/custom_app_bar.dart';
import 'package:em2rp/views/widgets/nav/main_drawer.dart'; import 'package:em2rp/views/widgets/nav/main_drawer.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:table_calendar/table_calendar.dart'; import 'package:table_calendar/table_calendar.dart';
import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/widgets/event_details.dart'; import 'package:em2rp/views/widgets/calendar_widgets/event_details.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:em2rp/views/widgets/calendar_widgets/month_view.dart'; import 'package:em2rp/views/widgets/calendar_widgets/month_view.dart';
import 'package:em2rp/views/widgets/calendar_widgets/week_view.dart'; import 'package:em2rp/views/widgets/calendar_widgets/week_view.dart';
import 'package:em2rp/views/pages/event_add_page.dart';
class CalendarPage extends StatefulWidget { class CalendarPage extends StatefulWidget {
const CalendarPage({super.key}); const CalendarPage({super.key});
@ -36,9 +37,13 @@ class _CalendarPageState extends State<CalendarPage> {
Provider.of<LocalUserProvider>(context, listen: false); Provider.of<LocalUserProvider>(context, listen: false);
final eventProvider = Provider.of<EventProvider>(context, listen: false); final eventProvider = Provider.of<EventProvider>(context, listen: false);
final userId = localAuthProvider.uid; final userId = localAuthProvider.uid;
print('Permissions utilisateur: ${localAuthProvider.permissions}');
final canViewAllEvents = localAuthProvider.hasPermission('view_all_events');
print('canViewAllEvents: $canViewAllEvents');
if (userId != null) { if (userId != null) {
await eventProvider.loadUserEvents(userId); await eventProvider.loadUserEvents(userId,
canViewAllEvents: canViewAllEvents);
} }
} }
@ -51,6 +56,8 @@ class _CalendarPageState extends State<CalendarPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final eventProvider = Provider.of<EventProvider>(context); final eventProvider = Provider.of<EventProvider>(context);
final localUserProvider = Provider.of<LocalUserProvider>(context);
final isAdmin = localUserProvider.role == 'ADMIN';
final isMobile = MediaQuery.of(context).size.width < 600; final isMobile = MediaQuery.of(context).size.width < 600;
if (eventProvider.isLoading) { if (eventProvider.isLoading) {
@ -67,6 +74,20 @@ class _CalendarPageState extends State<CalendarPage> {
), ),
drawer: const MainDrawer(currentPage: '/calendar'), drawer: const MainDrawer(currentPage: '/calendar'),
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(), body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
floatingActionButton: isAdmin
? FloatingActionButton(
backgroundColor: Colors.white,
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const EventAddPage(),
),
);
},
child: const Icon(Icons.add, color: Colors.red),
tooltip: 'Ajouter un événement',
)
: null,
); );
} }

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:em2rp/views/widgets/inputs/styled_text_field.dart'; import 'package:em2rp/views/widgets/inputs/styled_text_field.dart';
import 'package:em2rp/views/widgets/image/profile_picture_selector.dart'; import 'package:em2rp/views/widgets/image/profile_picture_selector.dart';
import 'package:em2rp/widgets/custom_app_bar.dart'; import 'package:em2rp/views/widgets/custom_app_bar.dart';
class MyAccountPage extends StatelessWidget { class MyAccountPage extends StatelessWidget {
const MyAccountPage({super.key}); const MyAccountPage({super.key});

View File

@ -0,0 +1,410 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/providers/event_provider.dart';
import 'package:latlong2/latlong.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:intl/intl.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/views/widgets/inputs/int_stepper_field.dart';
class EventAddPage extends StatefulWidget {
const EventAddPage({super.key});
@override
State<EventAddPage> createState() => _EventAddPageState();
}
class _EventAddPageState extends State<EventAddPage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
final TextEditingController _priceController = TextEditingController();
final TextEditingController _installationController = TextEditingController();
final TextEditingController _disassemblyController = TextEditingController();
final TextEditingController _latitudeController = TextEditingController();
final TextEditingController _longitudeController = TextEditingController();
DateTime? _startDateTime;
DateTime? _endDateTime;
bool _isLoading = false;
String? _error;
String? _success;
String? _selectedEventType;
final List<String> _eventTypes = ['Bal', 'Mariage', 'Anniversaire'];
int _descriptionMaxLines = 3;
@override
void initState() {
super.initState();
_descriptionController.addListener(_handleDescriptionChange);
}
void _handleDescriptionChange() {
final lines = '\n'.allMatches(_descriptionController.text).length + 1;
setState(() {
_descriptionMaxLines = lines.clamp(3, 6);
});
}
@override
void dispose() {
_nameController.dispose();
_descriptionController.dispose();
_priceController.dispose();
_installationController.dispose();
_disassemblyController.dispose();
_latitudeController.dispose();
_longitudeController.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate() ||
_startDateTime == null ||
_endDateTime == null ||
_selectedEventType == null) return;
setState(() {
_isLoading = true;
_error = null;
_success = null;
});
try {
final eventProvider = Provider.of<EventProvider>(context, listen: false);
final newEvent = EventModel(
id: '',
name: _nameController.text.trim(),
description: _descriptionController.text.trim(),
startDateTime: _startDateTime!,
endDateTime: _endDateTime!,
price: double.tryParse(_priceController.text) ?? 0.0,
installationTime: int.tryParse(_installationController.text) ?? 0,
disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0,
eventTypeId: _selectedEventType!,
customerId: '', // à adapter si tu veux gérer les clients
address: LatLng(
double.tryParse(_latitudeController.text) ?? 0.0,
double.tryParse(_longitudeController.text) ?? 0.0,
),
workforce: [],
);
await eventProvider.addEvent(newEvent);
setState(() {
_success = "Événement créé avec succès !";
});
if (context.mounted) Navigator.of(context).pop();
} catch (e) {
setState(() {
_error = "Erreur lors de la création : $e";
});
} finally {
setState(() {
_isLoading = false;
});
}
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(top: 16.0, bottom: 8.0),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('Créer un événement'),
),
body: Center(
child: SingleChildScrollView(
child: Card(
elevation: 6,
margin: const EdgeInsets.all(24),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 32),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.only(top: 0.0, bottom: 4.0),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'Informations principales',
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
),
),
),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Nom de l\'événement',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.event),
),
validator: (v) =>
v == null || v.isEmpty ? 'Champ requis' : null,
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
value: _selectedEventType,
items: _eventTypes
.map((type) => DropdownMenuItem<String>(
value: type,
child: Text(type),
))
.toList(),
onChanged: (val) =>
setState(() => _selectedEventType = val),
decoration: const InputDecoration(
labelText: 'Type d\'événement',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.category),
),
validator: (v) =>
v == null ? 'Sélectionnez un type' : null,
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2099),
);
if (picked != null) {
final time = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (time != null) {
setState(() {
_startDateTime = DateTime(
picked.year,
picked.month,
picked.day,
time.hour,
time.minute,
);
});
}
}
},
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
decoration: InputDecoration(
labelText: 'Début',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.calendar_today),
suffixIcon: const Icon(Icons.edit_calendar),
),
controller: TextEditingController(
text: _startDateTime == null
? ''
: DateFormat('dd/MM/yyyy HH:mm')
.format(_startDateTime!),
),
validator: (v) => _startDateTime == null
? 'Champ requis'
: null,
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: GestureDetector(
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: _startDateTime ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2099),
);
if (picked != null) {
final time = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (time != null) {
setState(() {
_endDateTime = DateTime(
picked.year,
picked.month,
picked.day,
time.hour,
time.minute,
);
});
}
}
},
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
decoration: InputDecoration(
labelText: 'Fin',
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.calendar_today),
suffixIcon: const Icon(Icons.edit_calendar),
),
controller: TextEditingController(
text: _endDateTime == null
? ''
: DateFormat('dd/MM/yyyy HH:mm')
.format(_endDateTime!),
),
validator: (v) => _endDateTime == null
? 'Champ requis'
: null,
),
),
),
),
],
),
const SizedBox(height: 16),
TextFormField(
controller: _priceController,
decoration: const InputDecoration(
labelText: 'Prix (€)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.euro),
),
keyboardType: TextInputType.number,
),
_buildSectionTitle('Détails'),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
constraints: BoxConstraints(
minHeight: 48,
maxHeight: 48.0 * 10,
),
child: TextFormField(
controller: _descriptionController,
minLines: 1,
maxLines: _descriptionMaxLines > 10
? 10
: _descriptionMaxLines,
decoration: const InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.description),
),
),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: IntStepperField(
label: 'Installation (h)',
controller: _installationController,
min: 0,
max: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: IntStepperField(
label: 'Démontage (h)',
controller: _disassemblyController,
min: 0,
max: 24,
),
),
],
),
_buildSectionTitle('Localisation'),
Row(
children: [
Expanded(
child: TextFormField(
controller: _latitudeController,
decoration: const InputDecoration(
labelText: 'Latitude',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.location_on),
),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 16),
Expanded(
child: TextFormField(
controller: _longitudeController,
decoration: const InputDecoration(
labelText: 'Longitude',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.location_on),
),
keyboardType: TextInputType.number,
),
),
],
),
if (_error != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(_error!,
style: const TextStyle(color: Colors.red)),
),
if (_success != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(_success!,
style: const TextStyle(color: Colors.green)),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: _isLoading
? null
: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
const SizedBox(width: 8),
ElevatedButton.icon(
icon: const Icon(Icons.check),
onPressed: _isLoading ? null : _submit,
label: _isLoading
? const SizedBox(
width: 20,
height: 20,
child:
CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Créer'),
),
],
),
],
),
),
),
),
),
),
);
}
}

View File

@ -8,7 +8,7 @@ import 'package:em2rp/views/widgets/user_management/edit_user_dialog.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/utils/permission_gate.dart'; import 'package:em2rp/utils/permission_gate.dart';
import 'package:em2rp/models/role_model.dart'; import 'package:em2rp/models/role_model.dart';
import 'package:em2rp/widgets/custom_app_bar.dart'; import 'package:em2rp/views/widgets/custom_app_bar.dart';
class UserManagementPage extends StatefulWidget { class UserManagementPage extends StatefulWidget {
const UserManagementPage({super.key}); const UserManagementPage({super.key});
@ -31,7 +31,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PermissionGate( return PermissionGate(
requiredPermissions: const [Permission.viewUsers], requiredPermissions: const ['view_all_users'],
fallback: const Scaffold( fallback: const Scaffold(
appBar: CustomAppBar( appBar: CustomAppBar(
title: 'Accès refusé', title: 'Accès refusé',
@ -110,7 +110,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
final lastNameController = TextEditingController(); final lastNameController = TextEditingController();
final emailController = TextEditingController(); final emailController = TextEditingController();
final phoneController = TextEditingController(); final phoneController = TextEditingController();
String selectedRole = Roles.values.first.name; String selectedRole = 'ADMIN';
InputDecoration buildInputDecoration(String label, IconData icon) { InputDecoration buildInputDecoration(String label, IconData icon) {
return InputDecoration( return InputDecoration(
@ -188,10 +188,10 @@ class _UserManagementPageState extends State<UserManagementPage> {
value: selectedRole, value: selectedRole,
decoration: buildInputDecoration( decoration: buildInputDecoration(
'Rôle', Icons.admin_panel_settings_outlined), 'Rôle', Icons.admin_panel_settings_outlined),
items: Roles.values.map((Role role) { items: ['ADMIN', 'CREW'].map((String role) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: role.name, value: role,
child: Text(role.name), child: Text(role),
); );
}).toList(), }).toList(),
onChanged: (String? newValue) { onChanged: (String? newValue) {
@ -247,7 +247,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
final scaffoldMessenger = ScaffoldMessenger.of(context); final scaffoldMessenger = ScaffoldMessenger.of(context);
await Provider.of<UsersProvider>(context, listen: false) await Provider.of<UsersProvider>(context, listen: false)
.createUserWithEmailInvite(newUser); .createUserWithEmailInvite(context, newUser);
if (context.mounted) { if (context.mounted) {
Navigator.pop(context); Navigator.pop(context);

View File

@ -0,0 +1,420 @@
import 'package:flutter/material.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/providers/local_user_provider.dart';
import 'package:em2rp/providers/event_provider.dart';
import 'package:latlong2/latlong.dart';
class EventDetails extends StatelessWidget {
final EventModel event;
final DateTime? selectedDate;
final List<EventModel> events;
final void Function(EventModel, DateTime) onSelectEvent;
const EventDetails({
super.key,
required this.event,
required this.selectedDate,
required this.events,
required this.onSelectEvent,
});
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('dd/MM/yyyy HH:mm');
final currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: '');
final fullDateFormat = DateFormat('EEEE d MMMM y', 'fr_FR');
// Trie les événements par date de début
final sortedEvents = List<EventModel>.from(events)
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
final currentIndex = sortedEvents.indexWhere((e) => e.id == event.id);
final localUserProvider = Provider.of<LocalUserProvider>(context);
final isAdmin = localUserProvider.role == 'ADMIN';
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: currentIndex > 0
? () {
final prevEvent = sortedEvents[currentIndex - 1];
onSelectEvent(prevEvent, prevEvent.startDateTime);
}
: null,
icon: const Icon(Icons.arrow_back),
color: AppColors.rouge,
),
if (selectedDate != null)
Expanded(
child: Center(
child: Text(
fullDateFormat.format(selectedDate!),
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppColors.rouge,
fontWeight: FontWeight.bold,
),
),
),
),
IconButton(
onPressed: currentIndex < sortedEvents.length - 1
? () {
final nextEvent = sortedEvents[currentIndex + 1];
onSelectEvent(nextEvent, nextEvent.startDateTime);
}
: null,
icon: const Icon(Icons.arrow_forward),
color: AppColors.rouge,
),
],
),
const SizedBox(height: 16),
Text(
event.name,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_buildInfoRow(
context,
Icons.calendar_today,
'Date de début',
dateFormat.format(event.startDateTime),
),
_buildInfoRow(
context,
Icons.calendar_today,
'Date de fin',
dateFormat.format(event.endDateTime),
),
_buildInfoRow(
context,
Icons.euro,
'Prix',
currencyFormat.format(event.price),
),
_buildInfoRow(
context,
Icons.build,
'Temps d\'installation',
'${event.installationTime} heures',
),
_buildInfoRow(
context,
Icons.construction,
'Temps de démontage',
'${event.disassemblyTime} heures',
),
const SizedBox(height: 16),
Text(
'Description',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
event.description,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 16),
Text(
'Adresse',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'${event.address.latitude}° N, ${event.address.longitude}° E',
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
),
);
}
Widget _buildInfoRow(
BuildContext context,
IconData icon,
String label,
String value,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Icon(icon, color: AppColors.rouge),
const SizedBox(width: 8),
Text(
'$label : ',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
Text(
value,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
);
}
}
class EventAddDialog extends StatefulWidget {
const EventAddDialog({super.key});
@override
State<EventAddDialog> createState() => _EventAddDialogState();
}
class _EventAddDialogState extends State<EventAddDialog> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController();
final TextEditingController _priceController = TextEditingController();
final TextEditingController _installationController = TextEditingController();
final TextEditingController _disassemblyController = TextEditingController();
final TextEditingController _latitudeController = TextEditingController();
final TextEditingController _longitudeController = TextEditingController();
DateTime? _startDateTime;
DateTime? _endDateTime;
bool _isLoading = false;
String? _error;
String? _success;
@override
void dispose() {
_nameController.dispose();
_descriptionController.dispose();
_priceController.dispose();
_installationController.dispose();
_disassemblyController.dispose();
_latitudeController.dispose();
_longitudeController.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate() ||
_startDateTime == null ||
_endDateTime == null) return;
setState(() {
_isLoading = true;
_error = null;
_success = null;
});
try {
final eventProvider = Provider.of<EventProvider>(context, listen: false);
final newEvent = EventModel(
id: '',
name: _nameController.text.trim(),
description: _descriptionController.text.trim(),
startDateTime: _startDateTime!,
endDateTime: _endDateTime!,
price: double.tryParse(_priceController.text) ?? 0.0,
installationTime: int.tryParse(_installationController.text) ?? 0,
disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0,
eventTypeId: '', // à adapter si tu veux gérer les types
customerId: '', // à adapter si tu veux gérer les clients
address: LatLng(
double.tryParse(_latitudeController.text) ?? 0.0,
double.tryParse(_longitudeController.text) ?? 0.0,
),
workforce: [],
);
await eventProvider.addEvent(newEvent);
setState(() {
_success = "Événement créé avec succès !";
});
Navigator.of(context).pop();
} catch (e) {
setState(() {
_error = "Erreur lors de la création : $e";
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Créer un événement'),
content: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Nom'),
validator: (v) =>
v == null || v.isEmpty ? 'Champ requis' : null,
),
TextFormField(
controller: _descriptionController,
decoration: const InputDecoration(labelText: 'Description'),
maxLines: 2,
),
TextFormField(
controller: _priceController,
decoration: const InputDecoration(labelText: 'Prix (€)'),
keyboardType: TextInputType.number,
),
TextFormField(
controller: _installationController,
decoration:
const InputDecoration(labelText: 'Installation (h)'),
keyboardType: TextInputType.number,
),
TextFormField(
controller: _disassemblyController,
decoration: const InputDecoration(labelText: 'Démontage (h)'),
keyboardType: TextInputType.number,
),
Row(
children: [
Expanded(
child: TextFormField(
controller: _latitudeController,
decoration: const InputDecoration(labelText: 'Latitude'),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 8),
Expanded(
child: TextFormField(
controller: _longitudeController,
decoration: const InputDecoration(labelText: 'Longitude'),
keyboardType: TextInputType.number,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () async {
final picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
final time = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (time != null) {
setState(() {
_startDateTime = DateTime(
picked.year,
picked.month,
picked.day,
time.hour,
time.minute,
);
});
}
}
},
child: Text(_startDateTime == null
? 'Début'
: DateFormat('dd/MM/yyyy HH:mm')
.format(_startDateTime!)),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton(
onPressed: () async {
final picked = await showDatePicker(
context: context,
initialDate: _startDateTime ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime(2030),
);
if (picked != null) {
final time = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (time != null) {
setState(() {
_endDateTime = DateTime(
picked.year,
picked.month,
picked.day,
time.hour,
time.minute,
);
});
}
}
},
child: Text(_endDateTime == null
? 'Fin'
: DateFormat('dd/MM/yyyy HH:mm')
.format(_endDateTime!)),
),
),
],
),
if (_error != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child:
Text(_error!, style: const TextStyle(color: Colors.red)),
),
if (_success != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(_success!,
style: const TextStyle(color: Colors.green)),
),
],
),
),
),
actions: [
TextButton(
onPressed: _isLoading ? null : () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
ElevatedButton(
onPressed: _isLoading ? null : _submit,
child: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Créer'),
),
],
);
}
}

View File

@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
class IntStepperField extends StatelessWidget {
final String label;
final TextEditingController controller;
final int min;
final int max;
const IntStepperField({
super.key,
required this.label,
required this.controller,
this.min = 0,
this.max = 24,
});
@override
Widget build(BuildContext context) {
int value = int.tryParse(controller.text) ?? 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 2.0),
child: Text(
label,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 13,
color: Colors.black87,
fontWeight: FontWeight.w500),
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline, size: 20),
splashRadius: 18,
onPressed: value > min
? () {
value--;
controller.text = value.toString();
(context as Element).markNeedsBuild();
}
: null,
),
SizedBox(
width: 60,
height: 36,
child: TextFormField(
controller: controller,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 15),
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding:
EdgeInsets.symmetric(vertical: 6, horizontal: 6),
),
onChanged: (val) {
int? v = int.tryParse(val);
if (v == null || v < min) {
controller.text = min.toString();
} else if (v > max) {
controller.text = max.toString();
}
},
),
),
IconButton(
icon: const Icon(Icons.add_circle_outline, size: 20),
splashRadius: 18,
onPressed: value < max
? () {
value++;
controller.text = value.toString();
(context as Element).markNeedsBuild();
}
: null,
),
],
),
],
);
}
}

View File

@ -117,7 +117,7 @@ class MainDrawer extends StatelessWidget {
}, },
), ),
PermissionGate( PermissionGate(
requiredPermissions: const [Permission.viewUsers], requiredPermissions: const ['view_all_users'],
child: ListTile( child: ListTile(
leading: const Icon(Icons.group), leading: const Icon(Icons.group),
title: const Text('Gestion des Utilisateurs'), title: const Text('Gestion des Utilisateurs'),

View File

@ -1,172 +0,0 @@
import 'package:flutter/material.dart';
import 'package:em2rp/models/event_model.dart';
import 'package:em2rp/utils/colors.dart';
import 'package:intl/intl.dart';
class EventDetails extends StatelessWidget {
final EventModel event;
final DateTime? selectedDate;
final List<EventModel> events;
final void Function(EventModel, DateTime) onSelectEvent;
const EventDetails({
super.key,
required this.event,
required this.selectedDate,
required this.events,
required this.onSelectEvent,
});
@override
Widget build(BuildContext context) {
final dateFormat = DateFormat('dd/MM/yyyy HH:mm');
final currencyFormat = NumberFormat.currency(locale: 'fr_FR', symbol: '');
final fullDateFormat = DateFormat('EEEE d MMMM y', 'fr_FR');
// Trie les événements par date de début
final sortedEvents = List<EventModel>.from(events)
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
final currentIndex = sortedEvents.indexWhere((e) => e.id == event.id);
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
onPressed: currentIndex > 0
? () {
final prevEvent = sortedEvents[currentIndex - 1];
onSelectEvent(prevEvent, prevEvent.startDateTime);
}
: null,
icon: const Icon(Icons.arrow_back),
color: AppColors.rouge,
),
if (selectedDate != null)
Expanded(
child: Center(
child: Text(
fullDateFormat.format(selectedDate!),
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppColors.rouge,
fontWeight: FontWeight.bold,
),
),
),
),
IconButton(
onPressed: currentIndex < sortedEvents.length - 1
? () {
final nextEvent = sortedEvents[currentIndex + 1];
onSelectEvent(nextEvent, nextEvent.startDateTime);
}
: null,
icon: const Icon(Icons.arrow_forward),
color: AppColors.rouge,
),
],
),
const SizedBox(height: 16),
Text(
event.name,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_buildInfoRow(
context,
Icons.calendar_today,
'Date de début',
dateFormat.format(event.startDateTime),
),
_buildInfoRow(
context,
Icons.calendar_today,
'Date de fin',
dateFormat.format(event.endDateTime),
),
_buildInfoRow(
context,
Icons.euro,
'Prix',
currencyFormat.format(event.price),
),
_buildInfoRow(
context,
Icons.build,
'Temps d\'installation',
'${event.installationTime} heures',
),
_buildInfoRow(
context,
Icons.construction,
'Temps de démontage',
'${event.disassemblyTime} heures',
),
const SizedBox(height: 16),
Text(
'Description',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
event.description,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 16),
Text(
'Adresse',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'${event.address.latitude}° N, ${event.address.longitude}° E',
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
),
);
}
Widget _buildInfoRow(
BuildContext context,
IconData icon,
String label,
String value,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Icon(icon, color: AppColors.rouge),
const SizedBox(width: 8),
Text(
'$label : ',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
Text(
value,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
);
}
}