Modifications des permissions, ajout Presta OK, vue calendrier ok
This commit is contained in:
420
em2rp/lib/views/widgets/calendar_widgets/event_details.dart
Normal file
420
em2rp/lib/views/widgets/calendar_widgets/event_details.dart
Normal 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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
71
em2rp/lib/views/widgets/custom_app_bar.dart
Normal file
71
em2rp/lib/views/widgets/custom_app_bar.dart
Normal file
@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/providers/local_user_provider.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
|
||||
class CustomAppBar extends StatefulWidget implements PreferredSizeWidget {
|
||||
final String title;
|
||||
final List<Widget>? actions;
|
||||
final bool showLogoutButton;
|
||||
|
||||
const CustomAppBar({
|
||||
super.key,
|
||||
required this.title,
|
||||
this.actions,
|
||||
this.showLogoutButton = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomAppBar> createState() => _CustomAppBarState();
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
|
||||
class _CustomAppBarState extends State<CustomAppBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
title: Text(widget.title),
|
||||
backgroundColor: AppColors.rouge,
|
||||
actions: [
|
||||
if (widget.showLogoutButton)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout, color: AppColors.blanc),
|
||||
onPressed: () async {
|
||||
// Afficher une boîte de dialogue de confirmation
|
||||
final shouldLogout = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Déconnexion'),
|
||||
content:
|
||||
const Text('Voulez-vous vraiment vous déconnecter ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Déconnexion'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (shouldLogout == true && context.mounted) {
|
||||
// Déconnexion
|
||||
final provider =
|
||||
Provider.of<LocalUserProvider>(context, listen: false);
|
||||
await provider.signOut();
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pushReplacementNamed('/login');
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
if (widget.actions != null) ...widget.actions!,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
87
em2rp/lib/views/widgets/inputs/int_stepper_field.dart
Normal file
87
em2rp/lib/views/widgets/inputs/int_stepper_field.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -117,7 +117,7 @@ class MainDrawer extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
PermissionGate(
|
||||
requiredPermissions: const [Permission.viewUsers],
|
||||
requiredPermissions: const ['view_all_users'],
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.group),
|
||||
title: const Text('Gestion des Utilisateurs'),
|
||||
|
Reference in New Issue
Block a user