feat: Sécurisation Firestore, gestion des prix HT/TTC et refactorisation majeure

Cette mise à jour verrouille l'accès direct à Firestore depuis le client pour renforcer la sécurité et introduit une gestion complète des prix HT/TTC dans toute l'application. Elle apporte également des améliorations significatives des permissions, des optimisations de performance et de nouvelles fonctionnalités.

### Sécurité et Backend
- **Firestore Rules :** Ajout de `firestore.rules` qui bloque par défaut tous les accès en lecture/écriture depuis le client. Toutes les opérations de données doivent maintenant passer par les Cloud Functions, renforçant considérablement la sécurité.
- **Index Firestore :** Création d'un fichier `firestore.indexes.json` pour optimiser les requêtes sur la collection `events`.
- **Cloud Functions :** Les fonctions de création/mise à jour d'événements ont été adaptées pour accepter des ID de documents (utilisateurs, type d'événement) et les convertir en `DocumentReference` côté serveur, simplifiant les appels depuis le client.

### Gestion des Prix HT/TTC
- **Calcul Automatisé :** Introduction d'un helper `PriceHelpers` et d'un widget `PriceHtTtcFields` pour calculer et synchroniser automatiquement les prix HT et TTC dans le formulaire d'événement.
- **Affichage Détaillé :**
    - Les détails des événements et des options affichent désormais les prix HT, la TVA et le TTC séparément pour plus de clarté.
    - Le prix de base (`basePrice`) est maintenant traité comme un prix TTC dans toute l'application.

### Permissions et Rôles
- **Centralisation (`AppPermission`) :** Création d'une énumération `AppPermission` pour centraliser toutes les permissions de l'application, avec descriptions et catégories.
- **Rôles Prédéfinis :** Définition de rôles standards (Admin, Manager, Technicien, User) avec des jeux de permissions prédéfinis.
- **Filtre par Utilisateur :** Ajout d'un filtre par utilisateur sur la page Calendrier, visible uniquement pour les utilisateurs ayant la permission `view_all_user_events`.

### Améliorations et Optimisations (Frontend)
- **`DebugLog` :** Ajout d'un utilitaire `DebugLog` pour gérer les logs, qui sont automatiquement désactivés en mode production.
- **Optimisation du Sélecteur d'Équipement :**
    - La boîte de dialogue de sélection d'équipement a été lourdement optimisée pour éviter les reconstructions complètes de la liste lors de la sélection/désélection d'items.
    - Utilisation de `ValueNotifier` et de caches locaux (`_cachedContainers`, `_cachedEquipment`) pour des mises à jour d'UI plus ciblées et fluides.
    - La position du scroll est désormais préservée.
- **Catégorie d'Équipement :** Ajout de la catégorie `Vehicle` (Véhicule) pour les équipements.
- **Formulaires :** Les formulaires de création/modification d'événements et d'équipements ont été nettoyés de leurs logs de débogage excessifs.
This commit is contained in:
ElPoyo
2026-01-14 17:32:58 +01:00
parent fb3f41df4d
commit b30ae0f10a
40 changed files with 1759 additions and 308 deletions

View File

@@ -12,6 +12,7 @@ import 'package:em2rp/views/widgets/calendar_widgets/month_view.dart';
import 'package:em2rp/views/widgets/calendar_widgets/week_view.dart';
import 'package:em2rp/views/event_add_page.dart';
import 'package:em2rp/views/widgets/calendar_widgets/mobile_calendar_view.dart';
import 'package:em2rp/views/widgets/calendar_widgets/user_filter_dropdown.dart';
import 'package:em2rp/utils/colors.dart';
class CalendarPage extends StatefulWidget {
@@ -28,6 +29,7 @@ class _CalendarPageState extends State<CalendarPage> {
EventModel? _selectedEvent;
bool _calendarCollapsed = false;
int _selectedEventIndex = 0;
String? _selectedUserId; // Filtre par utilisateur (null = tous les événements)
@override
void initState() {
@@ -94,6 +96,26 @@ class _CalendarPageState extends State<CalendarPage> {
}
}
/// Filtre les événements selon l'utilisateur sélectionné (si filtre actif)
/// TEMPORAIREMENT DÉSACTIVÉ - À réactiver quand permission ajoutée dans Firestore
List<EventModel> _getFilteredEvents(List<EventModel> allEvents) {
if (_selectedUserId == null) {
return allEvents; // Pas de filtre, retourner tous les événements
}
// Filtrer les événements où l'utilisateur sélectionné fait partie de la workforce
return allEvents.where((event) {
return event.workforce.any((worker) {
if (worker is String) {
return worker == _selectedUserId;
}
// Si c'est une DocumentReference, on ne peut pas facilement comparer
// On suppose que les données sont chargées correctement en String
return false;
});
}).toList();
}
void _changeWeek(int delta) {
setState(() {
_focusedDay = _focusedDay.add(Duration(days: 7 * delta));
@@ -104,9 +126,13 @@ class _CalendarPageState extends State<CalendarPage> {
Widget build(BuildContext context) {
final eventProvider = Provider.of<EventProvider>(context);
final localUserProvider = Provider.of<LocalUserProvider>(context);
final isAdmin = localUserProvider.hasPermission('view_all_users');
final canCreateEvents = localUserProvider.hasPermission('create_events');
final canViewAllUserEvents = localUserProvider.hasPermission('view_all_user_events');
final isMobile = MediaQuery.of(context).size.width < 600;
// Appliquer le filtre utilisateur si actif
final filteredEvents = _getFilteredEvents(eventProvider.events);
if (eventProvider.isLoading) {
return const Scaffold(
body: Center(
@@ -120,8 +146,42 @@ class _CalendarPageState extends State<CalendarPage> {
title: "Calendrier",
),
drawer: const MainDrawer(currentPage: '/calendar'),
body: isMobile ? _buildMobileLayout() : _buildDesktopLayout(),
floatingActionButton: isAdmin
body: Column(
children: [
// Filtre utilisateur dans le corps de la page
if (canViewAllUserEvents && !isMobile)
Container(
padding: const EdgeInsets.all(16),
color: Colors.grey[100],
child: Row(
children: [
const Icon(Icons.filter_list, color: AppColors.rouge),
const SizedBox(width: 12),
const Text(
'Filtrer par utilisateur :',
style: TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
),
const SizedBox(width: 16),
Expanded(
child: UserFilterDropdown(
selectedUserId: _selectedUserId,
onUserSelected: (userId) {
setState(() {
_selectedUserId = userId;
});
},
),
),
],
),
),
// Corps du calendrier
Expanded(
child: isMobile ? _buildMobileLayout(filteredEvents) : _buildDesktopLayout(filteredEvents),
),
],
),
floatingActionButton: canCreateEvents
? FloatingActionButton(
backgroundColor: Colors.white,
onPressed: () {
@@ -140,14 +200,13 @@ class _CalendarPageState extends State<CalendarPage> {
);
}
Widget _buildDesktopLayout() {
final eventProvider = Provider.of<EventProvider>(context);
Widget _buildDesktopLayout(List<EventModel> filteredEvents) {
return Row(
children: [
// Calendrier (65% de la largeur)
Expanded(
flex: 65,
child: _buildCalendar(),
child: _buildCalendar(filteredEvents),
),
// Détails de l'événement (35% de la largeur)
Expanded(
@@ -156,7 +215,7 @@ class _CalendarPageState extends State<CalendarPage> {
? EventDetails(
event: _selectedEvent!,
selectedDate: _selectedDay,
events: eventProvider.events,
events: filteredEvents,
onSelectEvent: (event, date) {
setState(() {
_selectedEvent = event;
@@ -175,11 +234,10 @@ class _CalendarPageState extends State<CalendarPage> {
);
}
Widget _buildMobileLayout() {
final eventProvider = Provider.of<EventProvider>(context);
Widget _buildMobileLayout(List<EventModel> filteredEvents) {
final eventsForSelectedDay = _selectedDay == null
? []
: eventProvider.events
: filteredEvents
.where((e) =>
e.startDateTime.year == _selectedDay!.year &&
e.startDateTime.month == _selectedDay!.month &&
@@ -264,9 +322,9 @@ class _CalendarPageState extends State<CalendarPage> {
child: MobileCalendarView(
focusedDay: _focusedDay,
selectedDay: _selectedDay,
events: eventProvider.events,
events: filteredEvents,
onDaySelected: (day) {
final eventsForDay = eventProvider.events
final eventsForDay = filteredEvents
.where((e) =>
e.startDateTime.year == day.year &&
e.startDateTime.month == day.month &&
@@ -502,13 +560,11 @@ class _CalendarPageState extends State<CalendarPage> {
}
}
Widget _buildCalendar() {
final eventProvider = Provider.of<EventProvider>(context);
Widget _buildCalendar(List<EventModel> filteredEvents) {
if (_calendarFormat == CalendarFormat.week) {
return WeekView(
focusedDay: _focusedDay,
events: eventProvider.events,
events: filteredEvents,
onWeekChange: _changeWeek,
onEventSelected: (event) {
setState(() {
@@ -522,7 +578,7 @@ class _CalendarPageState extends State<CalendarPage> {
});
},
onDaySelected: (selectedDay) {
final eventsForDay = eventProvider.events
final eventsForDay = filteredEvents
.where((e) =>
e.startDateTime.year == selectedDay.year &&
e.startDateTime.month == selectedDay.month &&
@@ -554,9 +610,9 @@ class _CalendarPageState extends State<CalendarPage> {
focusedDay: _focusedDay,
selectedDay: _selectedDay,
calendarFormat: _calendarFormat,
events: eventProvider.events,
events: filteredEvents,
onDaySelected: (selectedDay, focusedDay) {
final eventsForDay = eventProvider.events
final eventsForDay = filteredEvents
.where((event) =>
event.startDateTime.year == selectedDay.year &&
event.startDateTime.month == selectedDay.month &&