V1 calendrier

This commit is contained in:
ElPoyo
2025-10-10 14:58:05 +02:00
parent 080fb7d077
commit aae68f8ab7
26 changed files with 1328 additions and 847 deletions

View File

@@ -81,7 +81,7 @@ class _ForgotPasswordDialogState extends State<ForgotPasswordDialogWidget> {
),
actions: <Widget>[
TextButton(
child: const Text('Terminer'),
child: const Text('Annuler'),
onPressed: () {
Navigator.of(context).pop();
},

View File

@@ -87,13 +87,31 @@ class EventDetails extends StatelessWidget {
),
const SizedBox(height: 16),
Row(
crossAxisAlignment: CrossAxisAlignment.start, // Optionnel mais recommandé pour bien aligner
children: [
SelectableText(
event.name,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
// On remplace le SelectableText par une Column
Expanded( // Utiliser Expanded pour que le texte ne déborde pas
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, // Aligne les textes à gauche
children: [
// 1. Votre titre original
SelectableText(
event.name,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: AppColors.noir,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
event.eventTypeId,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: AppColors.rouge,
),
),
],
),
),
const SizedBox(width: 12),
_buildStatusIcon(event.status),
@@ -119,7 +137,7 @@ class EventDetails extends StatelessWidget {
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: _FirestoreStatusButton(
eventId: event.id,
eventId: event.id,
currentStatus: event.status,
onStatusChanged: (newStatus) async {
await FirebaseFirestore.instance

View File

@@ -0,0 +1,221 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:em2rp/models/event_type_model.dart';
class EventBasicInfoSection extends StatelessWidget {
final TextEditingController nameController;
final TextEditingController basePriceController;
final List<EventType> eventTypes;
final bool isLoadingEventTypes;
final String? selectedEventTypeId;
final DateTime? startDateTime;
final DateTime? endDateTime;
final Function(String?) onEventTypeChanged;
final Function(DateTime?) onStartDateTimeChanged;
final Function(DateTime?) onEndDateTimeChanged;
final VoidCallback onAnyFieldChanged;
const EventBasicInfoSection({
super.key,
required this.nameController,
required this.basePriceController,
required this.eventTypes,
required this.isLoadingEventTypes,
required this.selectedEventTypeId,
required this.startDateTime,
required this.endDateTime,
required this.onEventTypeChanged,
required this.onStartDateTimeChanged,
required this.onEndDateTimeChanged,
required this.onAnyFieldChanged,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildSectionTitle('Informations principales'),
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),
if (isLoadingEventTypes)
const Center(child: CircularProgressIndicator())
else
DropdownButtonFormField<String>(
value: selectedEventTypeId,
items: eventTypes
.map((type) => DropdownMenuItem<String>(
value: type.id,
child: Text(type.name),
))
.toList(),
onChanged: onEventTypeChanged,
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),
_buildDateTimeRow(context),
const SizedBox(height: 16),
TextFormField(
controller: basePriceController,
decoration: const InputDecoration(
labelText: 'Prix de base (€)',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.euro),
hintText: '1050.50',
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}')),
],
validator: (value) {
if (value == null || value.isEmpty) {
return 'Le prix de base est requis';
}
final price = double.tryParse(value.replaceAll(',', '.'));
if (price == null) {
return 'Veuillez entrer un nombre valide';
}
return null;
},
onChanged: (_) => onAnyFieldChanged(),
),
],
);
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(top: 0.0, bottom: 4.0),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
);
}
Widget _buildDateTimeRow(BuildContext context) {
return Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => _selectStartDateTime(context),
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
decoration: const InputDecoration(
labelText: 'Début',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.calendar_today),
suffixIcon: 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: startDateTime == null ? null : () => _selectEndDateTime(context),
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
decoration: const InputDecoration(
labelText: 'Fin',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.calendar_today),
suffixIcon: Icon(Icons.edit_calendar),
),
controller: TextEditingController(
text: endDateTime == null
? ''
: DateFormat('dd/MM/yyyy HH:mm').format(endDateTime!),
),
validator: (v) => endDateTime == null
? 'Champ requis'
: (startDateTime != null &&
endDateTime != null &&
(endDateTime!.isBefore(startDateTime!) ||
endDateTime!.isAtSameMomentAs(startDateTime!)))
? 'La date de fin doit être après la date de début'
: null,
),
),
),
),
],
);
}
Future<void> _selectStartDateTime(BuildContext context) 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) {
final newDateTime = DateTime(
picked.year,
picked.month,
picked.day,
time.hour,
time.minute,
);
onStartDateTimeChanged(newDateTime);
}
}
}
Future<void> _selectEndDateTime(BuildContext context) async {
final picked = await showDatePicker(
context: context,
initialDate: startDateTime!.add(const Duration(hours: 1)),
firstDate: startDateTime!,
lastDate: DateTime(2099),
);
if (picked != null) {
final time = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (time != null) {
final newDateTime = DateTime(
picked.year,
picked.month,
picked.day,
time.hour,
time.minute,
);
onEndDateTimeChanged(newDateTime);
}
}
}
}

View File

@@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
import 'package:em2rp/views/widgets/inputs/int_stepper_field.dart';
class EventDetailsSection extends StatefulWidget {
final TextEditingController descriptionController;
final TextEditingController installationController;
final TextEditingController disassemblyController;
final TextEditingController addressController;
final bool isMobile;
final VoidCallback onAnyFieldChanged;
const EventDetailsSection({
super.key,
required this.descriptionController,
required this.installationController,
required this.disassemblyController,
required this.addressController,
required this.isMobile,
required this.onAnyFieldChanged,
});
@override
State<EventDetailsSection> createState() => _EventDetailsSectionState();
}
class _EventDetailsSectionState extends State<EventDetailsSection> {
int _descriptionMaxLines = 3;
@override
void initState() {
super.initState();
widget.descriptionController.addListener(_handleDescriptionChange);
}
@override
void dispose() {
widget.descriptionController.removeListener(_handleDescriptionChange);
super.dispose();
}
void _handleDescriptionChange() {
final lines = '\n'.allMatches(widget.descriptionController.text).length + 1;
setState(() {
_descriptionMaxLines = lines.clamp(3, 6);
});
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildSectionTitle('Détails'),
AnimatedContainer(
duration: const Duration(milliseconds: 200),
constraints: BoxConstraints(
minHeight: 48,
maxHeight: widget.isMobile ? 48.0 * 20 : 48.0 * 10,
),
child: TextFormField(
controller: widget.descriptionController,
minLines: 1,
maxLines: _descriptionMaxLines > (widget.isMobile ? 20 : 10)
? (widget.isMobile ? 20 : 10)
: _descriptionMaxLines,
decoration: const InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.description),
),
onChanged: (_) => widget.onAnyFieldChanged(),
),
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: IntStepperField(
label: 'Installation (h)',
controller: widget.installationController,
min: 0,
max: 99,
),
),
const SizedBox(width: 16),
Expanded(
child: IntStepperField(
label: 'Démontage (h)',
controller: widget.disassemblyController,
min: 0,
max: 99,
),
),
],
),
_buildSectionTitle('Adresse'),
TextFormField(
controller: widget.addressController,
decoration: const InputDecoration(
labelText: 'Adresse',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.location_on),
),
validator: (v) => v == null || v.isEmpty ? 'Champ requis' : null,
onChanged: (_) => widget.onAnyFieldChanged(),
),
],
);
}
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),
),
),
);
}
}

View File

@@ -0,0 +1,64 @@
import 'package:flutter/material.dart';
class EventFormActions extends StatelessWidget {
final bool isLoading;
final bool isEditMode;
final VoidCallback onCancel;
final VoidCallback onSubmit;
final VoidCallback? onSetConfirmed;
const EventFormActions({
super.key,
required this.isLoading,
required this.isEditMode,
required this.onCancel,
required this.onSubmit,
this.onSetConfirmed,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: isLoading ? null : onCancel,
child: const Text('Annuler'),
),
const SizedBox(width: 8),
ElevatedButton.icon(
icon: const Icon(Icons.check),
onPressed: isLoading ? null : onSubmit,
label: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(isEditMode ? 'Enregistrer' : 'Créer'),
),
],
),
if (!isEditMode && onSetConfirmed != null)
Center(
child: Padding(
padding: const EdgeInsets.only(top: 16.0),
child: ElevatedButton.icon(
icon: const Icon(Icons.check_circle, color: Colors.white),
label: const Text('Définir cet événement comme confirmé'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
textStyle: const TextStyle(fontWeight: FontWeight.bold),
),
onPressed: onSetConfirmed,
),
),
),
],
);
}
}

View File

@@ -0,0 +1,130 @@
import 'package:flutter/material.dart';
import 'package:em2rp/models/user_model.dart';
import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart';
import 'package:em2rp/views/widgets/inputs/dropzone_upload_widget.dart';
import 'package:file_picker/file_picker.dart';
import 'package:firebase_storage/firebase_storage.dart';
class EventStaffAndDocumentsSection extends StatelessWidget {
final List<UserModel> allUsers;
final List<String> selectedUserIds;
final Function(List<String>) onUserSelectionChanged;
final bool isLoadingUsers;
final List<Map<String, String>> uploadedFiles;
final Function(List<Map<String, String>>) onFilesChanged;
final bool isLoading;
final String? error;
final String? success;
final bool isMobile;
final VoidCallback? onPickAndUploadFiles;
const EventStaffAndDocumentsSection({
super.key,
required this.allUsers,
required this.selectedUserIds,
required this.onUserSelectionChanged,
required this.isLoadingUsers,
required this.uploadedFiles,
required this.onFilesChanged,
required this.isLoading,
this.error,
this.success,
required this.isMobile,
this.onPickAndUploadFiles,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildSectionTitle('Personnel'),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: UserMultiSelectWidget(
allUsers: allUsers,
selectedUserIds: selectedUserIds,
onChanged: onUserSelectionChanged,
isLoading: isLoadingUsers,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionTitle('Documents'),
if (isMobile)
_buildMobileDocumentUpload()
else
DropzoneUploadWidget(
uploadedFiles: uploadedFiles,
onFilesChanged: onFilesChanged,
isLoading: isLoading,
error: error,
success: success,
),
],
),
),
],
),
],
);
}
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),
),
),
);
}
Widget _buildMobileDocumentUpload() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.attach_file),
label: const Text('Ajouter un fichier'),
onPressed: isLoading ? null : onPickAndUploadFiles,
),
const SizedBox(height: 8),
...uploadedFiles.map((file) => ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
leading: const Icon(Icons.insert_drive_file),
title: Text(file['name'] ?? ''),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: isLoading
? null
: () {
final newFiles = List<Map<String, String>>.from(uploadedFiles);
newFiles.remove(file);
onFilesChanged(newFiles);
},
),
)),
if (error != null)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(error!, style: const TextStyle(color: Colors.red)),
),
if (success != null)
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(success!, style: const TextStyle(color: Colors.green)),
),
],
);
}
}

View File

@@ -49,16 +49,14 @@ class _DropzoneUploadWidgetState extends State<DropzoneUploadWidget> {
for (final file in files) {
final name = await _dropzoneController!.getFilename(file);
final bytes = await _dropzoneController!.getFileData(file);
if (bytes != null) {
final ref = FirebaseStorage.instance.ref().child(
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$name');
final uploadTask = await ref.putData(bytes);
final url = await uploadTask.ref.getDownloadURL();
if (!newFiles.any((f) => f['name'] == name && f['url'] == url)) {
newFiles.add({'name': name, 'url': url});
}
final ref = FirebaseStorage.instance.ref().child(
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$name');
final uploadTask = await ref.putData(bytes);
final url = await uploadTask.ref.getDownloadURL();
if (!newFiles.any((f) => f['name'] == name && f['url'] == url)) {
newFiles.add({'name': name, 'url': url});
}
}
}
widget.onFilesChanged(newFiles);
setState(() {
_success = "Fichier(s) ajouté(s) !";
@@ -233,7 +231,7 @@ class _DropzoneUploadWidgetState extends State<DropzoneUploadWidget> {
contentPadding: EdgeInsets.zero,
dense: true,
);
}).toList(),
}),
SizedBox(
width: 160,
child: ElevatedButton.icon(

View File

@@ -27,7 +27,7 @@ class OptionSelectorWidget extends StatefulWidget {
class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
List<EventOption> _allOptions = [];
bool _loading = true;
String _search = '';
final String _search = '';
final List<String> _eventTypes = ['Bal', 'Mariage', 'Anniversaire'];
@override
@@ -201,8 +201,7 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> {
final opt = filtered[i];
return ListTile(
title: Text(opt.name),
subtitle: Text(opt.details +
'\nFourchette: ${opt.valMin}€ ~ ${opt.valMax}'),
subtitle: Text('${opt.details}\nFourchette: ${opt.valMin}€ ~ ${opt.valMax}'),
onTap: () async {
final min = opt.valMin;
final max = opt.valMax;
@@ -304,7 +303,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
final _detailsController = TextEditingController();
final _minPriceController = TextEditingController();
final _maxPriceController = TextEditingController();
List<String> _selectedTypes = [];
final List<String> _selectedTypes = [];
final List<String> _allTypes = ['Bal', 'Mariage', 'Anniversaire'];
String? _error;
bool _checkingName = false;

View File

@@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
import 'package:em2rp/views/widgets/image/profile_picture.dart';
import 'package:provider/provider.dart';
import 'package:em2rp/utils/permission_gate.dart';
import 'package:em2rp/models/role_model.dart';
class MainDrawer extends StatelessWidget {
final String currentPage;

View File

@@ -129,7 +129,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
isLoadingRoles
? const CircularProgressIndicator()
: DropdownButtonFormField<String>(
value: selectedRoleId,
initialValue: selectedRoleId,
decoration: _buildInputDecoration(
'Rôle', Icons.admin_panel_settings_outlined),
items: availableRoles.map((role) {

View File

@@ -38,7 +38,6 @@ class _UserMultiSelect extends StatefulWidget {
final ValueChanged<List<String>> onChanged;
const _UserMultiSelect({
super.key,
required this.allUsers,
required this.selectedUserIds,
required this.onChanged,