- Ajout d'une Cloud Function `aiEquipmentProposal` utilisant le modèle Gemini avec function calling pour suggérer du matériel et des containers. - Implémentation de plusieurs outils (tools) côté serveur pour permettre à l'IA d'interagir avec Firestore : `search_equipment`, `check_availability_batch`, `get_past_events`, `search_event_reference` et `search_containers`. - Ajout de la dépendance `@google/generative-ai` dans le backend. - Création d'un service Flutter `AiEquipmentAssistantService` pour communiquer avec la nouvelle Cloud Function. - Ajout d'une interface de dialogue `AiEquipmentAssistantDialog` permettant aux utilisateurs de discuter avec l'IA pour affiner les propositions de matériel. - Intégration de l'assistant IA dans la section de gestion du matériel des événements (`EventAssignedEquipmentSection`). - Mise à jour de `DataService` avec de nouvelles méthodes de recherche et de vérification de disponibilité optimisées pour l'assistant. - Activation du mode développement et configuration des identifiants de test dans `env.dart`. - Optimisation des paramètres de la Cloud Function (timeout de 300s et 1GiB de RAM) pour supporter les traitements IA.
307 lines
11 KiB
Dart
307 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:em2rp/models/event_model.dart';
|
|
import 'package:em2rp/controllers/event_form_controller.dart';
|
|
import 'package:em2rp/views/widgets/event_form/event_basic_info_section.dart';
|
|
import 'package:em2rp/views/widgets/event_form/event_details_section.dart';
|
|
import 'package:em2rp/views/widgets/event_form/event_staff_and_documents_section.dart';
|
|
import 'package:em2rp/views/widgets/event_form/event_assigned_equipment_section.dart';
|
|
import 'package:em2rp/views/widgets/event_form/event_form_actions.dart';
|
|
import 'package:em2rp/views/widgets/inputs/option_selector_widget.dart';
|
|
|
|
class EventAddEditPage extends StatefulWidget {
|
|
final EventModel? event;
|
|
final DateTime? selectedDate;
|
|
|
|
const EventAddEditPage({super.key, this.event, this.selectedDate});
|
|
|
|
@override
|
|
State<EventAddEditPage> createState() => _EventAddEditPageState();
|
|
}
|
|
|
|
class _EventAddEditPageState extends State<EventAddEditPage> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
late EventFormController _controller;
|
|
|
|
bool get isEditMode => widget.event != null;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = EventFormController();
|
|
_controller.initialize(
|
|
existingEvent: widget.event,
|
|
selectedDate: widget.selectedDate,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<bool> _onWillPop() async {
|
|
if (!_controller.formChanged) return true;
|
|
if (!mounted) return true;
|
|
|
|
final shouldLeave = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Quitter la page ?'),
|
|
content: const Text(
|
|
'Les modifications non enregistrées seront perdues. Voulez-vous vraiment quitter ?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
child: const Text('Quitter'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
return shouldLeave ?? false;
|
|
}
|
|
|
|
Future<void> _submit() async {
|
|
if (!_formKey.currentState!.validate()) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Veuillez remplir tous les champs obligatoires.'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
final success =
|
|
await _controller.submitForm(context, existingEvent: widget.event);
|
|
if (success && mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
}
|
|
|
|
Future<void> _deleteEvent() async {
|
|
if (widget.event == null) return;
|
|
|
|
final shouldDelete = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Supprimer l\'événement'),
|
|
content: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text('Êtes-vous sûr de vouloir supprimer cet événement ?'),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Nom : ${widget.event!.name}',
|
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 16),
|
|
const Text(
|
|
'Cette action est irréversible.',
|
|
style: TextStyle(
|
|
color: Colors.red,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
child: const Text('Annuler'),
|
|
),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Colors.red,
|
|
foregroundColor: Colors.white,
|
|
),
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
child: const Text('Supprimer'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (shouldDelete == true) {
|
|
final success = await _controller.deleteEvent(context, widget.event!.id);
|
|
if (success && mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Événement supprimé avec succès'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
Navigator.of(context).pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
|
|
|
return ChangeNotifierProvider.value(
|
|
value: _controller,
|
|
child: PopScope(
|
|
canPop: false,
|
|
onPopInvokedWithResult: (didPop, result) async {
|
|
if (didPop) return;
|
|
final shouldPop = await _onWillPop();
|
|
if (shouldPop && mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
},
|
|
child: Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(
|
|
isEditMode ? 'Modifier un événement' : 'Créer un événement'),
|
|
),
|
|
body: Center(
|
|
child: SingleChildScrollView(
|
|
child: (isMobile
|
|
? Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16, vertical: 12),
|
|
child: _buildFormContent(isMobile),
|
|
)
|
|
: 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: _buildFormContent(isMobile),
|
|
),
|
|
)),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFormContent(bool isMobile) {
|
|
return Consumer<EventFormController>(
|
|
builder: (context, controller, child) {
|
|
return Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
EventBasicInfoSection(
|
|
nameController: controller.nameController,
|
|
basePriceController: controller.basePriceController,
|
|
eventTypes: controller.eventTypes,
|
|
isLoadingEventTypes: controller.isLoadingEventTypes,
|
|
selectedEventTypeId: controller.selectedEventTypeId,
|
|
startDateTime: controller.startDateTime,
|
|
endDateTime: controller.endDateTime,
|
|
onEventTypeChanged: (typeId) =>
|
|
controller.onEventTypeChanged(typeId, context),
|
|
onStartDateTimeChanged: controller.setStartDateTime,
|
|
onEndDateTimeChanged: controller.setEndDateTime,
|
|
onAnyFieldChanged:
|
|
() {}, // Géré automatiquement par le contrôleur
|
|
),
|
|
const SizedBox(height: 16),
|
|
OptionSelectorWidget(
|
|
eventType: controller
|
|
.selectedEventTypeId, // Utilise l'ID au lieu du nom
|
|
selectedOptions: controller.selectedOptions,
|
|
onChanged: controller.setSelectedOptions,
|
|
onRemove: (optionId) {
|
|
final newOptions = List<Map<String, dynamic>>.from(
|
|
controller.selectedOptions);
|
|
newOptions.removeWhere((o) => o['id'] == optionId);
|
|
controller.setSelectedOptions(newOptions);
|
|
},
|
|
eventTypeRequired: controller.selectedEventTypeId == null,
|
|
isMobile: isMobile,
|
|
),
|
|
const SizedBox(height: 16),
|
|
// Section Matériel Assigné
|
|
EventAssignedEquipmentSection(
|
|
assignedEquipment: controller.assignedEquipment,
|
|
assignedContainers: controller.assignedContainers,
|
|
startDate: controller.startDateTime,
|
|
endDate: controller.endDateTime,
|
|
onChanged: controller.setAssignedEquipment,
|
|
eventId: widget.event?.id,
|
|
eventTypeId: controller.selectedEventTypeId,
|
|
),
|
|
const SizedBox(height: 16),
|
|
EventDetailsSection(
|
|
descriptionController: controller.descriptionController,
|
|
installationController: controller.installationController,
|
|
disassemblyController: controller.disassemblyController,
|
|
addressController: controller.addressController,
|
|
jaugeController: controller.jaugeController,
|
|
contactEmailController: controller.contactEmailController,
|
|
contactPhoneController: controller.contactPhoneController,
|
|
isMobile: isMobile,
|
|
onAnyFieldChanged:
|
|
() {}, // Géré automatiquement par le contrôleur
|
|
),
|
|
EventStaffAndDocumentsSection(
|
|
allUsers: controller.allUsers,
|
|
selectedUserIds: controller.selectedUserIds,
|
|
onUserSelectionChanged: controller.setSelectedUserIds,
|
|
isLoadingUsers: controller.isLoadingUsers,
|
|
uploadedFiles: controller.uploadedFiles,
|
|
onFilesChanged: controller.setUploadedFiles,
|
|
isLoading: controller.isLoading,
|
|
error: controller.error,
|
|
success: controller.success,
|
|
isMobile: isMobile,
|
|
onPickAndUploadFiles: controller.pickAndUploadFiles,
|
|
),
|
|
if (controller.error != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 16.0),
|
|
child: Text(
|
|
controller.error!,
|
|
style: const TextStyle(color: Colors.red),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
if (controller.success != null)
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 16.0),
|
|
child: Text(
|
|
controller.success!,
|
|
style: const TextStyle(color: Colors.green),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
EventFormActions(
|
|
isLoading: controller.isLoading,
|
|
isEditMode: isEditMode,
|
|
onCancel: () async {
|
|
final shouldLeave = await _onWillPop();
|
|
if (shouldLeave && mounted) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
},
|
|
onSubmit: _submit,
|
|
onSetConfirmed: !isEditMode ? () {} : null,
|
|
onDelete: isEditMode
|
|
? _deleteEvent
|
|
: null, // Ajout du callback de suppression
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|