V1 calendrier
This commit is contained in:
221
em2rp/lib/views/widgets/event_form/event_basic_info_section.dart
Normal file
221
em2rp/lib/views/widgets/event_form/event_basic_info_section.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
123
em2rp/lib/views/widgets/event_form/event_details_section.dart
Normal file
123
em2rp/lib/views/widgets/event_form/event_details_section.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
64
em2rp/lib/views/widgets/event_form/event_form_actions.dart
Normal file
64
em2rp/lib/views/widgets/event_form/event_form_actions.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user