Compare commits

...

2 Commits

Author SHA1 Message Date
080fb7d077 fix erreur firebase 2025-06-03 20:41:45 +02:00
57c59c911a Equipe sur event details OK
Modif evenement OK
2025-06-03 19:59:40 +02:00
7 changed files with 316 additions and 121 deletions

View File

@ -13,7 +13,7 @@ import 'views/user_management_page.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'providers/local_user_provider.dart'; import 'providers/local_user_provider.dart';
import 'services/user_service.dart'; import 'services/user_service.dart';
import 'pages/auth/reset_password_page.dart'; import 'views/reset_password_page.dart';
import 'config/env.dart'; import 'config/env.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';

View File

@ -20,22 +20,24 @@ class EventProvider with ChangeNotifier {
print( print(
'Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)'); 'Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)');
QuerySnapshot eventsSnapshot; QuerySnapshot eventsSnapshot;
if (canViewAllEvents) { // On charge tous les events pour les users non-admins aussi
eventsSnapshot = await _firestore.collection('events').get(); eventsSnapshot = await _firestore.collection('events').get();
} else {
eventsSnapshot = await _firestore
.collection('events')
.where('workforce',
arrayContains: _firestore.collection('users').doc(userId))
.get();
}
print('Found ${eventsSnapshot.docs.length} events for user'); print('Found ${eventsSnapshot.docs.length} events for user');
_events = eventsSnapshot.docs.map((doc) { // On filtre côté client si l'utilisateur n'est pas admin
final allEvents = eventsSnapshot.docs.map((doc) {
print('Event data: ${doc.data()}'); print('Event data: ${doc.data()}');
return EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id); return EventModel.fromMap(doc.data() as Map<String, dynamic>, doc.id);
}).toList(); }).toList();
if (canViewAllEvents) {
_events = allEvents;
} else {
final userRef = _firestore.collection('users').doc(userId);
_events = allEvents
.where((e) => e.workforce.any((ref) => ref.id == userRef.id))
.toList();
}
print('Parsed ${_events.length} events'); print('Parsed ${_events.length} events');

View File

@ -145,7 +145,7 @@ class _CalendarPageState extends State<CalendarPage> {
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const EventAddPage(), builder: (context) => const EventAddEditPage(),
), ),
); );
}, },

View File

@ -23,14 +23,15 @@ import 'package:em2rp/views/widgets/inputs/option_selector_widget.dart';
// ignore: avoid_web_libraries_in_flutter // ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html; import 'dart:html' as html;
class EventAddPage extends StatefulWidget { class EventAddEditPage extends StatefulWidget {
const EventAddPage({super.key}); final EventModel? event;
const EventAddEditPage({super.key, this.event});
@override @override
State<EventAddPage> createState() => _EventAddPageState(); State<EventAddEditPage> createState() => _EventAddEditPageState();
} }
class _EventAddPageState extends State<EventAddPage> { class _EventAddEditPageState extends State<EventAddEditPage> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final TextEditingController _nameController = TextEditingController(); final TextEditingController _nameController = TextEditingController();
final TextEditingController _descriptionController = TextEditingController(); final TextEditingController _descriptionController = TextEditingController();
@ -61,6 +62,8 @@ class _EventAddPageState extends State<EventAddPage> {
bool _formChanged = false; bool _formChanged = false;
EventStatus _selectedStatus = EventStatus.waitingForApproval; EventStatus _selectedStatus = EventStatus.waitingForApproval;
bool get isEditMode => widget.event != null;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -73,8 +76,25 @@ class _EventAddPageState extends State<EventAddPage> {
_addressController.addListener(_onAnyFieldChanged); _addressController.addListener(_onAnyFieldChanged);
_descriptionController.addListener(_onAnyFieldChanged); _descriptionController.addListener(_onAnyFieldChanged);
_addBeforeUnloadListener(); _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; _selectedStatus = EventStatus.waitingForApproval;
} }
}
void _handleDescriptionChange() { void _handleDescriptionChange() {
final lines = '\n'.allMatches(_descriptionController.text).length + 1; final lines = '\n'.allMatches(_descriptionController.text).length + 1;
@ -284,6 +304,47 @@ class _EventAddPageState extends State<EventAddPage> {
}); });
try { try {
final eventProvider = Provider.of<EventProvider>(context, listen: false); 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( final newEvent = EventModel(
id: '', id: '',
name: _nameController.text.trim(), name: _nameController.text.trim(),
@ -297,7 +358,8 @@ class _EventAddPageState extends State<EventAddPage> {
customerId: '', customerId: '',
address: _addressController.text.trim(), address: _addressController.text.trim(),
workforce: _selectedUserIds workforce: _selectedUserIds
.map((id) => FirebaseFirestore.instance.collection('users').doc(id)) .map((id) =>
FirebaseFirestore.instance.collection('users').doc(id))
.toList(), .toList(),
latitude: 0.0, latitude: 0.0,
longitude: 0.0, longitude: 0.0,
@ -353,9 +415,10 @@ class _EventAddPageState extends State<EventAddPage> {
_success = "Événement créé avec succès !"; _success = "Événement créé avec succès !";
}); });
if (context.mounted) Navigator.of(context).pop(); if (context.mounted) Navigator.of(context).pop();
}
} catch (e) { } catch (e) {
setState(() { setState(() {
_error = "Erreur lors de la création : $e"; _error = "Erreur lors de la sauvegarde : $e";
}); });
} finally { } finally {
setState(() { setState(() {
@ -385,7 +448,8 @@ class _EventAddPageState extends State<EventAddPage> {
onWillPop: _onWillPop, onWillPop: _onWillPop,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Créer un événement'), title:
Text(isEditMode ? 'Modifier un événement' : 'Créer un événement'),
), ),
body: Center( body: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
@ -393,10 +457,7 @@ class _EventAddPageState extends State<EventAddPage> {
? Padding( ? Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12), horizontal: 16, vertical: 12),
child: Container(
// Pas de Card sur mobile, juste un conteneur
child: _buildFormContent(isMobile), child: _buildFormContent(isMobile),
),
) )
: Card( : Card(
elevation: 6, elevation: 6,
@ -764,11 +825,11 @@ class _EventAddPageState extends State<EventAddPage> {
height: 20, height: 20,
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(strokeWidth: 2),
) )
: const Text('Créer'), : Text(isEditMode ? 'Enregistrer' : 'Créer'),
), ),
], ],
), ),
const SizedBox(height: 16), if (!isEditMode)
Center( Center(
child: ElevatedButton.icon( child: ElevatedButton.icon(
icon: const Icon(Icons.check_circle, color: Colors.white), icon: const Icon(Icons.check_circle, color: Colors.white),

View File

@ -9,6 +9,10 @@ import 'package:latlong2/latlong.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:em2rp/views/widgets/user_management/user_card.dart';
import 'package:em2rp/models/user_model.dart';
import 'package:em2rp/views/widgets/user_management/user_multi_select_widget.dart';
import 'package:em2rp/views/event_add_page.dart';
class EventDetails extends StatelessWidget { class EventDetails extends StatelessWidget {
final EventModel event; final EventModel event;
@ -93,6 +97,21 @@ class EventDetails extends StatelessWidget {
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
_buildStatusIcon(event.status), _buildStatusIcon(event.status),
const SizedBox(width: 8),
Spacer(),
if (Provider.of<LocalUserProvider>(context, listen: false)
.hasPermission('edit_event'))
IconButton(
icon: const Icon(Icons.edit, color: AppColors.rouge),
tooltip: 'Modifier',
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => EventAddEditPage(event: event),
),
);
},
),
], ],
), ),
if (Provider.of<LocalUserProvider>(context, listen: false) if (Provider.of<LocalUserProvider>(context, listen: false)
@ -337,6 +356,9 @@ class EventDetails extends StatelessWidget {
); );
}).toList(), }).toList(),
), ),
// --- EQUIPE SECTION ---
const SizedBox(height: 16),
EquipeSection(workforce: event.workforce),
], ],
], ],
), ),
@ -773,3 +795,84 @@ class _FirestoreStatusButtonState extends State<_FirestoreStatusButton> {
); );
} }
} }
class EquipeSection extends StatelessWidget {
final List workforce;
const EquipeSection({super.key, required this.workforce});
@override
Widget build(BuildContext context) {
if (workforce.isEmpty) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Equipe',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.black,
fontWeight: FontWeight.bold,
)),
const SizedBox(height: 8),
Text('Aucun membre assigné.',
style: Theme.of(context).textTheme.bodyMedium),
],
);
}
return FutureBuilder<List<UserModel>>(
future: _fetchUsers(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
);
}
if (snapshot.hasError) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(
snapshot.error.toString().contains('permission-denied')
? "Vous n'avez pas la permission de voir tous les membres de l'équipe."
: "Erreur lors du chargement de l'équipe : ${snapshot.error}",
style: const TextStyle(color: Colors.red),
),
);
}
final users = snapshot.data ?? [];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Equipe',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: Colors.black,
fontWeight: FontWeight.bold,
)),
const SizedBox(height: 8),
if (users.isEmpty)
Text('Aucun membre assigné.',
style: Theme.of(context).textTheme.bodyMedium),
if (users.isNotEmpty)
UserChipsList(
users: users,
showRemove: false,
),
],
);
},
);
}
Future<List<UserModel>> _fetchUsers() async {
final firestore = FirebaseFirestore.instance;
List<UserModel> users = [];
for (final ref in workforce) {
try {
final doc = await firestore.doc(ref.path).get();
if (doc.exists) {
users.add(
UserModel.fromMap(doc.data() as Map<String, dynamic>, doc.id));
}
} catch (_) {}
}
return users;
}
}

View File

@ -70,26 +70,15 @@ class _UserMultiSelectState extends State<_UserMultiSelect> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Wrap( UserChipsList(
spacing: 12, users: selectedUsers,
runSpacing: 12, selectedUserIds: widget.selectedUserIds,
children: selectedUsers showRemove: true,
.map((user) => Chip( onRemove: (uid) {
avatar: ProfilePictureWidget(userId: user.uid, radius: 28),
label: Text('${user.firstName} ${user.lastName}',
style: const TextStyle(fontSize: 16)),
labelPadding: const EdgeInsets.symmetric(horizontal: 8),
deleteIcon: const Icon(Icons.close, size: 20),
onDeleted: () {
final newList = List<String>.from(widget.selectedUserIds) final newList = List<String>.from(widget.selectedUserIds)
..remove(user.uid); ..remove(uid);
widget.onChanged(newList); widget.onChanged(newList);
}, },
backgroundColor: Colors.grey[200],
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
))
.toList(),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
@ -188,3 +177,43 @@ class _UserPickerDialogState extends State<_UserPickerDialog> {
); );
} }
} }
class UserChipsList extends StatelessWidget {
final List<UserModel> users;
final List<String> selectedUserIds;
final ValueChanged<String>? onRemove;
final bool showRemove;
final double avatarRadius;
const UserChipsList({
super.key,
required this.users,
this.selectedUserIds = const [],
this.onRemove,
this.showRemove = false,
this.avatarRadius = 28,
});
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 12,
runSpacing: 12,
children: users
.map((user) => Chip(
avatar: ProfilePictureWidget(
userId: user.uid, radius: avatarRadius),
label: Text('${user.firstName} ${user.lastName}',
style: const TextStyle(fontSize: 16)),
labelPadding: const EdgeInsets.symmetric(horizontal: 8),
deleteIcon:
showRemove ? const Icon(Icons.close, size: 20) : null,
onDeleted: showRemove && onRemove != null
? () => onRemove!(user.uid)
: null,
backgroundColor: Colors.grey[200],
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
))
.toList(),
);
}
}