V1 calendrier
This commit is contained in:
@@ -34,5 +34,13 @@
|
|||||||
"*.local"
|
"*.local"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"hosting": {
|
||||||
|
"public": "build/web",
|
||||||
|
"ignore": [
|
||||||
|
"firebase.json",
|
||||||
|
"**/.*",
|
||||||
|
"**/node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
|
|
||||||
const {onRequest} = require("firebase-functions/v2/https");
|
const {onRequest} = require("firebase-functions/v2/https");
|
||||||
const logger = require("firebase-functions/logger");
|
const logger = require("firebase-functions/logger");
|
||||||
|
const admin = require('firebase-admin');
|
||||||
|
const { Storage } = require('@google-cloud/storage');
|
||||||
|
|
||||||
|
admin.initializeApp();
|
||||||
|
const storage = new Storage();
|
||||||
|
|
||||||
// Create and deploy your first functions
|
// Create and deploy your first functions
|
||||||
// https://firebase.google.com/docs/functions/get-started
|
// https://firebase.google.com/docs/functions/get-started
|
||||||
@@ -18,22 +23,12 @@ const logger = require("firebase-functions/logger");
|
|||||||
// response.send("Hello from Firebase!");
|
// response.send("Hello from Firebase!");
|
||||||
// });
|
// });
|
||||||
|
|
||||||
const functions = require('firebase-functions');
|
|
||||||
const admin = require('firebase-admin');
|
|
||||||
const { Storage } = require('@google-cloud/storage');
|
|
||||||
admin.initializeApp();
|
|
||||||
const storage = new Storage();
|
|
||||||
|
|
||||||
// Nouvelle version HTTP sécurisée
|
// Nouvelle version HTTP sécurisée
|
||||||
exports.moveEventFileV2 = functions.https.onRequest(async (req, res) => {
|
exports.moveEventFileV2 = onRequest({cors: true}, async (req, res) => {
|
||||||
// Ajout des headers CORS
|
// La gestion CORS est maintenant gérée par l'option {cors: true}
|
||||||
res.set('Access-Control-Allow-Origin', '*');
|
// La vérification pour les requêtes OPTIONS n'est plus nécessaire
|
||||||
res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
||||||
if (req.method === 'OPTIONS') {
|
|
||||||
res.set('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
||||||
res.status(204).send('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Vérification du token Firebase dans l'en-tête Authorization
|
// Vérification du token Firebase dans l'en-tête Authorization
|
||||||
let uid = null;
|
let uid = null;
|
||||||
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
|
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
|
||||||
@@ -42,11 +37,13 @@ exports.moveEventFileV2 = functions.https.onRequest(async (req, res) => {
|
|||||||
const decodedToken = await admin.auth().verifyIdToken(idToken);
|
const decodedToken = await admin.auth().verifyIdToken(idToken);
|
||||||
uid = decodedToken.uid;
|
uid = decodedToken.uid;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(401).json({ error: 'Invalid token' });
|
logger.error("Error while verifying Firebase ID token:", e);
|
||||||
|
res.status(401).json({ error: 'Unauthorized: Invalid token' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(401).json({ error: 'No token provided' });
|
logger.warn("No Firebase ID token was passed as a Bearer token in the Authorization header.");
|
||||||
|
res.status(401).json({ error: 'Unauthorized: No token provided' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +65,7 @@ exports.moveEventFileV2 = functions.https.onRequest(async (req, res) => {
|
|||||||
});
|
});
|
||||||
res.status(200).json({ url });
|
res.status(200).json({ url });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logger.error("Error moving file:", error);
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
class Env {
|
class Env {
|
||||||
static const bool isDevelopment = true;
|
static const bool isDevelopment = false;
|
||||||
|
|
||||||
// Configuration de l'auto-login en développement
|
// Configuration de l'auto-login en développement
|
||||||
static const String devAdminEmail = 'paul.fournel@em2events.fr';
|
static const String devAdminEmail = 'paul.fournel@em2events.fr';
|
||||||
|
|||||||
386
em2rp/lib/controllers/event_form_controller.dart
Normal file
386
em2rp/lib/controllers/event_form_controller.dart
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:em2rp/models/event_model.dart';
|
||||||
|
import 'package:em2rp/models/event_type_model.dart';
|
||||||
|
import 'package:em2rp/models/user_model.dart';
|
||||||
|
import 'package:em2rp/services/event_form_service.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:em2rp/providers/event_provider.dart';
|
||||||
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
|
|
||||||
|
class EventFormController extends ChangeNotifier {
|
||||||
|
// Controllers
|
||||||
|
final TextEditingController nameController = TextEditingController();
|
||||||
|
final TextEditingController descriptionController = TextEditingController();
|
||||||
|
final TextEditingController basePriceController = TextEditingController();
|
||||||
|
final TextEditingController installationController = TextEditingController();
|
||||||
|
final TextEditingController disassemblyController = TextEditingController();
|
||||||
|
final TextEditingController addressController = TextEditingController();
|
||||||
|
|
||||||
|
// State variables
|
||||||
|
DateTime? _startDateTime;
|
||||||
|
DateTime? _endDateTime;
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _error;
|
||||||
|
String? _success;
|
||||||
|
String? _selectedEventTypeId;
|
||||||
|
List<EventType> _eventTypes = [];
|
||||||
|
bool _isLoadingEventTypes = true;
|
||||||
|
List<String> _selectedUserIds = [];
|
||||||
|
List<UserModel> _allUsers = [];
|
||||||
|
bool _isLoadingUsers = true;
|
||||||
|
List<Map<String, String>> _uploadedFiles = [];
|
||||||
|
List<Map<String, dynamic>> _selectedOptions = [];
|
||||||
|
bool _formChanged = false;
|
||||||
|
EventStatus _selectedStatus = EventStatus.waitingForApproval;
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
DateTime? get startDateTime => _startDateTime;
|
||||||
|
DateTime? get endDateTime => _endDateTime;
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
String? get error => _error;
|
||||||
|
String? get success => _success;
|
||||||
|
String? get selectedEventTypeId => _selectedEventTypeId;
|
||||||
|
List<EventType> get eventTypes => _eventTypes;
|
||||||
|
bool get isLoadingEventTypes => _isLoadingEventTypes;
|
||||||
|
List<String> get selectedUserIds => _selectedUserIds;
|
||||||
|
List<UserModel> get allUsers => _allUsers;
|
||||||
|
bool get isLoadingUsers => _isLoadingUsers;
|
||||||
|
List<Map<String, String>> get uploadedFiles => _uploadedFiles;
|
||||||
|
List<Map<String, dynamic>> get selectedOptions => _selectedOptions;
|
||||||
|
bool get formChanged => _formChanged;
|
||||||
|
EventStatus get selectedStatus => _selectedStatus;
|
||||||
|
|
||||||
|
EventFormController() {
|
||||||
|
_setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setupListeners() {
|
||||||
|
nameController.addListener(_onAnyFieldChanged);
|
||||||
|
basePriceController.addListener(_onAnyFieldChanged);
|
||||||
|
installationController.addListener(_onAnyFieldChanged);
|
||||||
|
disassemblyController.addListener(_onAnyFieldChanged);
|
||||||
|
addressController.addListener(_onAnyFieldChanged);
|
||||||
|
descriptionController.addListener(_onAnyFieldChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAnyFieldChanged() {
|
||||||
|
if (!_formChanged) {
|
||||||
|
_formChanged = true;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> initialize([EventModel? existingEvent]) async {
|
||||||
|
await Future.wait([
|
||||||
|
_fetchUsers(),
|
||||||
|
_fetchEventTypes(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (existingEvent != null) {
|
||||||
|
_populateFromEvent(existingEvent);
|
||||||
|
} else {
|
||||||
|
_selectedStatus = EventStatus.waitingForApproval;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _populateFromEvent(EventModel event) {
|
||||||
|
nameController.text = event.name;
|
||||||
|
descriptionController.text = event.description;
|
||||||
|
basePriceController.text = event.basePrice.toStringAsFixed(2);
|
||||||
|
installationController.text = event.installationTime.toString();
|
||||||
|
disassemblyController.text = event.disassemblyTime.toString();
|
||||||
|
addressController.text = event.address;
|
||||||
|
_startDateTime = event.startDateTime;
|
||||||
|
_endDateTime = event.endDateTime;
|
||||||
|
_selectedEventTypeId = event.eventTypeId.isNotEmpty ? event.eventTypeId : null;
|
||||||
|
_selectedUserIds = event.workforce.map((ref) => ref.id).toList();
|
||||||
|
_uploadedFiles = List<Map<String, String>>.from(event.documents);
|
||||||
|
_selectedOptions = List<Map<String, dynamic>>.from(event.options);
|
||||||
|
_selectedStatus = event.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchUsers() async {
|
||||||
|
try {
|
||||||
|
_allUsers = await EventFormService.fetchUsers();
|
||||||
|
_isLoadingUsers = false;
|
||||||
|
} catch (e) {
|
||||||
|
_error = e.toString();
|
||||||
|
_isLoadingUsers = false;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchEventTypes() async {
|
||||||
|
try {
|
||||||
|
_eventTypes = await EventFormService.fetchEventTypes();
|
||||||
|
_isLoadingEventTypes = false;
|
||||||
|
} catch (e) {
|
||||||
|
_error = e.toString();
|
||||||
|
_isLoadingEventTypes = false;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStartDateTime(DateTime? dateTime) {
|
||||||
|
_startDateTime = dateTime;
|
||||||
|
if (_endDateTime != null &&
|
||||||
|
dateTime != null &&
|
||||||
|
(_endDateTime!.isBefore(dateTime) || _endDateTime!.isAtSameMomentAs(dateTime))) {
|
||||||
|
_endDateTime = null;
|
||||||
|
}
|
||||||
|
_onAnyFieldChanged();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setEndDateTime(DateTime? dateTime) {
|
||||||
|
_endDateTime = dateTime;
|
||||||
|
_onAnyFieldChanged();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onEventTypeChanged(String? newTypeId, BuildContext context) {
|
||||||
|
if (newTypeId == _selectedEventTypeId) return;
|
||||||
|
|
||||||
|
final oldEventTypeIndex = _selectedEventTypeId != null
|
||||||
|
? _eventTypes.indexWhere((et) => et.id == _selectedEventTypeId)
|
||||||
|
: -1;
|
||||||
|
final EventType? oldEventType = oldEventTypeIndex != -1 ? _eventTypes[oldEventTypeIndex] : null;
|
||||||
|
|
||||||
|
_selectedEventTypeId = newTypeId;
|
||||||
|
|
||||||
|
if (newTypeId != null) {
|
||||||
|
final selectedType = _eventTypes.firstWhere((et) => et.id == newTypeId);
|
||||||
|
final defaultPrice = selectedType.defaultPrice;
|
||||||
|
final currentPrice = double.tryParse(basePriceController.text.replaceAll(',', '.'));
|
||||||
|
final oldDefaultPrice = oldEventType?.defaultPrice;
|
||||||
|
|
||||||
|
if (basePriceController.text.isEmpty ||
|
||||||
|
(currentPrice != null && oldDefaultPrice != null && currentPrice == oldDefaultPrice)) {
|
||||||
|
basePriceController.text = defaultPrice.toStringAsFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
final before = _selectedOptions.length;
|
||||||
|
_selectedOptions.removeWhere((opt) {
|
||||||
|
final types = opt['compatibleTypes'] as List<dynamic>?;
|
||||||
|
if (types == null) return true;
|
||||||
|
return !types.contains(selectedType.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (_selectedOptions.length < before) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Certaines options ont été retirées car non compatibles avec "${selectedType.name}".')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_selectedOptions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onAnyFieldChanged();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSelectedUserIds(List<String> userIds) {
|
||||||
|
_selectedUserIds = userIds;
|
||||||
|
_onAnyFieldChanged();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setUploadedFiles(List<Map<String, String>> files) {
|
||||||
|
_uploadedFiles = files;
|
||||||
|
_onAnyFieldChanged();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSelectedOptions(List<Map<String, dynamic>> options) {
|
||||||
|
_selectedOptions = options;
|
||||||
|
_onAnyFieldChanged();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pickAndUploadFiles() async {
|
||||||
|
final result = await FilePicker.platform.pickFiles(allowMultiple: true, withData: true);
|
||||||
|
if (result != null && result.files.isNotEmpty) {
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final files = await EventFormService.uploadFiles(result.files);
|
||||||
|
_uploadedFiles.addAll(files);
|
||||||
|
_onAnyFieldChanged();
|
||||||
|
} catch (e) {
|
||||||
|
_error = 'Erreur lors de l\'upload : $e';
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool validateForm() {
|
||||||
|
return nameController.text.isNotEmpty &&
|
||||||
|
_startDateTime != null &&
|
||||||
|
_endDateTime != null &&
|
||||||
|
_selectedEventTypeId != null &&
|
||||||
|
addressController.text.isNotEmpty &&
|
||||||
|
(_endDateTime!.isAfter(_startDateTime!));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> submitForm(BuildContext context, {EventModel? existingEvent}) async {
|
||||||
|
if (!validateForm()) {
|
||||||
|
_error = "Veuillez remplir tous les champs obligatoires.";
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading = true;
|
||||||
|
_error = null;
|
||||||
|
_success = null;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final eventTypeRef = _selectedEventTypeId != null
|
||||||
|
? FirebaseFirestore.instance.collection('eventTypes').doc(_selectedEventTypeId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (existingEvent != null) {
|
||||||
|
// Mode édition
|
||||||
|
// Gérer les nouveaux fichiers uploadés s'il y en a
|
||||||
|
List<Map<String, String>> finalDocuments = List<Map<String, String>>.from(_uploadedFiles);
|
||||||
|
|
||||||
|
// Identifier les nouveaux fichiers (ceux qui ont une URL temp)
|
||||||
|
final newFiles = _uploadedFiles.where((file) =>
|
||||||
|
file['url']?.contains('events/temp/') ?? false).toList();
|
||||||
|
|
||||||
|
if (newFiles.isNotEmpty) {
|
||||||
|
// Déplacer les nouveaux fichiers vers le dossier de l'événement
|
||||||
|
final movedFiles = await EventFormService.moveFilesToEvent(newFiles, existingEvent.id);
|
||||||
|
|
||||||
|
// Remplacer les URLs temporaires par les nouvelles URLs
|
||||||
|
for (int i = 0; i < finalDocuments.length; i++) {
|
||||||
|
final tempFile = finalDocuments[i];
|
||||||
|
final movedFile = movedFiles.firstWhere(
|
||||||
|
(moved) => moved['name'] == tempFile['name'],
|
||||||
|
orElse: () => tempFile,
|
||||||
|
);
|
||||||
|
finalDocuments[i] = movedFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final updatedEvent = EventModel(
|
||||||
|
id: existingEvent.id,
|
||||||
|
name: nameController.text.trim(),
|
||||||
|
description: descriptionController.text.trim(),
|
||||||
|
startDateTime: _startDateTime!,
|
||||||
|
endDateTime: _endDateTime!,
|
||||||
|
basePrice: double.tryParse(basePriceController.text.replaceAll(',', '.')) ?? 0.0,
|
||||||
|
installationTime: int.tryParse(installationController.text) ?? 0,
|
||||||
|
disassemblyTime: int.tryParse(disassemblyController.text) ?? 0,
|
||||||
|
eventTypeId: _selectedEventTypeId!,
|
||||||
|
eventTypeRef: eventTypeRef,
|
||||||
|
customerId: existingEvent.customerId,
|
||||||
|
address: addressController.text.trim(),
|
||||||
|
workforce: _selectedUserIds
|
||||||
|
.map((id) => FirebaseFirestore.instance.collection('users').doc(id))
|
||||||
|
.toList(),
|
||||||
|
latitude: existingEvent.latitude,
|
||||||
|
longitude: existingEvent.longitude,
|
||||||
|
documents: finalDocuments,
|
||||||
|
options: _selectedOptions,
|
||||||
|
status: _selectedStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
await EventFormService.updateEvent(updatedEvent);
|
||||||
|
|
||||||
|
// Recharger les événements après modification
|
||||||
|
final localUserProvider = Provider.of<LocalUserProvider>(context, listen: false);
|
||||||
|
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||||
|
final userId = localUserProvider.uid;
|
||||||
|
final canViewAllEvents = localUserProvider.hasPermission('view_all_events');
|
||||||
|
|
||||||
|
if (userId != null) {
|
||||||
|
await eventProvider.loadUserEvents(userId, canViewAllEvents: canViewAllEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
_success = "Événement modifié avec succès !";
|
||||||
|
} else {
|
||||||
|
// Mode création
|
||||||
|
final newEvent = EventModel(
|
||||||
|
id: '',
|
||||||
|
name: nameController.text.trim(),
|
||||||
|
description: descriptionController.text.trim(),
|
||||||
|
startDateTime: _startDateTime!,
|
||||||
|
endDateTime: _endDateTime!,
|
||||||
|
basePrice: double.tryParse(basePriceController.text.replaceAll(',', '.')) ?? 0.0,
|
||||||
|
installationTime: int.tryParse(installationController.text) ?? 0,
|
||||||
|
disassemblyTime: int.tryParse(disassemblyController.text) ?? 0,
|
||||||
|
eventTypeId: _selectedEventTypeId!,
|
||||||
|
eventTypeRef: eventTypeRef,
|
||||||
|
customerId: '',
|
||||||
|
address: addressController.text.trim(),
|
||||||
|
workforce: _selectedUserIds
|
||||||
|
.map((id) => FirebaseFirestore.instance.collection('users').doc(id))
|
||||||
|
.toList(),
|
||||||
|
latitude: 0.0,
|
||||||
|
longitude: 0.0,
|
||||||
|
documents: _uploadedFiles,
|
||||||
|
options: _selectedOptions,
|
||||||
|
status: _selectedStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
final eventId = await EventFormService.createEvent(newEvent);
|
||||||
|
final newFiles = await EventFormService.moveFilesToEvent(_uploadedFiles, eventId);
|
||||||
|
await EventFormService.updateEventDocuments(eventId, newFiles);
|
||||||
|
|
||||||
|
// Reload events
|
||||||
|
final localUserProvider = Provider.of<LocalUserProvider>(context, listen: false);
|
||||||
|
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||||
|
final userId = localUserProvider.uid;
|
||||||
|
final canViewAllEvents = localUserProvider.hasPermission('view_all_events');
|
||||||
|
|
||||||
|
if (userId != null) {
|
||||||
|
await eventProvider.loadUserEvents(userId, canViewAllEvents: canViewAllEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
_success = "Événement créé avec succès !";
|
||||||
|
}
|
||||||
|
|
||||||
|
_formChanged = false;
|
||||||
|
notifyListeners();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
_error = "Erreur lors de la sauvegarde : $e";
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearError() {
|
||||||
|
_error = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearSuccess() {
|
||||||
|
_success = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
nameController.dispose();
|
||||||
|
descriptionController.dispose();
|
||||||
|
basePriceController.dispose();
|
||||||
|
installationController.dispose();
|
||||||
|
disassemblyController.dispose();
|
||||||
|
addressController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
|
||||||
|
|
||||||
enum EventStatus {
|
enum EventStatus {
|
||||||
confirmed,
|
confirmed,
|
||||||
@@ -41,6 +40,7 @@ class EventModel {
|
|||||||
final int installationTime;
|
final int installationTime;
|
||||||
final int disassemblyTime;
|
final int disassemblyTime;
|
||||||
final String eventTypeId;
|
final String eventTypeId;
|
||||||
|
final DocumentReference? eventTypeRef;
|
||||||
final String customerId;
|
final String customerId;
|
||||||
final String address;
|
final String address;
|
||||||
final double latitude;
|
final double latitude;
|
||||||
@@ -60,6 +60,7 @@ class EventModel {
|
|||||||
required this.installationTime,
|
required this.installationTime,
|
||||||
required this.disassemblyTime,
|
required this.disassemblyTime,
|
||||||
required this.eventTypeId,
|
required this.eventTypeId,
|
||||||
|
this.eventTypeRef,
|
||||||
required this.customerId,
|
required this.customerId,
|
||||||
required this.address,
|
required this.address,
|
||||||
required this.latitude,
|
required this.latitude,
|
||||||
@@ -79,7 +80,7 @@ class EventModel {
|
|||||||
final docs = docsRaw is List
|
final docs = docsRaw is List
|
||||||
? docsRaw.map<Map<String, String>>((e) {
|
? docsRaw.map<Map<String, String>>((e) {
|
||||||
if (e is Map) {
|
if (e is Map) {
|
||||||
return Map<String, String>.from(e as Map);
|
return Map<String, String>.from(e);
|
||||||
} else if (e is String) {
|
} else if (e is String) {
|
||||||
final fileName = Uri.decodeComponent(
|
final fileName = Uri.decodeComponent(
|
||||||
e.split('/').last.split('?').first,
|
e.split('/').last.split('?').first,
|
||||||
@@ -94,7 +95,7 @@ class EventModel {
|
|||||||
final options = optionsRaw is List
|
final options = optionsRaw is List
|
||||||
? optionsRaw.map<Map<String, dynamic>>((e) {
|
? optionsRaw.map<Map<String, dynamic>>((e) {
|
||||||
if (e is Map) {
|
if (e is Map) {
|
||||||
return Map<String, dynamic>.from(e as Map);
|
return Map<String, dynamic>.from(e);
|
||||||
} else {
|
} else {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -112,7 +113,10 @@ class EventModel {
|
|||||||
disassemblyTime: map['DisassemblyTime'] ?? 0,
|
disassemblyTime: map['DisassemblyTime'] ?? 0,
|
||||||
eventTypeId: map['EventType'] is DocumentReference
|
eventTypeId: map['EventType'] is DocumentReference
|
||||||
? (map['EventType'] as DocumentReference).id
|
? (map['EventType'] as DocumentReference).id
|
||||||
: '',
|
: map['EventType'] ?? '',
|
||||||
|
eventTypeRef: map['EventType'] is DocumentReference
|
||||||
|
? map['EventType'] as DocumentReference
|
||||||
|
: null,
|
||||||
customerId: map['customer'] is DocumentReference
|
customerId: map['customer'] is DocumentReference
|
||||||
? (map['customer'] as DocumentReference).id
|
? (map['customer'] as DocumentReference).id
|
||||||
: '',
|
: '',
|
||||||
@@ -135,8 +139,8 @@ class EventModel {
|
|||||||
'BasePrice': basePrice,
|
'BasePrice': basePrice,
|
||||||
'InstallationTime': installationTime,
|
'InstallationTime': installationTime,
|
||||||
'DisassemblyTime': disassemblyTime,
|
'DisassemblyTime': disassemblyTime,
|
||||||
'EventType': eventTypeId,
|
'EventType': eventTypeId.isNotEmpty ? FirebaseFirestore.instance.collection('eventTypes').doc(eventTypeId) : null,
|
||||||
'customer': customerId,
|
'customer': customerId.isNotEmpty ? FirebaseFirestore.instance.collection('customers').doc(customerId) : null,
|
||||||
'Address': address,
|
'Address': address,
|
||||||
'Position': GeoPoint(latitude, longitude),
|
'Position': GeoPoint(latitude, longitude),
|
||||||
'Latitude': latitude,
|
'Latitude': latitude,
|
||||||
|
|||||||
32
em2rp/lib/models/event_type_model.dart
Normal file
32
em2rp/lib/models/event_type_model.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
|
||||||
|
class EventType {
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final double defaultPrice;
|
||||||
|
|
||||||
|
EventType({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.defaultPrice,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory EventType.fromFirestore(DocumentSnapshot doc) {
|
||||||
|
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
|
||||||
|
|
||||||
|
double price = 0.0;
|
||||||
|
final priceData = data['defaultPrice'];
|
||||||
|
if (priceData is num) {
|
||||||
|
price = priceData.toDouble();
|
||||||
|
} else if (priceData is String) {
|
||||||
|
price = double.tryParse(priceData.replaceAll(',', '.')) ?? 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EventType(
|
||||||
|
id: doc.id,
|
||||||
|
name: data['name'] ?? '',
|
||||||
|
defaultPrice: price,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
||||||
|
|
||||||
class RoleModel {
|
class RoleModel {
|
||||||
final String id;
|
final String id;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import '../models/user_model.dart';
|
|||||||
import '../services/user_service.dart';
|
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:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:em2rp/providers/local_user_provider.dart';
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
|
|
||||||
@@ -130,7 +129,7 @@ class UsersProvider with ChangeNotifier {
|
|||||||
await _auth.sendPasswordResetEmail(
|
await _auth.sendPasswordResetEmail(
|
||||||
email: user.email,
|
email: user.email,
|
||||||
actionCodeSettings: ActionCodeSettings(
|
actionCodeSettings: ActionCodeSettings(
|
||||||
url: 'http://localhost:63337/finishSignUp?email=${user.email}',
|
url: 'http://app.em2events.fr/finishSignUp?email=${user.email}',
|
||||||
handleCodeInApp: true,
|
handleCodeInApp: true,
|
||||||
androidPackageName: 'com.em2rp.app',
|
androidPackageName: 'com.em2rp.app',
|
||||||
androidInstallApp: true,
|
androidInstallApp: true,
|
||||||
|
|||||||
143
em2rp/lib/services/event_form_service.dart
Normal file
143
em2rp/lib/services/event_form_service.dart
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import 'package:firebase_storage/firebase_storage.dart';
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'package:em2rp/models/event_model.dart';
|
||||||
|
import 'package:em2rp/models/event_type_model.dart';
|
||||||
|
import 'package:em2rp/models/user_model.dart';
|
||||||
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
|
class EventFormService {
|
||||||
|
static Future<List<EventType>> fetchEventTypes() async {
|
||||||
|
developer.log('Fetching event types from Firestore...', name: 'EventFormService');
|
||||||
|
try {
|
||||||
|
final snapshot = await FirebaseFirestore.instance.collection('eventTypes').get();
|
||||||
|
final eventTypes = snapshot.docs.map((doc) => EventType.fromFirestore(doc)).toList();
|
||||||
|
developer.log('${eventTypes.length} event types loaded.', name: 'EventFormService');
|
||||||
|
return eventTypes;
|
||||||
|
} catch (e, s) {
|
||||||
|
developer.log('Error fetching event types', name: 'EventFormService', error: e, stackTrace: s);
|
||||||
|
throw Exception("Could not load event types. Please check Firestore permissions.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<UserModel>> fetchUsers() async {
|
||||||
|
try {
|
||||||
|
final snapshot = await FirebaseFirestore.instance.collection('users').get();
|
||||||
|
return snapshot.docs.map((doc) => UserModel.fromMap(doc.data(), doc.id)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
developer.log('Error fetching users', name: 'EventFormService', error: e);
|
||||||
|
throw Exception("Could not load users.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Map<String, String>>> uploadFiles(List<PlatformFile> files) async {
|
||||||
|
List<Map<String, String>> uploadedFiles = [];
|
||||||
|
|
||||||
|
for (final file in files) {
|
||||||
|
final fileBytes = file.bytes;
|
||||||
|
final fileName = file.name;
|
||||||
|
|
||||||
|
if (fileBytes != null) {
|
||||||
|
final ref = FirebaseStorage.instance.ref().child(
|
||||||
|
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$fileName');
|
||||||
|
final uploadTask = await ref.putData(fileBytes);
|
||||||
|
final url = await uploadTask.ref.getDownloadURL();
|
||||||
|
uploadedFiles.add({'name': fileName, 'url': url});
|
||||||
|
} else {
|
||||||
|
throw Exception("Impossible de lire le fichier $fileName");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uploadedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String?> moveEventFileHttp({
|
||||||
|
required String sourcePath,
|
||||||
|
required String destinationPath,
|
||||||
|
}) async {
|
||||||
|
final url = Uri.parse('https://us-central1-em2rp-951dc.cloudfunctions.net/moveEventFileV2');
|
||||||
|
final user = FirebaseAuth.instance.currentUser;
|
||||||
|
final idToken = await user?.getIdToken();
|
||||||
|
|
||||||
|
final response = await http.post(
|
||||||
|
url,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
if (idToken != null) 'Authorization': 'Bearer $idToken',
|
||||||
|
},
|
||||||
|
body: jsonEncode({
|
||||||
|
'data': {
|
||||||
|
'sourcePath': sourcePath,
|
||||||
|
'destinationPath': destinationPath,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
|
if (data['url'] != null) {
|
||||||
|
return data['url'] as String;
|
||||||
|
} else if (data['result'] != null && data['result']['url'] != null) {
|
||||||
|
return data['result']['url'] as String;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
print('Erreur Cloud Function: \n${response.body}');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String> createEvent(EventModel event) async {
|
||||||
|
final docRef = await FirebaseFirestore.instance.collection('events').add(event.toMap());
|
||||||
|
return docRef.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> updateEvent(EventModel event) async {
|
||||||
|
final docRef = FirebaseFirestore.instance.collection('events').doc(event.id);
|
||||||
|
await docRef.update(event.toMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<Map<String, String>>> moveFilesToEvent(
|
||||||
|
List<Map<String, String>> tempFiles, String eventId) async {
|
||||||
|
List<Map<String, String>> newFiles = [];
|
||||||
|
|
||||||
|
for (final file in tempFiles) {
|
||||||
|
final fileName = file['name']!;
|
||||||
|
final oldUrl = file['url']!;
|
||||||
|
|
||||||
|
String sourcePath;
|
||||||
|
final tempPattern = RegExp(r'events/temp/[^?]+');
|
||||||
|
final match = tempPattern.firstMatch(oldUrl);
|
||||||
|
if (match != null) {
|
||||||
|
sourcePath = match.group(0)!;
|
||||||
|
} else {
|
||||||
|
final tempFileName = Uri.decodeComponent(oldUrl.split('/').last.split('?').first);
|
||||||
|
sourcePath = tempFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
final destinationPath = 'events/$eventId/$fileName';
|
||||||
|
final newUrl = await moveEventFileHttp(
|
||||||
|
sourcePath: sourcePath,
|
||||||
|
destinationPath: destinationPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newUrl != null) {
|
||||||
|
newFiles.add({'name': fileName, 'url': newUrl});
|
||||||
|
} else {
|
||||||
|
newFiles.add({'name': fileName, 'url': oldUrl});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> updateEventDocuments(String eventId, List<Map<String, String>> documents) async {
|
||||||
|
await FirebaseFirestore.instance
|
||||||
|
.collection('events')
|
||||||
|
.doc(eventId)
|
||||||
|
.update({'documents': documents});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:em2rp/models/role_model.dart';
|
|
||||||
import 'package:em2rp/providers/local_user_provider.dart';
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
|
|
||||||
class PermissionGate extends StatelessWidget {
|
class PermissionGate extends StatelessWidget {
|
||||||
|
|||||||
@@ -120,22 +120,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: CustomAppBar(
|
appBar: CustomAppBar(
|
||||||
title: _getMonthName(_focusedDay.month),
|
title: "Calendrier",
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
_calendarCollapsed
|
|
||||||
? Icons.keyboard_arrow_down
|
|
||||||
: Icons.keyboard_arrow_up,
|
|
||||||
color: AppColors.blanc,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
setState(() {
|
|
||||||
_calendarCollapsed = !_calendarCollapsed;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
drawer: const MainDrawer(currentPage: '/calendar'),
|
drawer: const MainDrawer(currentPage: '/calendar'),
|
||||||
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
|
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
|
||||||
@@ -149,8 +134,8 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Icon(Icons.add, color: Colors.red),
|
|
||||||
tooltip: 'Ajouter un événement',
|
tooltip: 'Ajouter un événement',
|
||||||
|
child: const Icon(Icons.add, color: Colors.red),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
@@ -252,7 +237,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: _calendarCollapsed ? 0 : null,
|
height: _calendarCollapsed ? 0 : null,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
height: MediaQuery.of(context).size.height,
|
height: MediaQuery.of(context).size.height,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -360,7 +345,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
child: Container(
|
child: SizedBox(
|
||||||
height: MediaQuery.of(context).size.height,
|
height: MediaQuery.of(context).size.height,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -1,27 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:em2rp/providers/event_provider.dart';
|
|
||||||
import 'package:em2rp/models/event_model.dart';
|
import 'package:em2rp/models/event_model.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:em2rp/controllers/event_form_controller.dart';
|
||||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
import 'package:em2rp/views/widgets/event_form/event_basic_info_section.dart';
|
||||||
import 'package:em2rp/views/widgets/inputs/int_stepper_field.dart';
|
import 'package:em2rp/views/widgets/event_form/event_details_section.dart';
|
||||||
import 'package:em2rp/models/user_model.dart';
|
import 'package:em2rp/views/widgets/event_form/event_staff_and_documents_section.dart';
|
||||||
import 'package:em2rp/views/widgets/image/profile_picture.dart';
|
import 'package:em2rp/views/widgets/event_form/event_form_actions.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import 'package:firebase_storage/firebase_storage.dart';
|
|
||||||
import 'package:path/path.dart' as p;
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
|
||||||
import 'package:em2rp/providers/local_user_provider.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter_dropzone/flutter_dropzone.dart';
|
|
||||||
import 'package:em2rp/views/widgets/inputs/dropzone_upload_widget.dart';
|
|
||||||
import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart';
|
|
||||||
import 'package:em2rp/views/widgets/inputs/option_selector_widget.dart';
|
import 'package:em2rp/views/widgets/inputs/option_selector_widget.dart';
|
||||||
// ignore: avoid_web_libraries_in_flutter
|
import 'package:flutter/foundation.dart';
|
||||||
import 'dart:html' as html;
|
|
||||||
|
|
||||||
class EventAddEditPage extends StatefulWidget {
|
class EventAddEditPage extends StatefulWidget {
|
||||||
final EventModel? event;
|
final EventModel? event;
|
||||||
@@ -33,166 +19,27 @@ class EventAddEditPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _EventAddEditPageState extends State<EventAddEditPage> {
|
class _EventAddEditPageState extends State<EventAddEditPage> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
final TextEditingController _nameController = TextEditingController();
|
late EventFormController _controller;
|
||||||
final TextEditingController _descriptionController = TextEditingController();
|
|
||||||
final TextEditingController _basePriceController = TextEditingController();
|
|
||||||
final TextEditingController _installationController = TextEditingController();
|
|
||||||
final TextEditingController _disassemblyController = TextEditingController();
|
|
||||||
final TextEditingController _addressController = TextEditingController();
|
|
||||||
DateTime? _startDateTime;
|
|
||||||
DateTime? _endDateTime;
|
|
||||||
bool _isLoading = false;
|
|
||||||
String? _error;
|
|
||||||
String? _success;
|
|
||||||
String? _selectedEventType;
|
|
||||||
final List<String> _eventTypes = ['Bal', 'Mariage', 'Anniversaire'];
|
|
||||||
final Map<String, double> _eventTypeDefaultPrices = {
|
|
||||||
'Bal': 800.0,
|
|
||||||
'Mariage': 1500.0,
|
|
||||||
'Anniversaire': 500.0,
|
|
||||||
};
|
|
||||||
int _descriptionMaxLines = 3;
|
|
||||||
List<String> _selectedUserIds = [];
|
|
||||||
List<UserModel> _allUsers = [];
|
|
||||||
bool _isLoadingUsers = true;
|
|
||||||
List<Map<String, String>> _uploadedFiles = [];
|
|
||||||
DropzoneViewController? _dropzoneController;
|
|
||||||
bool _isDropzoneHighlighted = false;
|
|
||||||
List<Map<String, dynamic>> _selectedOptions = [];
|
|
||||||
bool _formChanged = false;
|
|
||||||
EventStatus _selectedStatus = EventStatus.waitingForApproval;
|
|
||||||
|
|
||||||
bool get isEditMode => widget.event != null;
|
bool get isEditMode => widget.event != null;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_descriptionController.addListener(_handleDescriptionChange);
|
_controller = EventFormController();
|
||||||
_fetchUsers();
|
_controller.initialize(widget.event);
|
||||||
_nameController.addListener(_onAnyFieldChanged);
|
|
||||||
_basePriceController.addListener(_onAnyFieldChanged);
|
|
||||||
_installationController.addListener(_onAnyFieldChanged);
|
|
||||||
_disassemblyController.addListener(_onAnyFieldChanged);
|
|
||||||
_addressController.addListener(_onAnyFieldChanged);
|
|
||||||
_descriptionController.addListener(_onAnyFieldChanged);
|
|
||||||
_addBeforeUnloadListener();
|
|
||||||
if (isEditMode) {
|
|
||||||
final e = widget.event!;
|
|
||||||
_nameController.text = e.name;
|
|
||||||
_descriptionController.text = e.description;
|
|
||||||
_basePriceController.text = e.basePrice.toStringAsFixed(2);
|
|
||||||
_installationController.text = e.installationTime.toString();
|
|
||||||
_disassemblyController.text = e.disassemblyTime.toString();
|
|
||||||
_addressController.text = e.address;
|
|
||||||
_startDateTime = e.startDateTime;
|
|
||||||
_endDateTime = e.endDateTime;
|
|
||||||
_selectedEventType = e.eventTypeId.isNotEmpty ? e.eventTypeId : null;
|
|
||||||
_selectedUserIds = e.workforce.map((ref) => ref.id).toList();
|
|
||||||
_uploadedFiles = List<Map<String, String>>.from(e.documents);
|
|
||||||
_selectedOptions = List<Map<String, dynamic>>.from(e.options);
|
|
||||||
_selectedStatus = e.status;
|
|
||||||
} else {
|
|
||||||
_selectedStatus = EventStatus.waitingForApproval;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleDescriptionChange() {
|
|
||||||
final lines = '\n'.allMatches(_descriptionController.text).length + 1;
|
|
||||||
setState(() {
|
|
||||||
_descriptionMaxLines = lines.clamp(3, 6);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onAnyFieldChanged() {
|
|
||||||
if (!_formChanged) {
|
|
||||||
setState(() {
|
|
||||||
_formChanged = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _fetchUsers() async {
|
|
||||||
final snapshot = await FirebaseFirestore.instance.collection('users').get();
|
|
||||||
setState(() {
|
|
||||||
_allUsers = snapshot.docs
|
|
||||||
.map((doc) => UserModel.fromMap(doc.data(), doc.id))
|
|
||||||
.toList();
|
|
||||||
_isLoadingUsers = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onEventTypeChanged(String? newType) {
|
|
||||||
if (newType == _selectedEventType) return;
|
|
||||||
setState(() {
|
|
||||||
_selectedEventType = newType;
|
|
||||||
if (newType != null) {
|
|
||||||
// Appliquer le prix par défaut si champ vide ou si type changé
|
|
||||||
final defaultPrice = _eventTypeDefaultPrices[newType] ?? 0.0;
|
|
||||||
if (_basePriceController.text.isEmpty ||
|
|
||||||
(_selectedEventType != null &&
|
|
||||||
_basePriceController.text ==
|
|
||||||
(_eventTypeDefaultPrices[_selectedEventType] ?? '')
|
|
||||||
.toString())) {
|
|
||||||
_basePriceController.text = defaultPrice.toStringAsFixed(2);
|
|
||||||
}
|
|
||||||
// Efface les options non compatibles
|
|
||||||
final before = _selectedOptions.length;
|
|
||||||
_selectedOptions.removeWhere((opt) {
|
|
||||||
final types = opt['compatibleTypes'] as List<String>?;
|
|
||||||
if (types == null) return true;
|
|
||||||
return !types.contains(newType);
|
|
||||||
});
|
|
||||||
if (_selectedOptions.length < before && context.mounted) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
'Certaines options ont été retirées car elles ne sont pas compatibles avec le type "$newType".')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_selectedOptions.clear();
|
|
||||||
}
|
|
||||||
_onAnyFieldChanged();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_nameController.dispose();
|
_controller.dispose();
|
||||||
_descriptionController.dispose();
|
|
||||||
_basePriceController.dispose();
|
|
||||||
_installationController.dispose();
|
|
||||||
_disassemblyController.dispose();
|
|
||||||
_addressController.dispose();
|
|
||||||
_removeBeforeUnloadListener();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Web: beforeunload pour empêcher la fermeture sans confirmation ---
|
|
||||||
void _addBeforeUnloadListener() {
|
|
||||||
if (kIsWeb) {
|
|
||||||
html.window.onBeforeUnload.listen(_beforeUnloadHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _removeBeforeUnloadListener() {
|
|
||||||
if (kIsWeb) {
|
|
||||||
// Il n'est pas possible de retirer un listener anonyme, donc on ne fait rien ici.
|
|
||||||
// Pour une gestion plus fine, il faudrait stocker la référence du listener.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _beforeUnloadHandler(html.Event event) {
|
|
||||||
if (_formChanged) {
|
|
||||||
event.preventDefault();
|
|
||||||
// Pour Chrome/Edge/Firefox, il faut définir returnValue
|
|
||||||
// ignore: unsafe_html
|
|
||||||
(event as dynamic).returnValue = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _onWillPop() async {
|
Future<bool> _onWillPop() async {
|
||||||
if (!_formChanged) return true;
|
if (!_controller.formChanged) return true;
|
||||||
|
if (!mounted) return true;
|
||||||
|
|
||||||
final shouldLeave = await showDialog<bool>(
|
final shouldLeave = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
@@ -214,262 +61,59 @@ class _EventAddEditPageState extends State<EventAddEditPage> {
|
|||||||
return shouldLeave ?? false;
|
return shouldLeave ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickAndUploadFiles() async {
|
|
||||||
final result = await FilePicker.platform
|
|
||||||
.pickFiles(allowMultiple: true, withData: true);
|
|
||||||
if (result != null && result.files.isNotEmpty) {
|
|
||||||
setState(() => _isLoading = true);
|
|
||||||
try {
|
|
||||||
List<Map<String, String>> files = [];
|
|
||||||
for (final file in result.files) {
|
|
||||||
final fileBytes = file.bytes;
|
|
||||||
final fileName = file.name;
|
|
||||||
if (fileBytes != null) {
|
|
||||||
final ref = FirebaseStorage.instance.ref().child(
|
|
||||||
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$fileName');
|
|
||||||
final uploadTask = await ref.putData(fileBytes);
|
|
||||||
final url = await uploadTask.ref.getDownloadURL();
|
|
||||||
files.add({'name': fileName, 'url': url});
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_error = "Impossible de lire le fichier ${file.name}";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_uploadedFiles.addAll(files);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
setState(() {
|
|
||||||
_error = 'Erreur lors de l\'upload : $e';
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setState(() => _isLoading = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String?> moveEventFileHttp({
|
|
||||||
required String sourcePath,
|
|
||||||
required String destinationPath,
|
|
||||||
}) async {
|
|
||||||
final url = Uri.parse(
|
|
||||||
'https://us-central1-em2rp-951dc.cloudfunctions.net/moveEventFileV2');
|
|
||||||
final user = FirebaseAuth.instance.currentUser;
|
|
||||||
final idToken = await user?.getIdToken();
|
|
||||||
final response = await http.post(
|
|
||||||
url,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
if (idToken != null) 'Authorization': 'Bearer $idToken',
|
|
||||||
},
|
|
||||||
body: jsonEncode({
|
|
||||||
'data': {
|
|
||||||
'sourcePath': sourcePath,
|
|
||||||
'destinationPath': destinationPath,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if (response.statusCode == 200) {
|
|
||||||
final data = jsonDecode(response.body);
|
|
||||||
if (data['url'] != null) {
|
|
||||||
return data['url'] as String;
|
|
||||||
} else if (data['result'] != null && data['result']['url'] != null) {
|
|
||||||
return data['result']['url'] as String;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
print('Erreur Cloud Function: \\n${response.body}');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _submit() async {
|
Future<void> _submit() async {
|
||||||
if (!_formKey.currentState!.validate() ||
|
if (!_formKey.currentState!.validate()) {
|
||||||
_startDateTime == null ||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
_endDateTime == null ||
|
const SnackBar(
|
||||||
_selectedEventType == null ||
|
content: Text('Veuillez remplir tous les champs obligatoires.'),
|
||||||
_addressController.text.isEmpty) return;
|
backgroundColor: Colors.red,
|
||||||
if (_endDateTime!.isBefore(_startDateTime!) ||
|
),
|
||||||
_endDateTime!.isAtSameMomentAs(_startDateTime!)) {
|
);
|
||||||
setState(() {
|
|
||||||
_error = "La date de fin doit être postérieure à la date de début.";
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
|
||||||
_isLoading = true;
|
|
||||||
_error = null;
|
|
||||||
_success = null;
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
|
||||||
if (isEditMode) {
|
|
||||||
// Edition : on met à jour l'événement existant
|
|
||||||
final updatedEvent = EventModel(
|
|
||||||
id: widget.event!.id,
|
|
||||||
name: _nameController.text.trim(),
|
|
||||||
description: _descriptionController.text.trim(),
|
|
||||||
startDateTime: _startDateTime!,
|
|
||||||
endDateTime: _endDateTime!,
|
|
||||||
basePrice: double.tryParse(_basePriceController.text) ?? 0.0,
|
|
||||||
installationTime: int.tryParse(_installationController.text) ?? 0,
|
|
||||||
disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0,
|
|
||||||
eventTypeId: _selectedEventType!,
|
|
||||||
customerId: '',
|
|
||||||
address: _addressController.text.trim(),
|
|
||||||
workforce: _selectedUserIds
|
|
||||||
.map((id) =>
|
|
||||||
FirebaseFirestore.instance.collection('users').doc(id))
|
|
||||||
.toList(),
|
|
||||||
latitude: 0.0,
|
|
||||||
longitude: 0.0,
|
|
||||||
documents: _uploadedFiles,
|
|
||||||
options: _selectedOptions
|
|
||||||
.map((opt) => {
|
|
||||||
'name': opt['name'],
|
|
||||||
'price': opt['price'],
|
|
||||||
})
|
|
||||||
.toList(),
|
|
||||||
status: _selectedStatus,
|
|
||||||
);
|
|
||||||
final docRef = FirebaseFirestore.instance
|
|
||||||
.collection('events')
|
|
||||||
.doc(widget.event!.id);
|
|
||||||
await docRef.update(updatedEvent.toMap());
|
|
||||||
// Gestion des fichiers (si besoin, à adapter selon ta logique)
|
|
||||||
// ...
|
|
||||||
setState(() {
|
|
||||||
_success = "Événement modifié avec succès !";
|
|
||||||
});
|
|
||||||
if (context.mounted) Navigator.of(context).pop();
|
|
||||||
} else {
|
|
||||||
// Création : logique existante
|
|
||||||
final newEvent = EventModel(
|
|
||||||
id: '',
|
|
||||||
name: _nameController.text.trim(),
|
|
||||||
description: _descriptionController.text.trim(),
|
|
||||||
startDateTime: _startDateTime!,
|
|
||||||
endDateTime: _endDateTime!,
|
|
||||||
basePrice: double.tryParse(_basePriceController.text) ?? 0.0,
|
|
||||||
installationTime: int.tryParse(_installationController.text) ?? 0,
|
|
||||||
disassemblyTime: int.tryParse(_disassemblyController.text) ?? 0,
|
|
||||||
eventTypeId: _selectedEventType!,
|
|
||||||
customerId: '',
|
|
||||||
address: _addressController.text.trim(),
|
|
||||||
workforce: _selectedUserIds
|
|
||||||
.map((id) =>
|
|
||||||
FirebaseFirestore.instance.collection('users').doc(id))
|
|
||||||
.toList(),
|
|
||||||
latitude: 0.0,
|
|
||||||
longitude: 0.0,
|
|
||||||
documents: _uploadedFiles,
|
|
||||||
options: _selectedOptions
|
|
||||||
.map((opt) => {
|
|
||||||
'name': opt['name'],
|
|
||||||
'price': opt['price'],
|
|
||||||
})
|
|
||||||
.toList(),
|
|
||||||
status: _selectedStatus,
|
|
||||||
);
|
|
||||||
final docRef = await FirebaseFirestore.instance
|
|
||||||
.collection('events')
|
|
||||||
.add(newEvent.toMap());
|
|
||||||
final eventId = docRef.id;
|
|
||||||
List<Map<String, String>> newFiles = [];
|
|
||||||
for (final file in _uploadedFiles) {
|
|
||||||
final fileName = file['name']!;
|
|
||||||
final oldUrl = file['url']!;
|
|
||||||
String sourcePath;
|
|
||||||
final tempPattern = RegExp(r'events/temp/[^?]+');
|
|
||||||
final match = tempPattern.firstMatch(oldUrl);
|
|
||||||
if (match != null) {
|
|
||||||
sourcePath = match.group(0)!;
|
|
||||||
} else {
|
|
||||||
final tempFileName =
|
|
||||||
Uri.decodeComponent(oldUrl.split('/').last.split('?').first);
|
|
||||||
sourcePath = tempFileName;
|
|
||||||
}
|
|
||||||
final destinationPath = 'events/$eventId/$fileName';
|
|
||||||
final newUrl = await moveEventFileHttp(
|
|
||||||
sourcePath: sourcePath,
|
|
||||||
destinationPath: destinationPath,
|
|
||||||
);
|
|
||||||
if (newUrl != null) {
|
|
||||||
newFiles.add({'name': fileName, 'url': newUrl});
|
|
||||||
} else {
|
|
||||||
newFiles.add({'name': fileName, 'url': oldUrl});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await docRef.update({'documents': newFiles});
|
|
||||||
final localUserProvider =
|
|
||||||
Provider.of<LocalUserProvider>(context, listen: false);
|
|
||||||
final userId = localUserProvider.uid;
|
|
||||||
final canViewAllEvents =
|
|
||||||
localUserProvider.hasPermission('view_all_events');
|
|
||||||
if (userId != null) {
|
|
||||||
await eventProvider.loadUserEvents(userId,
|
|
||||||
canViewAllEvents: canViewAllEvents);
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
_success = "Événement créé avec succès !";
|
|
||||||
});
|
|
||||||
if (context.mounted) Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setState(() {
|
|
||||||
_error = "Erreur lors de la sauvegarde : $e";
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setState(() {
|
|
||||||
_isLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSectionTitle(String title) {
|
final success = await _controller.submitForm(context, existingEvent: widget.event);
|
||||||
return Padding(
|
if (success && mounted) {
|
||||||
padding: const EdgeInsets.only(top: 16.0, bottom: 8.0),
|
Navigator.of(context).pop();
|
||||||
child: Align(
|
}
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Text(
|
|
||||||
title,
|
|
||||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
|
||||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||||
return WillPopScope(
|
|
||||||
onWillPop: _onWillPop,
|
return ChangeNotifierProvider.value(
|
||||||
child: Scaffold(
|
value: _controller,
|
||||||
appBar: AppBar(
|
child: PopScope(
|
||||||
title:
|
canPop: false,
|
||||||
Text(isEditMode ? 'Modifier un événement' : 'Créer un événement'),
|
onPopInvokedWithResult: (didPop, result) async {
|
||||||
),
|
if (didPop) return;
|
||||||
body: Center(
|
final shouldPop = await _onWillPop();
|
||||||
child: SingleChildScrollView(
|
if (shouldPop && mounted) {
|
||||||
child: (isMobile
|
Navigator.of(context).pop();
|
||||||
? Padding(
|
}
|
||||||
padding: const EdgeInsets.symmetric(
|
},
|
||||||
horizontal: 16, vertical: 12),
|
child: Scaffold(
|
||||||
child: _buildFormContent(isMobile),
|
appBar: AppBar(
|
||||||
)
|
title: Text(isEditMode ? 'Modifier un événement' : 'Créer un événement'),
|
||||||
: Card(
|
),
|
||||||
elevation: 6,
|
body: Center(
|
||||||
margin: const EdgeInsets.all(24),
|
child: SingleChildScrollView(
|
||||||
shape: RoundedRectangleBorder(
|
child: (isMobile
|
||||||
borderRadius: BorderRadius.circular(18)),
|
? Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 32, vertical: 32),
|
|
||||||
child: _buildFormContent(isMobile),
|
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),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -477,373 +121,105 @@ class _EventAddEditPageState extends State<EventAddEditPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFormContent(bool isMobile) {
|
Widget _buildFormContent(bool isMobile) {
|
||||||
return Form(
|
return Consumer<EventFormController>(
|
||||||
key: _formKey,
|
builder: (context, controller, child) {
|
||||||
child: Column(
|
// Trouver le nom du type d'événement pour le passer au sélecteur d'options
|
||||||
mainAxisSize: MainAxisSize.min,
|
final selectedEventTypeIndex = controller.selectedEventTypeId != null
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
? controller.eventTypes.indexWhere((et) => et.id == controller.selectedEventTypeId)
|
||||||
children: [
|
: -1;
|
||||||
Padding(
|
final selectedEventType = selectedEventTypeIndex != -1
|
||||||
padding: const EdgeInsets.only(top: 0.0, bottom: 4.0),
|
? controller.eventTypes[selectedEventTypeIndex]
|
||||||
child: Align(
|
: null;
|
||||||
alignment: Alignment.centerLeft,
|
final selectedEventTypeName = selectedEventType?.name;
|
||||||
child: Text(
|
|
||||||
'Informations principales',
|
return Form(
|
||||||
style:
|
key: _formKey,
|
||||||
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
child: Column(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
),
|
|
||||||
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: _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),
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
EventBasicInfoSection(
|
||||||
child: GestureDetector(
|
nameController: controller.nameController,
|
||||||
onTap: () async {
|
basePriceController: controller.basePriceController,
|
||||||
final picked = await showDatePicker(
|
eventTypes: controller.eventTypes,
|
||||||
context: context,
|
isLoadingEventTypes: controller.isLoadingEventTypes,
|
||||||
initialDate: DateTime.now(),
|
selectedEventTypeId: controller.selectedEventTypeId,
|
||||||
firstDate: DateTime(2020),
|
startDateTime: controller.startDateTime,
|
||||||
lastDate: DateTime(2099),
|
endDateTime: controller.endDateTime,
|
||||||
);
|
onEventTypeChanged: (typeId) => controller.onEventTypeChanged(typeId, context),
|
||||||
if (picked != null) {
|
onStartDateTimeChanged: controller.setStartDateTime,
|
||||||
final time = await showTimePicker(
|
onEndDateTimeChanged: controller.setEndDateTime,
|
||||||
context: context,
|
onAnyFieldChanged: () {}, // Géré automatiquement par le contrôleur
|
||||||
initialTime: TimeOfDay.now(),
|
),
|
||||||
);
|
const SizedBox(height: 16),
|
||||||
if (time != null) {
|
OptionSelectorWidget(
|
||||||
setState(() {
|
eventType: selectedEventTypeName,
|
||||||
_startDateTime = DateTime(
|
selectedOptions: controller.selectedOptions,
|
||||||
picked.year,
|
onChanged: controller.setSelectedOptions,
|
||||||
picked.month,
|
onRemove: (name) {
|
||||||
picked.day,
|
final newOptions = List<Map<String, dynamic>>.from(controller.selectedOptions);
|
||||||
time.hour,
|
newOptions.removeWhere((o) => o['name'] == name);
|
||||||
time.minute,
|
controller.setSelectedOptions(newOptions);
|
||||||
);
|
},
|
||||||
if (_endDateTime != null &&
|
eventTypeRequired: controller.selectedEventTypeId == null,
|
||||||
(_endDateTime!.isBefore(_startDateTime!) ||
|
isMobile: isMobile,
|
||||||
_endDateTime!
|
),
|
||||||
.isAtSameMomentAs(_startDateTime!))) {
|
EventDetailsSection(
|
||||||
_endDateTime = null;
|
descriptionController: controller.descriptionController,
|
||||||
}
|
installationController: controller.installationController,
|
||||||
});
|
disassemblyController: controller.disassemblyController,
|
||||||
}
|
addressController: controller.addressController,
|
||||||
}
|
isMobile: isMobile,
|
||||||
},
|
onAnyFieldChanged: () {}, // Géré automatiquement par le contrôleur
|
||||||
child: AbsorbPointer(
|
),
|
||||||
child: TextFormField(
|
EventStaffAndDocumentsSection(
|
||||||
readOnly: true,
|
allUsers: controller.allUsers,
|
||||||
decoration: InputDecoration(
|
selectedUserIds: controller.selectedUserIds,
|
||||||
labelText: 'Début',
|
onUserSelectionChanged: controller.setSelectedUserIds,
|
||||||
border: const OutlineInputBorder(),
|
isLoadingUsers: controller.isLoadingUsers,
|
||||||
prefixIcon: const Icon(Icons.calendar_today),
|
uploadedFiles: controller.uploadedFiles,
|
||||||
suffixIcon: const Icon(Icons.edit_calendar),
|
onFilesChanged: controller.setUploadedFiles,
|
||||||
),
|
isLoading: controller.isLoading,
|
||||||
controller: TextEditingController(
|
error: controller.error,
|
||||||
text: _startDateTime == null
|
success: controller.success,
|
||||||
? ''
|
isMobile: isMobile,
|
||||||
: DateFormat('dd/MM/yyyy HH:mm')
|
onPickAndUploadFiles: controller.pickAndUploadFiles,
|
||||||
.format(_startDateTime!),
|
),
|
||||||
),
|
if (controller.error != null)
|
||||||
validator: (v) =>
|
Padding(
|
||||||
_startDateTime == null ? 'Champ requis' : null,
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
),
|
child: Text(
|
||||||
|
controller.error!,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (controller.success != null)
|
||||||
const SizedBox(width: 16),
|
Padding(
|
||||||
Expanded(
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
child: GestureDetector(
|
child: Text(
|
||||||
onTap: _startDateTime == null
|
controller.success!,
|
||||||
? null
|
style: const TextStyle(color: Colors.green),
|
||||||
: () async {
|
textAlign: TextAlign.center,
|
||||||
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) {
|
|
||||||
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'
|
|
||||||
: (_startDateTime != null &&
|
|
||||||
_endDateTime != null &&
|
|
||||||
(_endDateTime!.isBefore(_startDateTime!) ||
|
|
||||||
_endDateTime!
|
|
||||||
.isAtSameMomentAs(_startDateTime!)))
|
|
||||||
? 'La date de fin doit être après la date de début'
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
EventFormActions(
|
||||||
|
isLoading: controller.isLoading,
|
||||||
|
isEditMode: isEditMode,
|
||||||
|
onCancel: () async {
|
||||||
|
final shouldLeave = await _onWillPop();
|
||||||
|
if (shouldLeave && mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSubmit: _submit,
|
||||||
|
onSetConfirmed: !isEditMode ? () {
|
||||||
|
} : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
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(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
OptionSelectorWidget(
|
|
||||||
eventType: _selectedEventType,
|
|
||||||
selectedOptions: _selectedOptions,
|
|
||||||
onChanged: (opts) => setState(() => _selectedOptions = opts),
|
|
||||||
onRemove: (name) {
|
|
||||||
setState(() {
|
|
||||||
_selectedOptions.removeWhere((o) => o['name'] == name);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
eventTypeRequired: _selectedEventType == null,
|
|
||||||
isMobile: isMobile,
|
|
||||||
),
|
|
||||||
_buildSectionTitle('Détails'),
|
|
||||||
AnimatedContainer(
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
minHeight: 48,
|
|
||||||
maxHeight: isMobile ? 48.0 * 20 : 48.0 * 10,
|
|
||||||
),
|
|
||||||
child: TextFormField(
|
|
||||||
controller: _descriptionController,
|
|
||||||
minLines: 1,
|
|
||||||
maxLines: _descriptionMaxLines > (isMobile ? 20 : 10)
|
|
||||||
? (isMobile ? 20 : 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: 99,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: IntStepperField(
|
|
||||||
label: 'Démontage (h)',
|
|
||||||
controller: _disassemblyController,
|
|
||||||
min: 0,
|
|
||||||
max: 99,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
_buildSectionTitle('Adresse'),
|
|
||||||
TextFormField(
|
|
||||||
controller: _addressController,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Adresse',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
prefixIcon: Icon(Icons.location_on),
|
|
||||||
),
|
|
||||||
validator: (v) => v == null || v.isEmpty ? 'Champ requis' : null,
|
|
||||||
),
|
|
||||||
_buildSectionTitle('Personnel'),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: UserMultiSelectWidget(
|
|
||||||
allUsers: _allUsers,
|
|
||||||
selectedUserIds: _selectedUserIds,
|
|
||||||
onChanged: (ids) => setState(() => _selectedUserIds = ids),
|
|
||||||
isLoading: _isLoadingUsers,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildSectionTitle('Documents'),
|
|
||||||
if (isMobile)
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
ElevatedButton.icon(
|
|
||||||
icon: const Icon(Icons.attach_file),
|
|
||||||
label: const Text('Ajouter un fichier'),
|
|
||||||
onPressed: _isLoading ? null : _pickAndUploadFiles,
|
|
||||||
),
|
|
||||||
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
|
|
||||||
: () {
|
|
||||||
setState(() {
|
|
||||||
_uploadedFiles.remove(file);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
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)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (!isMobile)
|
|
||||||
DropzoneUploadWidget(
|
|
||||||
uploadedFiles: _uploadedFiles,
|
|
||||||
onFilesChanged: (files) =>
|
|
||||||
setState(() => _uploadedFiles = files),
|
|
||||||
isLoading: _isLoading,
|
|
||||||
error: _error,
|
|
||||||
success: _success,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: _isLoading
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
final shouldLeave = await _onWillPop();
|
|
||||||
if (shouldLeave && context.mounted) {
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
: Text(isEditMode ? 'Enregistrer' : 'Créer'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (!isEditMode)
|
|
||||||
Center(
|
|
||||||
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: null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
|||||||
List<RoleModel> availableRoles = [];
|
List<RoleModel> availableRoles = [];
|
||||||
bool isLoadingRoles = true;
|
bool isLoadingRoles = true;
|
||||||
|
|
||||||
Future<void> _loadRoles() async {
|
Future<void> loadRoles() async {
|
||||||
final snapshot =
|
final snapshot =
|
||||||
await FirebaseFirestore.instance.collection('roles').get();
|
await FirebaseFirestore.instance.collection('roles').get();
|
||||||
availableRoles = snapshot.docs
|
availableRoles = snapshot.docs
|
||||||
@@ -145,7 +145,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
|||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => FutureBuilder(
|
builder: (context) => FutureBuilder(
|
||||||
future: _loadRoles(),
|
future: loadRoles(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
return Dialog(
|
return Dialog(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -204,7 +204,7 @@ class _UserManagementPageState extends State<UserManagementPage> {
|
|||||||
isLoadingRoles
|
isLoadingRoles
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator()
|
||||||
: DropdownButtonFormField<String>(
|
: DropdownButtonFormField<String>(
|
||||||
value: selectedRoleId,
|
initialValue: selectedRoleId,
|
||||||
decoration: buildInputDecoration('Rôle',
|
decoration: buildInputDecoration('Rôle',
|
||||||
Icons.admin_panel_settings_outlined),
|
Icons.admin_panel_settings_outlined),
|
||||||
items: availableRoles.map((role) {
|
items: availableRoles.map((role) {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class _ForgotPasswordDialogState extends State<ForgotPasswordDialogWidget> {
|
|||||||
),
|
),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Terminer'),
|
child: const Text('Annuler'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -87,13 +87,31 @@ class EventDetails extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start, // Optionnel mais recommandé pour bien aligner
|
||||||
children: [
|
children: [
|
||||||
SelectableText(
|
// On remplace le SelectableText par une Column
|
||||||
event.name,
|
Expanded( // Utiliser Expanded pour que le texte ne déborde pas
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
child: Column(
|
||||||
color: AppColors.noir,
|
crossAxisAlignment: CrossAxisAlignment.start, // Aligne les textes à gauche
|
||||||
fontWeight: FontWeight.bold,
|
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),
|
const SizedBox(width: 12),
|
||||||
_buildStatusIcon(event.status),
|
_buildStatusIcon(event.status),
|
||||||
@@ -119,7 +137,7 @@ class EventDetails extends StatelessWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||||
child: _FirestoreStatusButton(
|
child: _FirestoreStatusButton(
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
currentStatus: event.status,
|
currentStatus: event.status,
|
||||||
onStatusChanged: (newStatus) async {
|
onStatusChanged: (newStatus) async {
|
||||||
await FirebaseFirestore.instance
|
await FirebaseFirestore.instance
|
||||||
|
|||||||
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)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,16 +49,14 @@ class _DropzoneUploadWidgetState extends State<DropzoneUploadWidget> {
|
|||||||
for (final file in files) {
|
for (final file in files) {
|
||||||
final name = await _dropzoneController!.getFilename(file);
|
final name = await _dropzoneController!.getFilename(file);
|
||||||
final bytes = await _dropzoneController!.getFileData(file);
|
final bytes = await _dropzoneController!.getFileData(file);
|
||||||
if (bytes != null) {
|
final ref = FirebaseStorage.instance.ref().child(
|
||||||
final ref = FirebaseStorage.instance.ref().child(
|
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$name');
|
||||||
'events/temp/${DateTime.now().millisecondsSinceEpoch}_$name');
|
final uploadTask = await ref.putData(bytes);
|
||||||
final uploadTask = await ref.putData(bytes);
|
final url = await uploadTask.ref.getDownloadURL();
|
||||||
final url = await uploadTask.ref.getDownloadURL();
|
if (!newFiles.any((f) => f['name'] == name && f['url'] == url)) {
|
||||||
if (!newFiles.any((f) => f['name'] == name && f['url'] == url)) {
|
newFiles.add({'name': name, 'url': url});
|
||||||
newFiles.add({'name': name, 'url': url});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
widget.onFilesChanged(newFiles);
|
widget.onFilesChanged(newFiles);
|
||||||
setState(() {
|
setState(() {
|
||||||
_success = "Fichier(s) ajouté(s) !";
|
_success = "Fichier(s) ajouté(s) !";
|
||||||
@@ -233,7 +231,7 @@ class _DropzoneUploadWidgetState extends State<DropzoneUploadWidget> {
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
dense: true,
|
dense: true,
|
||||||
);
|
);
|
||||||
}).toList(),
|
}),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 160,
|
width: 160,
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class OptionSelectorWidget extends StatefulWidget {
|
|||||||
class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
class _OptionSelectorWidgetState extends State<OptionSelectorWidget> {
|
||||||
List<EventOption> _allOptions = [];
|
List<EventOption> _allOptions = [];
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
String _search = '';
|
final String _search = '';
|
||||||
final List<String> _eventTypes = ['Bal', 'Mariage', 'Anniversaire'];
|
final List<String> _eventTypes = ['Bal', 'Mariage', 'Anniversaire'];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -201,8 +201,7 @@ class _OptionPickerDialogState extends State<_OptionPickerDialog> {
|
|||||||
final opt = filtered[i];
|
final opt = filtered[i];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(opt.name),
|
title: Text(opt.name),
|
||||||
subtitle: Text(opt.details +
|
subtitle: Text('${opt.details}\nFourchette: ${opt.valMin}€ ~ ${opt.valMax}€'),
|
||||||
'\nFourchette: ${opt.valMin}€ ~ ${opt.valMax}€'),
|
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final min = opt.valMin;
|
final min = opt.valMin;
|
||||||
final max = opt.valMax;
|
final max = opt.valMax;
|
||||||
@@ -304,7 +303,7 @@ class _CreateOptionDialogState extends State<_CreateOptionDialog> {
|
|||||||
final _detailsController = TextEditingController();
|
final _detailsController = TextEditingController();
|
||||||
final _minPriceController = TextEditingController();
|
final _minPriceController = TextEditingController();
|
||||||
final _maxPriceController = TextEditingController();
|
final _maxPriceController = TextEditingController();
|
||||||
List<String> _selectedTypes = [];
|
final List<String> _selectedTypes = [];
|
||||||
final List<String> _allTypes = ['Bal', 'Mariage', 'Anniversaire'];
|
final List<String> _allTypes = ['Bal', 'Mariage', 'Anniversaire'];
|
||||||
String? _error;
|
String? _error;
|
||||||
bool _checkingName = false;
|
bool _checkingName = false;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:em2rp/views/widgets/image/profile_picture.dart';
|
import 'package:em2rp/views/widgets/image/profile_picture.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:em2rp/utils/permission_gate.dart';
|
import 'package:em2rp/utils/permission_gate.dart';
|
||||||
import 'package:em2rp/models/role_model.dart';
|
|
||||||
|
|
||||||
class MainDrawer extends StatelessWidget {
|
class MainDrawer extends StatelessWidget {
|
||||||
final String currentPage;
|
final String currentPage;
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class _EditUserDialogState extends State<EditUserDialog> {
|
|||||||
isLoadingRoles
|
isLoadingRoles
|
||||||
? const CircularProgressIndicator()
|
? const CircularProgressIndicator()
|
||||||
: DropdownButtonFormField<String>(
|
: DropdownButtonFormField<String>(
|
||||||
value: selectedRoleId,
|
initialValue: selectedRoleId,
|
||||||
decoration: _buildInputDecoration(
|
decoration: _buildInputDecoration(
|
||||||
'Rôle', Icons.admin_panel_settings_outlined),
|
'Rôle', Icons.admin_panel_settings_outlined),
|
||||||
items: availableRoles.map((role) {
|
items: availableRoles.map((role) {
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ class _UserMultiSelect extends StatefulWidget {
|
|||||||
final ValueChanged<List<String>> onChanged;
|
final ValueChanged<List<String>> onChanged;
|
||||||
|
|
||||||
const _UserMultiSelect({
|
const _UserMultiSelect({
|
||||||
super.key,
|
|
||||||
required this.allUsers,
|
required this.allUsers,
|
||||||
required this.selectedUserIds,
|
required this.selectedUserIds,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ dependencies:
|
|||||||
universal_io: ^2.2.2
|
universal_io: ^2.2.2
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
table_calendar: ^3.0.9
|
table_calendar: ^3.0.9
|
||||||
intl: ^0.19.0
|
intl: ^0.20.2
|
||||||
google_maps_flutter: ^2.5.0
|
google_maps_flutter: ^2.5.0
|
||||||
permission_handler: ^12.0.0+1
|
permission_handler: ^12.0.0+1
|
||||||
geolocator: ^14.0.1
|
geolocator: ^14.0.1
|
||||||
@@ -55,6 +55,7 @@ dependencies:
|
|||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
path: any
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="../assets/EM2_NsurB.jpg"/>
|
||||||
|
|
||||||
<title>em2rp</title>
|
<title>em2rp</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|||||||
Reference in New Issue
Block a user