feat: recherche d'événements et gestion avancée de la suppression d'équipement
- **Recherche d'événements** : Ajout d'une fonctionnalité de recherche (titre, description, lieu) dans le calendrier et d'une nouvelle fonction Cloud `searchEvents` avec gestion des permissions.
- **Suppression d'équipement avec forçage** :
- Mise à jour de la fonction Cloud `deleteEquipment` pour détecter les assignations à des événements futurs.
- Ajout d'une option `forceDelete` pour passer outre les conflits d'assignation.
- Création de `EquipmentDeleteUtils` pour gérer uniformément les dialogues de confirmation et les erreurs de conflit (HTTP 409).
- Intégration de la logique de suppression sécurisée dans `EquipmentDetailPage` et `EquipmentManagementPage`.
- **Calendrier** :
- Refonte de l'interface mobile pour intégrer la barre de recherche.
- Optimisation du chargement des événements lors de la sélection d'un résultat de recherche (lazy loading du mois concerné).
- Amélioration de la stabilité de la sélection d'événements et du filtrage par utilisateur.
- **Services & Providers** :
- Amélioration de la gestion des erreurs dans `ApiService` pour faciliter le re-throw des exceptions personnalisées.
- Ajout du support de la suppression forcée dans `DataService` et `EquipmentProvider`.
- **Refactoring** : Nettoyage du code, amélioration du formatage et ajout de logs de debug dans les services de données et d'équipements.
This commit is contained in:
@@ -34,16 +34,16 @@ assets/assets/images/tshirt-incrust.webp,1737393735487,af7cb34adfca19c0b41c8eb63
|
|||||||
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
|
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
|
||||||
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
|
||||||
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
|
||||||
version.json,1774883074073,049c47e9089dc5497475a6cf7733e11235bc9cfa30d458cc9a8eae761214c2b8
|
version.json,1776853346038,b9cb334972abfae63e76477e574d02e1b3cdf4210fa3edf744a8d33c6250a12e
|
||||||
flutter_service_worker.js,1774883173949,00cc791f6cc0d2beb4b16cc382b049268125aa6a7c5b73cd4bc89a003fc70f3a
|
index.html,1776853378875,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
||||||
index.html,1774883102020,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
|
flutter_service_worker.js,1776853476352,5524fe7e227e1a0ecdd4c9b14a764638a86fe836ced8d3f80ab0817d043d436a
|
||||||
flutter_bootstrap.js,1774883102005,80bbca812eb76632e250fe5c6b726db647443cbabc7f90010618e6a6f445d222
|
flutter_bootstrap.js,1776853378859,a9fbceaa97579d418c548aaa1b4fc94284bc33ef5fc2a835d80f9a96d8d6bbd8
|
||||||
assets/FontManifest.json,1774883170660,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
assets/FontManifest.json,1776853472496,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
|
||||||
assets/AssetManifest.bin,1774883170657,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907
|
assets/AssetManifest.json,1776853472495,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6
|
||||||
assets/AssetManifest.bin.json,1774883170660,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53
|
assets/AssetManifest.bin.json,1776853472495,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53
|
||||||
assets/shaders/ink_sparkle.frag,1774883170848,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
assets/AssetManifest.bin,1776853472495,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907
|
||||||
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1774883173201,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1776853475437,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
|
||||||
assets/AssetManifest.json,1774883170657,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6
|
assets/shaders/ink_sparkle.frag,1776853472722,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
|
||||||
assets/fonts/MaterialIcons-Regular.otf,1774883173207,d1409c3c8050990bdc63a413539d600245a27c9794a053c211299cc86d4f6a5c
|
assets/fonts/MaterialIcons-Regular.otf,1776853475444,213705f583b626b88f6f0c7a122a13567b6492f560ad5284176ef149f0b51fef
|
||||||
assets/NOTICES,1774883170660,1d9a08da58db7959b9607f0f1f342f96243af76dc608ed659614d586ec58cd79
|
assets/NOTICES,1776853472497,1d9a08da58db7959b9607f0f1f342f96243af76dc608ed659614d586ec58cd79
|
||||||
main.dart.js,1774883168025,bc4bc60206728a982496fe5977f48e690fe8abdfd1167a9226de18fe0052cdcf
|
main.dart.js,1776853469906,1fc20606c99d6d4dde1e7d2a8a0b2e8f2e6c6f81317b8e7e4dd17d54f71a23b2
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
Toutes les modifications notables de ce projet seront documentées dans ce fichier.
|
Toutes les modifications notables de ce projet seront documentées dans ce fichier.
|
||||||
|
|
||||||
|
## 22/04/2026
|
||||||
|
Ajout de la recherche d'événements et gestion avancée de la suppression d'équipement
|
||||||
|
|
||||||
## 30/03/2026
|
## 30/03/2026
|
||||||
Patch bug envoi d'alerte equipement perdu, date dans les alertes, recherche par ID d'équipement.
|
Patch bug envoi d'alerte equipement perdu, date dans les alertes, recherche par ID d'équipement.
|
||||||
|
|
||||||
|
|||||||
+140
-8
@@ -203,29 +203,51 @@ exports.deleteEquipment = onRequest(httpOptions, withCors(async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { equipmentId } = req.body.data;
|
const { equipmentId, forceDelete = false } = req.body.data;
|
||||||
|
|
||||||
if (!equipmentId) {
|
if (!equipmentId) {
|
||||||
res.status(400).json({ error: 'Equipment ID is required' });
|
res.status(400).json({ error: 'Equipment ID is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier si l'équipement est utilisé dans des événements actifs
|
// Vérifier si l'équipement est utilisé dans des événements à venir
|
||||||
const eventsSnapshot = await db.collection('events')
|
const eventsSnapshot = await db.collection('events')
|
||||||
.where('status', '!=', 'CANCELLED')
|
.where('status', '!=', 'CANCELLED')
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const upcomingEvents = [];
|
||||||
|
|
||||||
for (const eventDoc of eventsSnapshot.docs) {
|
for (const eventDoc of eventsSnapshot.docs) {
|
||||||
const eventData = eventDoc.data();
|
const eventData = eventDoc.data();
|
||||||
const assignedEquipment = eventData.assignedEquipment || [];
|
const assignedEquipment = eventData.assignedEquipment || [];
|
||||||
|
|
||||||
if (assignedEquipment.some(eq => eq.equipmentId === equipmentId)) {
|
if (!assignedEquipment.some(eq => eq.equipmentId === equipmentId)) {
|
||||||
res.status(409).json({
|
continue;
|
||||||
error: 'Cannot delete equipment: it is assigned to active events',
|
|
||||||
eventId: eventDoc.id
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let eventStart = null;
|
||||||
|
if (eventData.StartDateTime) {
|
||||||
|
eventStart = eventData.StartDateTime.toDate
|
||||||
|
? eventData.StartDateTime.toDate()
|
||||||
|
: new Date(eventData.StartDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventStart && eventStart > now) {
|
||||||
|
upcomingEvents.push({
|
||||||
|
eventId: eventDoc.id,
|
||||||
|
eventName: eventData.Name || '',
|
||||||
|
startDate: eventStart.toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upcomingEvents.length > 0 && !forceDelete) {
|
||||||
|
res.status(409).json({
|
||||||
|
error: 'FUTURE_EVENT_ASSIGNMENT: Cannot delete equipment because it is assigned to upcoming events',
|
||||||
|
upcomingEvents,
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.collection('equipments').doc(equipmentId).delete();
|
await db.collection('equipments').doc(equipmentId).delete();
|
||||||
@@ -1864,6 +1886,116 @@ exports.getEventsByMonth = onRequest(httpOptions, withCors(async (req, res) => {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const normalizeSearchText = (value) => {
|
||||||
|
return (value || '')
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEventStartDate = (eventData) => {
|
||||||
|
const startValue = eventData.StartDateTime;
|
||||||
|
|
||||||
|
if (!startValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startValue.toDate) {
|
||||||
|
return startValue.toDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedDate = new Date(startValue);
|
||||||
|
return Number.isNaN(parsedDate.getTime()) ? null : parsedDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEventWorkforceUids = (eventData) => {
|
||||||
|
if (!eventData.workforce || !Array.isArray(eventData.workforce)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventData.workforce
|
||||||
|
.map((userRef) => {
|
||||||
|
if (userRef && userRef.id) {
|
||||||
|
return userRef.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof userRef === 'string' && userRef.startsWith('users/')) {
|
||||||
|
return userRef.split('/')[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter((uid) => uid !== null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const serializeEventSearchResult = (doc) => {
|
||||||
|
const data = doc.data();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: doc.id,
|
||||||
|
...helpers.serializeTimestamps(data),
|
||||||
|
workforce: getEventWorkforceUids(data),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// EVENTS - Search
|
||||||
|
// ============================================================================
|
||||||
|
exports.searchEvents = onRequest(httpOptions, withCors(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const decodedToken = await auth.authenticateUser(req);
|
||||||
|
const { userId, query, limit = 20 } = req.body.data || {};
|
||||||
|
const maxResults = Number.isFinite(Number(limit)) ? Math.max(1, Number(limit)) : 20;
|
||||||
|
|
||||||
|
const normalizedQuery = normalizeSearchText(query);
|
||||||
|
if (!normalizedQuery) {
|
||||||
|
res.status(200).json({ events: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canViewAll = await auth.hasPermission(decodedToken.uid, 'view_all_events');
|
||||||
|
|
||||||
|
let eventsSnapshot;
|
||||||
|
if (canViewAll) {
|
||||||
|
eventsSnapshot = await db.collection('events').get();
|
||||||
|
} else {
|
||||||
|
const userRef = db.collection('users').doc(userId || decodedToken.uid);
|
||||||
|
eventsSnapshot = await db.collection('events')
|
||||||
|
.where('workforce', 'array-contains', userRef)
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingEvents = eventsSnapshot.docs
|
||||||
|
.filter((doc) => {
|
||||||
|
const eventData = doc.data();
|
||||||
|
const startDate = getEventStartDate(eventData);
|
||||||
|
const searchableText = normalizeSearchText([
|
||||||
|
eventData.Name,
|
||||||
|
eventData.Description,
|
||||||
|
eventData.Address,
|
||||||
|
startDate ? startDate.toLocaleString('fr-FR') : '',
|
||||||
|
startDate ? startDate.toISOString() : '',
|
||||||
|
].join(' '));
|
||||||
|
|
||||||
|
return searchableText.includes(normalizedQuery);
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const startA = getEventStartDate(a.data()) || new Date(0);
|
||||||
|
const startB = getEventStartDate(b.data()) || new Date(0);
|
||||||
|
return startA.getTime() - startB.getTime();
|
||||||
|
})
|
||||||
|
.slice(0, maxResults)
|
||||||
|
.map((doc) => serializeEventSearchResult(doc));
|
||||||
|
|
||||||
|
res.status(200).json({ events: matchingEvents });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error searching events:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Récupère un événement avec tous les détails (équipements complets + containers avec enfants)
|
* Récupère un événement avec tous les détails (équipements complets + containers avec enfants)
|
||||||
* Optimisé pour la page de préparation et l'affichage détaillé
|
* Optimisé pour la page de préparation et l'affichage détaillé
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/// Configuration de la version de l'application
|
/// Configuration de la version de l'application
|
||||||
class AppVersion {
|
class AppVersion {
|
||||||
static const String version = '1.1.20';
|
static const String version = '1.1.21';
|
||||||
|
|
||||||
/// Retourne la version complète de l'application
|
/// Retourne la version complète de l'application
|
||||||
static String get fullVersion => 'v$version';
|
static String get fullVersion => 'v$version';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
class Env {
|
class Env {
|
||||||
static const bool isDevelopment = false;
|
static const bool isDevelopment = true;
|
||||||
|
|
||||||
// Configuration de l'auto-login en développement
|
// Configuration de l'auto-login en développement
|
||||||
static const String devAdminEmail = '';
|
static const String devAdminEmail = 'paul.fournel@em2events.fr';
|
||||||
static const String devAdminPassword = '';
|
static const String devAdminPassword = 'Pastis51!';
|
||||||
|
|
||||||
// URLs et endpoints
|
// URLs et endpoints
|
||||||
static const String baseUrl = 'https://em2rp-951dc.firebaseapp.com';
|
static const String baseUrl = 'https://em2rp-951dc.firebaseapp.com';
|
||||||
@@ -14,3 +14,4 @@ class Env {
|
|||||||
// Autres configurations
|
// Autres configurations
|
||||||
static const int apiTimeout = 30000; // 30 secondes
|
static const int apiTimeout = 30000; // 30 secondes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -433,9 +433,9 @@ class EquipmentProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Supprimer un équipement
|
/// Supprimer un équipement
|
||||||
Future<void> deleteEquipment(String equipmentId) async {
|
Future<void> deleteEquipment(String equipmentId, {bool forceDelete = false}) async {
|
||||||
try {
|
try {
|
||||||
await _dataService.deleteEquipment(equipmentId);
|
await _dataService.deleteEquipment(equipmentId, forceDelete: forceDelete);
|
||||||
if (_usePagination) {
|
if (_usePagination) {
|
||||||
await reload();
|
await reload();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class EventProvider with ChangeNotifier {
|
|||||||
bool _lastCanViewAll = false;
|
bool _lastCanViewAll = false;
|
||||||
|
|
||||||
// Nouveau: Cache par mois pour le lazy loading
|
// Nouveau: Cache par mois pour le lazy loading
|
||||||
final Map<String, List<EventModel>> _eventsByMonth = {}; // "2026-02" => [events]
|
final Map<String, List<EventModel>> _eventsByMonth =
|
||||||
|
{}; // "2026-02" => [events]
|
||||||
String? _currentMonth; // Mois actuellement affiché
|
String? _currentMonth; // Mois actuellement affiché
|
||||||
|
|
||||||
List<EventModel> get events => _events;
|
List<EventModel> get events => _events;
|
||||||
@@ -28,7 +29,8 @@ class EventProvider with ChangeNotifier {
|
|||||||
/// Vérifie si les données doivent être rechargées (cache de 30 secondes)
|
/// Vérifie si les données doivent être rechargées (cache de 30 secondes)
|
||||||
bool _shouldReload(String userId, bool canViewAllEvents) {
|
bool _shouldReload(String userId, bool canViewAllEvents) {
|
||||||
if (_lastLoadTime == null) return true;
|
if (_lastLoadTime == null) return true;
|
||||||
if (_lastUserId != userId || _lastCanViewAll != canViewAllEvents) return true;
|
if (_lastUserId != userId || _lastCanViewAll != canViewAllEvents)
|
||||||
|
return true;
|
||||||
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final difference = now.difference(_lastLoadTime!);
|
final difference = now.difference(_lastLoadTime!);
|
||||||
@@ -36,12 +38,14 @@ class EventProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Charger les événements d'un utilisateur via l'API
|
/// Charger les événements d'un utilisateur via l'API
|
||||||
Future<void> loadUserEvents(String userId, {bool canViewAllEvents = false, bool forceReload = false}) async {
|
Future<void> loadUserEvents(String userId,
|
||||||
|
{bool canViewAllEvents = false, bool forceReload = false}) async {
|
||||||
PerformanceMonitor.start('EventProvider.loadUserEvents');
|
PerformanceMonitor.start('EventProvider.loadUserEvents');
|
||||||
|
|
||||||
// Éviter les rechargements inutiles
|
// Éviter les rechargements inutiles
|
||||||
if (!forceReload && !_shouldReload(userId, canViewAllEvents)) {
|
if (!forceReload && !_shouldReload(userId, canViewAllEvents)) {
|
||||||
print('Using cached events (loaded ${DateTime.now().difference(_lastLoadTime!).inSeconds}s ago)');
|
print(
|
||||||
|
'Using cached events (loaded ${DateTime.now().difference(_lastLoadTime!).inSeconds}s ago)');
|
||||||
PerformanceMonitor.end('EventProvider.loadUserEvents');
|
PerformanceMonitor.end('EventProvider.loadUserEvents');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -50,7 +54,8 @@ class EventProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
print('Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)');
|
print(
|
||||||
|
'Loading events for user: $userId (canViewAllEvents: $canViewAllEvents)');
|
||||||
|
|
||||||
PerformanceMonitor.start('EventProvider.getEvents_API');
|
PerformanceMonitor.start('EventProvider.getEvents_API');
|
||||||
// Charger via l'API - les permissions sont vérifiées côté serveur
|
// Charger via l'API - les permissions sont vérifiées côté serveur
|
||||||
@@ -61,9 +66,8 @@ class EventProvider with ChangeNotifier {
|
|||||||
final usersData = result['users'] as Map<String, dynamic>;
|
final usersData = result['users'] as Map<String, dynamic>;
|
||||||
|
|
||||||
// Stocker les utilisateurs dans le cache
|
// Stocker les utilisateurs dans le cache
|
||||||
_usersCache = usersData.map((key, value) =>
|
_usersCache = usersData
|
||||||
MapEntry(key, value as Map<String, dynamic>)
|
.map((key, value) => MapEntry(key, value as Map<String, dynamic>));
|
||||||
);
|
|
||||||
|
|
||||||
print('Found ${eventsData.length} events from API');
|
print('Found ${eventsData.length} events from API');
|
||||||
|
|
||||||
@@ -74,7 +78,8 @@ class EventProvider with ChangeNotifier {
|
|||||||
// Parser chaque événement
|
// Parser chaque événement
|
||||||
for (var eventData in eventsData) {
|
for (var eventData in eventsData) {
|
||||||
try {
|
try {
|
||||||
final event = EventModel.fromMap(eventData, eventData['id'] as String);
|
final event =
|
||||||
|
EventModel.fromMap(eventData, eventData['id'] as String);
|
||||||
allEvents.add(event);
|
allEvents.add(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Failed to parse event ${eventData['id']}: $e');
|
print('Failed to parse event ${eventData['id']}: $e');
|
||||||
@@ -88,7 +93,8 @@ class EventProvider with ChangeNotifier {
|
|||||||
_lastUserId = userId;
|
_lastUserId = userId;
|
||||||
_lastCanViewAll = canViewAllEvents;
|
_lastCanViewAll = canViewAllEvents;
|
||||||
|
|
||||||
print('Successfully loaded ${_events.length} events ($failedCount failed)');
|
print(
|
||||||
|
'Successfully loaded ${_events.length} events ($failedCount failed)');
|
||||||
|
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -104,8 +110,9 @@ class EventProvider with ChangeNotifier {
|
|||||||
|
|
||||||
/// Charger les événements d'un mois spécifique (lazy loading optimisé)
|
/// Charger les événements d'un mois spécifique (lazy loading optimisé)
|
||||||
Future<void> loadMonthEvents(String userId, int year, int month,
|
Future<void> loadMonthEvents(String userId, int year, int month,
|
||||||
{bool canViewAllEvents = false, bool forceReload = false, bool silent = false}) async {
|
{bool canViewAllEvents = false,
|
||||||
|
bool forceReload = false,
|
||||||
|
bool silent = false}) async {
|
||||||
final monthKey = '$year-${month.toString().padLeft(2, '0')}';
|
final monthKey = '$year-${month.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
// Vérifier le cache
|
// Vérifier le cache
|
||||||
@@ -130,19 +137,15 @@ class EventProvider with ChangeNotifier {
|
|||||||
|
|
||||||
PerformanceMonitor.start('EventProvider.loadMonthEvents_API');
|
PerformanceMonitor.start('EventProvider.loadMonthEvents_API');
|
||||||
final result = await _dataService.getEventsByMonth(
|
final result = await _dataService.getEventsByMonth(
|
||||||
userId: userId,
|
userId: userId, year: year, month: month);
|
||||||
year: year,
|
|
||||||
month: month
|
|
||||||
);
|
|
||||||
PerformanceMonitor.end('EventProvider.loadMonthEvents_API');
|
PerformanceMonitor.end('EventProvider.loadMonthEvents_API');
|
||||||
|
|
||||||
final eventsData = result['events'] as List<Map<String, dynamic>>;
|
final eventsData = result['events'] as List<Map<String, dynamic>>;
|
||||||
final usersData = result['users'] as Map<String, dynamic>;
|
final usersData = result['users'] as Map<String, dynamic>;
|
||||||
|
|
||||||
// Mettre à jour le cache utilisateurs (addAll pour cumuler)
|
// Mettre à jour le cache utilisateurs (addAll pour cumuler)
|
||||||
_usersCache.addAll(
|
_usersCache.addAll(usersData
|
||||||
usersData.map((key, value) => MapEntry(key, value as Map<String, dynamic>))
|
.map((key, value) => MapEntry(key, value as Map<String, dynamic>)));
|
||||||
);
|
|
||||||
|
|
||||||
print('[EventProvider] Found ${eventsData.length} events for $monthKey');
|
print('[EventProvider] Found ${eventsData.length} events for $monthKey');
|
||||||
|
|
||||||
@@ -153,7 +156,8 @@ class EventProvider with ChangeNotifier {
|
|||||||
// Parser les événements
|
// Parser les événements
|
||||||
for (var eventData in eventsData) {
|
for (var eventData in eventsData) {
|
||||||
try {
|
try {
|
||||||
final event = EventModel.fromMap(eventData, eventData['id'] as String);
|
final event =
|
||||||
|
EventModel.fromMap(eventData, eventData['id'] as String);
|
||||||
monthEvents.add(event);
|
monthEvents.add(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[EventProvider] Failed to parse event ${eventData['id']}: $e');
|
print('[EventProvider] Failed to parse event ${eventData['id']}: $e');
|
||||||
@@ -176,7 +180,8 @@ class EventProvider with ChangeNotifier {
|
|||||||
_lastUserId = userId;
|
_lastUserId = userId;
|
||||||
_lastCanViewAll = canViewAllEvents;
|
_lastCanViewAll = canViewAllEvents;
|
||||||
|
|
||||||
print('[EventProvider] Successfully loaded ${monthEvents.length} events for $monthKey ($failedCount failed)');
|
print(
|
||||||
|
'[EventProvider] Successfully loaded ${monthEvents.length} events for $monthKey ($failedCount failed)');
|
||||||
|
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
@@ -195,7 +200,6 @@ class EventProvider with ChangeNotifier {
|
|||||||
/// Précharger les mois adjacents en arrière-plan
|
/// Précharger les mois adjacents en arrière-plan
|
||||||
void preloadAdjacentMonths(String userId, int year, int month,
|
void preloadAdjacentMonths(String userId, int year, int month,
|
||||||
{bool canViewAllEvents = false}) {
|
{bool canViewAllEvents = false}) {
|
||||||
|
|
||||||
// Mois précédent
|
// Mois précédent
|
||||||
final prevMonth = month == 1 ? 12 : month - 1;
|
final prevMonth = month == 1 ? 12 : month - 1;
|
||||||
final prevYear = month == 1 ? year - 1 : year;
|
final prevYear = month == 1 ? year - 1 : year;
|
||||||
@@ -230,8 +234,10 @@ class EventProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Recharger les événements (utilise le dernier userId)
|
/// Recharger les événements (utilise le dernier userId)
|
||||||
Future<void> refreshEvents(String userId, {bool canViewAllEvents = false}) async {
|
Future<void> refreshEvents(String userId,
|
||||||
await loadUserEvents(userId, canViewAllEvents: canViewAllEvents, forceReload: true);
|
{bool canViewAllEvents = false}) async {
|
||||||
|
await loadUserEvents(userId,
|
||||||
|
canViewAllEvents: canViewAllEvents, forceReload: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Récupérer un événement spécifique par ID
|
/// Récupérer un événement spécifique par ID
|
||||||
@@ -243,6 +249,41 @@ class EventProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recherche des événements accessibles à l'utilisateur.
|
||||||
|
Future<List<EventModel>> searchEvents({
|
||||||
|
required String userId,
|
||||||
|
required String query,
|
||||||
|
int limit = 20,
|
||||||
|
}) async {
|
||||||
|
final trimmedQuery = query.trim();
|
||||||
|
if (trimmedQuery.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await _dataService.searchEvents(
|
||||||
|
userId: userId,
|
||||||
|
query: trimmedQuery,
|
||||||
|
limit: limit,
|
||||||
|
);
|
||||||
|
|
||||||
|
final events = <EventModel>[];
|
||||||
|
for (final eventData in result) {
|
||||||
|
try {
|
||||||
|
final eventId = eventData['id'] as String?;
|
||||||
|
if (eventId == null || eventId.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
events.add(EventModel.fromMap(eventData, eventId));
|
||||||
|
} catch (e) {
|
||||||
|
print('Failed to parse searched event ${eventData['id']}: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
events.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
/// Ajouter un nouvel événement
|
/// Ajouter un nouvel événement
|
||||||
Future<void> addEvent(EventModel event) async {
|
Future<void> addEvent(EventModel event) async {
|
||||||
try {
|
try {
|
||||||
@@ -250,7 +291,8 @@ class EventProvider with ChangeNotifier {
|
|||||||
_events.add(event);
|
_events.add(event);
|
||||||
|
|
||||||
// Ajouter dans le cache par mois
|
// Ajouter dans le cache par mois
|
||||||
final monthKey = '${event.startDateTime.year}-${event.startDateTime.month.toString().padLeft(2, '0')}';
|
final monthKey =
|
||||||
|
'${event.startDateTime.year}-${event.startDateTime.month.toString().padLeft(2, '0')}';
|
||||||
if (_eventsByMonth.containsKey(monthKey)) {
|
if (_eventsByMonth.containsKey(monthKey)) {
|
||||||
_eventsByMonth[monthKey]!.add(event);
|
_eventsByMonth[monthKey]!.add(event);
|
||||||
}
|
}
|
||||||
@@ -272,8 +314,10 @@ class EventProvider with ChangeNotifier {
|
|||||||
_events[index] = event;
|
_events[index] = event;
|
||||||
|
|
||||||
// Mettre à jour dans le cache par mois
|
// Mettre à jour dans le cache par mois
|
||||||
final oldMonthKey = '${oldEvent.startDateTime.year}-${oldEvent.startDateTime.month.toString().padLeft(2, '0')}';
|
final oldMonthKey =
|
||||||
final newMonthKey = '${event.startDateTime.year}-${event.startDateTime.month.toString().padLeft(2, '0')}';
|
'${oldEvent.startDateTime.year}-${oldEvent.startDateTime.month.toString().padLeft(2, '0')}';
|
||||||
|
final newMonthKey =
|
||||||
|
'${event.startDateTime.year}-${event.startDateTime.month.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
// Si le mois a changé, supprimer de l'ancien et ajouter au nouveau
|
// Si le mois a changé, supprimer de l'ancien et ajouter au nouveau
|
||||||
if (oldMonthKey != newMonthKey) {
|
if (oldMonthKey != newMonthKey) {
|
||||||
@@ -286,7 +330,8 @@ class EventProvider with ChangeNotifier {
|
|||||||
} else {
|
} else {
|
||||||
// Même mois, juste mettre à jour
|
// Même mois, juste mettre à jour
|
||||||
if (_eventsByMonth.containsKey(newMonthKey)) {
|
if (_eventsByMonth.containsKey(newMonthKey)) {
|
||||||
final monthIndex = _eventsByMonth[newMonthKey]!.indexWhere((e) => e.id == event.id);
|
final monthIndex = _eventsByMonth[newMonthKey]!
|
||||||
|
.indexWhere((e) => e.id == event.id);
|
||||||
if (monthIndex != -1) {
|
if (monthIndex != -1) {
|
||||||
_eventsByMonth[newMonthKey]![monthIndex] = event;
|
_eventsByMonth[newMonthKey]![monthIndex] = event;
|
||||||
}
|
}
|
||||||
@@ -308,7 +353,8 @@ class EventProvider with ChangeNotifier {
|
|||||||
|
|
||||||
// Trouver l'événement pour obtenir sa date avant de le supprimer
|
// Trouver l'événement pour obtenir sa date avant de le supprimer
|
||||||
final eventToDelete = _events.firstWhere((e) => e.id == eventId);
|
final eventToDelete = _events.firstWhere((e) => e.id == eventId);
|
||||||
final monthKey = '${eventToDelete.startDateTime.year}-${eventToDelete.startDateTime.month.toString().padLeft(2, '0')}';
|
final monthKey =
|
||||||
|
'${eventToDelete.startDateTime.year}-${eventToDelete.startDateTime.month.toString().padLeft(2, '0')}';
|
||||||
|
|
||||||
// Supprimer de _events
|
// Supprimer de _events
|
||||||
_events.removeWhere((event) => event.id == eventId);
|
_events.removeWhere((event) => event.id == eventId);
|
||||||
|
|||||||
@@ -173,6 +173,8 @@ class FirebaseFunctionsApiService implements ApiService {
|
|||||||
statusCode: response.statusCode,
|
statusCode: response.statusCode,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} on ApiException {
|
||||||
|
rethrow;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLog.error('[API] Error during request: $functionName', e);
|
DebugLog.error('[API] Error during request: $functionName', e);
|
||||||
throw ApiException(
|
throw ApiException(
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ class DataService {
|
|||||||
if (eventTypes == null) return [];
|
if (eventTypes == null) return [];
|
||||||
return eventTypes.map((e) => e as Map<String, dynamic>).toList();
|
return eventTypes.map((e) => e as Map<String, dynamic>).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Erreur lors de la récupération des types d\'événements: $e');
|
throw Exception(
|
||||||
|
'Erreur lors de la récupération des types d\'événements: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,15 +56,18 @@ class DataService {
|
|||||||
try {
|
try {
|
||||||
final data = <String, dynamic>{'eventId': eventId};
|
final data = <String, dynamic>{'eventId': eventId};
|
||||||
|
|
||||||
if (assignedEquipment != null) data['assignedEquipment'] = assignedEquipment;
|
if (assignedEquipment != null)
|
||||||
if (preparationStatus != null) data['preparationStatus'] = preparationStatus;
|
data['assignedEquipment'] = assignedEquipment;
|
||||||
|
if (preparationStatus != null)
|
||||||
|
data['preparationStatus'] = preparationStatus;
|
||||||
if (loadingStatus != null) data['loadingStatus'] = loadingStatus;
|
if (loadingStatus != null) data['loadingStatus'] = loadingStatus;
|
||||||
if (unloadingStatus != null) data['unloadingStatus'] = unloadingStatus;
|
if (unloadingStatus != null) data['unloadingStatus'] = unloadingStatus;
|
||||||
if (returnStatus != null) data['returnStatus'] = returnStatus;
|
if (returnStatus != null) data['returnStatus'] = returnStatus;
|
||||||
|
|
||||||
await _apiService.call('updateEventEquipment', data);
|
await _apiService.call('updateEventEquipment', data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Erreur lors de la mise à jour des équipements de l\'événement: $e');
|
throw Exception(
|
||||||
|
'Erreur lors de la mise à jour des équipements de l\'événement: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,11 +81,13 @@ class DataService {
|
|||||||
final data = <String, dynamic>{'equipmentId': equipmentId};
|
final data = <String, dynamic>{'equipmentId': equipmentId};
|
||||||
|
|
||||||
if (status != null) data['status'] = status;
|
if (status != null) data['status'] = status;
|
||||||
if (availableQuantity != null) data['availableQuantity'] = availableQuantity;
|
if (availableQuantity != null)
|
||||||
|
data['availableQuantity'] = availableQuantity;
|
||||||
|
|
||||||
await _apiService.call('updateEquipmentStatusOnly', data);
|
await _apiService.call('updateEquipmentStatusOnly', data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Erreur lors de la mise à jour du statut de l\'équipement: $e');
|
throw Exception(
|
||||||
|
'Erreur lors de la mise à jour du statut de l\'équipement: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +112,8 @@ class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Crée un équipement
|
/// Crée un équipement
|
||||||
Future<void> createEquipment(String equipmentId, Map<String, dynamic> data) async {
|
Future<void> createEquipment(
|
||||||
|
String equipmentId, Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
// S'assurer que l'ID est dans les données
|
// S'assurer que l'ID est dans les données
|
||||||
final equipmentData = Map<String, dynamic>.from(data);
|
final equipmentData = Map<String, dynamic>.from(data);
|
||||||
@@ -119,7 +126,8 @@ class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Met à jour un équipement
|
/// Met à jour un équipement
|
||||||
Future<void> updateEquipment(String equipmentId, Map<String, dynamic> data) async {
|
Future<void> updateEquipment(
|
||||||
|
String equipmentId, Map<String, dynamic> data) async {
|
||||||
try {
|
try {
|
||||||
await _apiService.call('updateEquipment', {
|
await _apiService.call('updateEquipment', {
|
||||||
'equipmentId': equipmentId,
|
'equipmentId': equipmentId,
|
||||||
@@ -131,18 +139,26 @@ class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Supprime un équipement
|
/// Supprime un équipement
|
||||||
Future<void> deleteEquipment(String equipmentId) async {
|
Future<void> deleteEquipment(String equipmentId,
|
||||||
|
{bool forceDelete = false}) async {
|
||||||
try {
|
try {
|
||||||
await _apiService.call('deleteEquipment', {'equipmentId': equipmentId});
|
await _apiService.call('deleteEquipment', {
|
||||||
|
'equipmentId': equipmentId,
|
||||||
|
'forceDelete': forceDelete,
|
||||||
|
});
|
||||||
|
} on ApiException {
|
||||||
|
rethrow;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Erreur lors de la suppression de l\'équipement: $e');
|
throw Exception('Erreur lors de la suppression de l\'équipement: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Récupère les événements utilisant un type d'événement donné
|
/// Récupère les événements utilisant un type d'événement donné
|
||||||
Future<List<Map<String, dynamic>>> getEventsByEventType(String eventTypeId) async {
|
Future<List<Map<String, dynamic>>> getEventsByEventType(
|
||||||
|
String eventTypeId) async {
|
||||||
try {
|
try {
|
||||||
final result = await _apiService.call('getEventsByEventType', {'eventTypeId': eventTypeId});
|
final result = await _apiService
|
||||||
|
.call('getEventsByEventType', {'eventTypeId': eventTypeId});
|
||||||
final events = result['events'] as List<dynamic>?;
|
final events = result['events'] as List<dynamic>?;
|
||||||
if (events == null) return [];
|
if (events == null) return [];
|
||||||
return events.map((e) => e as Map<String, dynamic>).toList();
|
return events.map((e) => e as Map<String, dynamic>).toList();
|
||||||
@@ -271,7 +287,8 @@ class DataService {
|
|||||||
final events = result['events'] as List<dynamic>? ?? [];
|
final events = result['events'] as List<dynamic>? ?? [];
|
||||||
final users = result['users'] as Map<String, dynamic>? ?? {};
|
final users = result['users'] as Map<String, dynamic>? ?? {};
|
||||||
|
|
||||||
print('[DataService] Events loaded for $year-$month: ${events.length} events');
|
print(
|
||||||
|
'[DataService] Events loaded for $year-$month: ${events.length} events');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'events': events.map((e) => e as Map<String, dynamic>).toList(),
|
'events': events.map((e) => e as Map<String, dynamic>).toList(),
|
||||||
@@ -279,7 +296,32 @@ class DataService {
|
|||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[DataService] Error getting events by month: $e');
|
print('[DataService] Error getting events by month: $e');
|
||||||
throw Exception('Erreur lors de la récupération des événements du mois: $e');
|
throw Exception(
|
||||||
|
'Erreur lors de la récupération des événements du mois: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recherche des événements accessibles à l'utilisateur.
|
||||||
|
Future<List<Map<String, dynamic>>> searchEvents({
|
||||||
|
required String userId,
|
||||||
|
required String query,
|
||||||
|
int limit = 20,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final result = await _apiService.call('searchEvents', {
|
||||||
|
'userId': userId,
|
||||||
|
'query': query,
|
||||||
|
'limit': limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
final events = result['events'] as List<dynamic>?;
|
||||||
|
if (events == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return events.map((e) => e as Map<String, dynamic>).toList();
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Erreur lors de la recherche d\'événements: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +341,8 @@ class DataService {
|
|||||||
throw Exception('Event not found');
|
throw Exception('Event not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
print('[DataService] Event loaded with ${equipments.length} equipments and ${containers.length} containers');
|
print(
|
||||||
|
'[DataService] Event loaded with ${equipments.length} equipments and ${containers.length} containers');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'event': event,
|
'event': event,
|
||||||
@@ -308,7 +351,8 @@ class DataService {
|
|||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[DataService] Error getting event with details: $e');
|
print('[DataService] Error getting event with details: $e');
|
||||||
throw Exception('Erreur lors de la récupération de l\'événement avec détails: $e');
|
throw Exception(
|
||||||
|
'Erreur lors de la récupération de l\'événement avec détails: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,11 +376,13 @@ class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Récupère plusieurs équipements par leurs IDs
|
/// Récupère plusieurs équipements par leurs IDs
|
||||||
Future<List<Map<String, dynamic>>> getEquipmentsByIds(List<String> equipmentIds) async {
|
Future<List<Map<String, dynamic>>> getEquipmentsByIds(
|
||||||
|
List<String> equipmentIds) async {
|
||||||
try {
|
try {
|
||||||
if (equipmentIds.isEmpty) return [];
|
if (equipmentIds.isEmpty) return [];
|
||||||
|
|
||||||
print('[DataService] Getting equipments by IDs: ${equipmentIds.length} items');
|
print(
|
||||||
|
'[DataService] Getting equipments by IDs: ${equipmentIds.length} items');
|
||||||
final result = await _apiService.call('getEquipmentsByIds', {
|
final result = await _apiService.call('getEquipmentsByIds', {
|
||||||
'equipmentIds': equipmentIds,
|
'equipmentIds': equipmentIds,
|
||||||
});
|
});
|
||||||
@@ -366,11 +412,13 @@ class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Récupère plusieurs containers par leurs IDs
|
/// Récupère plusieurs containers par leurs IDs
|
||||||
Future<List<Map<String, dynamic>>> getContainersByIds(List<String> containerIds) async {
|
Future<List<Map<String, dynamic>>> getContainersByIds(
|
||||||
|
List<String> containerIds) async {
|
||||||
try {
|
try {
|
||||||
if (containerIds.isEmpty) return [];
|
if (containerIds.isEmpty) return [];
|
||||||
|
|
||||||
print('[DataService] Getting containers by IDs: ${containerIds.length} items');
|
print(
|
||||||
|
'[DataService] Getting containers by IDs: ${containerIds.length} items');
|
||||||
final result = await _apiService.call('getContainersByIds', {
|
final result = await _apiService.call('getContainersByIds', {
|
||||||
'containerIds': containerIds,
|
'containerIds': containerIds,
|
||||||
});
|
});
|
||||||
@@ -415,22 +463,25 @@ class DataService {
|
|||||||
params['searchQuery'] = searchQuery;
|
params['searchQuery'] = searchQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await (_apiService as FirebaseFunctionsApiService).callPaginated(
|
final result =
|
||||||
|
await (_apiService as FirebaseFunctionsApiService).callPaginated(
|
||||||
'getEquipmentsPaginated',
|
'getEquipmentsPaginated',
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'equipments': (result['equipments'] as List<dynamic>?)
|
'equipments': (result['equipments'] as List<dynamic>?)
|
||||||
?.map((e) => e as Map<String, dynamic>)
|
?.map((e) => e as Map<String, dynamic>)
|
||||||
.toList() ?? [],
|
.toList() ??
|
||||||
|
[],
|
||||||
'hasMore': result['hasMore'] as bool? ?? false,
|
'hasMore': result['hasMore'] as bool? ?? false,
|
||||||
'lastVisible': result['lastVisible'] as String?,
|
'lastVisible': result['lastVisible'] as String?,
|
||||||
'total': result['total'] as int? ?? 0,
|
'total': result['total'] as int? ?? 0,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLog.error('[DataService] Error in getEquipmentsPaginated', e);
|
DebugLog.error('[DataService] Error in getEquipmentsPaginated', e);
|
||||||
throw Exception('Erreur lors de la récupération paginée des équipements: $e');
|
throw Exception(
|
||||||
|
'Erreur lors de la récupération paginée des équipements: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,22 +511,25 @@ class DataService {
|
|||||||
params['searchQuery'] = searchQuery;
|
params['searchQuery'] = searchQuery;
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await (_apiService as FirebaseFunctionsApiService).callPaginated(
|
final result =
|
||||||
|
await (_apiService as FirebaseFunctionsApiService).callPaginated(
|
||||||
'getContainersPaginated',
|
'getContainersPaginated',
|
||||||
params,
|
params,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'containers': (result['containers'] as List<dynamic>?)
|
'containers': (result['containers'] as List<dynamic>?)
|
||||||
?.map((e) => e as Map<String, dynamic>)
|
?.map((e) => e as Map<String, dynamic>)
|
||||||
.toList() ?? [],
|
.toList() ??
|
||||||
|
[],
|
||||||
'hasMore': result['hasMore'] as bool? ?? false,
|
'hasMore': result['hasMore'] as bool? ?? false,
|
||||||
'lastVisible': result['lastVisible'] as String?,
|
'lastVisible': result['lastVisible'] as String?,
|
||||||
'total': result['total'] as int? ?? 0,
|
'total': result['total'] as int? ?? 0,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLog.error('[DataService] Error in getContainersPaginated', e);
|
DebugLog.error('[DataService] Error in getContainersPaginated', e);
|
||||||
throw Exception('Erreur lors de la récupération paginée des containers: $e');
|
throw Exception(
|
||||||
|
'Erreur lors de la récupération paginée des containers: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +566,8 @@ class DataService {
|
|||||||
return result['user'] as Map<String, dynamic>;
|
return result['user'] as Map<String, dynamic>;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[DataService] Error getting current user: $e');
|
print('[DataService] Error getting current user: $e');
|
||||||
throw Exception('Erreur lors de la récupération de l\'utilisateur actuel: $e');
|
throw Exception(
|
||||||
|
'Erreur lors de la récupération de l\'utilisateur actuel: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,7 +648,8 @@ class DataService {
|
|||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Erreur lors de la récupération des équipements en conflit: $e');
|
throw Exception(
|
||||||
|
'Erreur lors de la récupération des équipements en conflit: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,7 +658,8 @@ class DataService {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Récupère toutes les maintenances
|
/// Récupère toutes les maintenances
|
||||||
Future<List<Map<String, dynamic>>> getMaintenances({String? equipmentId}) async {
|
Future<List<Map<String, dynamic>>> getMaintenances(
|
||||||
|
{String? equipmentId}) async {
|
||||||
try {
|
try {
|
||||||
final data = <String, dynamic>{};
|
final data = <String, dynamic>{};
|
||||||
if (equipmentId != null) data['equipmentId'] = equipmentId;
|
if (equipmentId != null) data['equipmentId'] = equipmentId;
|
||||||
@@ -619,14 +676,16 @@ class DataService {
|
|||||||
/// Supprime une maintenance
|
/// Supprime une maintenance
|
||||||
Future<void> deleteMaintenance(String maintenanceId) async {
|
Future<void> deleteMaintenance(String maintenanceId) async {
|
||||||
try {
|
try {
|
||||||
await _apiService.call('deleteMaintenance', {'maintenanceId': maintenanceId});
|
await _apiService
|
||||||
|
.call('deleteMaintenance', {'maintenanceId': maintenanceId});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Erreur lors de la suppression de la maintenance: $e');
|
throw Exception('Erreur lors de la suppression de la maintenance: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Récupère les containers contenant un équipement
|
/// Récupère les containers contenant un équipement
|
||||||
Future<List<Map<String, dynamic>>> getContainersByEquipment(String equipmentId) async {
|
Future<List<Map<String, dynamic>>> getContainersByEquipment(
|
||||||
|
String equipmentId) async {
|
||||||
try {
|
try {
|
||||||
final result = await _apiService.call('getContainersByEquipment', {
|
final result = await _apiService.call('getContainersByEquipment', {
|
||||||
'equipmentId': equipmentId,
|
'equipmentId': equipmentId,
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import 'package:em2rp/services/api_service.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Utilitaires partages pour la suppression d'equipement avec forcage.
|
||||||
|
class EquipmentDeleteUtils {
|
||||||
|
static const String _legacyConflictToken = 'future_event_assignment';
|
||||||
|
static const List<String> _conflictMessageTokens = [
|
||||||
|
'cannot delete equipment because it is assigned to upcoming events',
|
||||||
|
'cannot delete equipment because it is assigned to future events',
|
||||||
|
'assigned to upcoming events',
|
||||||
|
'assigned to future events',
|
||||||
|
];
|
||||||
|
|
||||||
|
static const String deleteDialogTitle = 'Confirmer la suppression';
|
||||||
|
static const String deleteDialogCancelLabel = 'Annuler';
|
||||||
|
static const String deleteDialogConfirmLabel = 'Supprimer';
|
||||||
|
static const String deleteSuccessMessage = 'Équipement supprimé avec succès';
|
||||||
|
|
||||||
|
/// Retourne [name] si renseigne, sinon [id].
|
||||||
|
static String resolveEquipmentLabel({required String id, String? name}) {
|
||||||
|
final trimmedName = name?.trim();
|
||||||
|
if (trimmedName == null || trimmedName.isEmpty) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
return trimmedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit le message de confirmation de suppression d'un equipement.
|
||||||
|
static String buildSingleDeleteConfirmationMessage(String equipmentLabel) {
|
||||||
|
return 'Voulez-vous vraiment supprimer "$equipmentLabel" ?\n\n'
|
||||||
|
'Cette action est irréversible.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit le message de confirmation de suppression multiple.
|
||||||
|
static String buildBulkDeleteConfirmationMessage(int selectedCount) {
|
||||||
|
return 'Voulez-vous vraiment supprimer $selectedCount équipement(s) ?\n\n'
|
||||||
|
'Cette action est irréversible.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit le message de succes de suppression multiple.
|
||||||
|
static String buildBulkDeleteSuccessMessage(int deletedCount) {
|
||||||
|
return '$deletedCount équipement(s) supprimé(s) avec succès';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construit un message d'erreur de suppression homogene.
|
||||||
|
static String buildDeleteErrorMessage(Object error) {
|
||||||
|
return 'Erreur lors de la suppression : $error';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indique si l'erreur correspond a un conflit de suppression 409.
|
||||||
|
static bool isFutureAssignmentDeleteConflict(Object error) {
|
||||||
|
if (error is ApiException && !error.isConflict) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalizedMessage = _normalizeErrorMessage(error);
|
||||||
|
if (normalizedMessage.contains(_legacyConflictToken)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _conflictMessageTokens.any(normalizedMessage.contains);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Affiche la confirmation de suppression forcee.
|
||||||
|
static Future<bool> showForceDeleteDialog(
|
||||||
|
BuildContext context, {
|
||||||
|
required String equipmentLabel,
|
||||||
|
}) async {
|
||||||
|
final shouldForceDelete = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: const Text('Équipement utilisé dans un événement à venir'),
|
||||||
|
content: Text(
|
||||||
|
'"$equipmentLabel" est assigné à au moins un événement à venir.\n\n'
|
||||||
|
'Voulez-vous forcer la suppression ?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(dialogContext, false),
|
||||||
|
child: const Text('Annuler'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(dialogContext, true),
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
child: const Text('Forcer la suppression'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return shouldForceDelete == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute une suppression, puis propose un forcage en cas de conflit 409.
|
||||||
|
static Future<bool> deleteWithFutureAssignmentCheck({
|
||||||
|
required BuildContext context,
|
||||||
|
required String equipmentLabel,
|
||||||
|
required Future<void> Function({bool forceDelete}) deleteEquipment,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await deleteEquipment(forceDelete: false);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (!isFutureAssignmentDeleteConflict(error)) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.mounted) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final shouldForceDelete = await showForceDeleteDialog(
|
||||||
|
context,
|
||||||
|
equipmentLabel: equipmentLabel,
|
||||||
|
);
|
||||||
|
if (!shouldForceDelete) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteEquipment(forceDelete: true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _normalizeErrorMessage(Object error) {
|
||||||
|
if (error is ApiException) {
|
||||||
|
return error.message.toLowerCase();
|
||||||
|
}
|
||||||
|
return error.toString().toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:em2rp/providers/local_user_provider.dart';
|
import 'package:em2rp/providers/local_user_provider.dart';
|
||||||
@@ -10,6 +11,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:table_calendar/table_calendar.dart';
|
import 'package:table_calendar/table_calendar.dart';
|
||||||
import 'package:em2rp/models/event_model.dart';
|
import 'package:em2rp/models/event_model.dart';
|
||||||
import 'package:em2rp/views/widgets/calendar_widgets/event_details.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/event_details.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:em2rp/views/widgets/calendar_widgets/month_view.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/month_view.dart';
|
||||||
import 'package:em2rp/views/widgets/calendar_widgets/week_view.dart';
|
import 'package:em2rp/views/widgets/calendar_widgets/week_view.dart';
|
||||||
@@ -40,6 +42,14 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
int _selectedEventIndex = 0;
|
int _selectedEventIndex = 0;
|
||||||
String?
|
String?
|
||||||
_selectedUserId; // Filtre par utilisateur (null = tous les événements)
|
_selectedUserId; // Filtre par utilisateur (null = tous les événements)
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
Timer? _searchDebounce;
|
||||||
|
List<EventModel> _searchResults = [];
|
||||||
|
String _searchQuery = '';
|
||||||
|
String? _searchError;
|
||||||
|
bool _isSearching = false;
|
||||||
|
int _searchRequestId = 0;
|
||||||
|
bool _isMobileSearchVisible = false;
|
||||||
bool _isRefreshing = false;
|
bool _isRefreshing = false;
|
||||||
double _detailsPaneFraction = 0.35;
|
double _detailsPaneFraction = 0.35;
|
||||||
|
|
||||||
@@ -105,21 +115,6 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Charge les événements de manière asynchrone et sélectionne l'événement approprié
|
|
||||||
/// DEPRECATED: Utiliser _loadCurrentMonthEvents à la place
|
|
||||||
Future<void> _loadEventsAsync() async {
|
|
||||||
PerformanceMonitor.start('CalendarPage.loadEventsAsync');
|
|
||||||
await _loadEvents();
|
|
||||||
|
|
||||||
// Sélectionner l'événement approprié après le chargement
|
|
||||||
if (mounted) {
|
|
||||||
PerformanceMonitor.start('CalendarPage.selectDefaultEvent');
|
|
||||||
_selectDefaultEvent();
|
|
||||||
PerformanceMonitor.end('CalendarPage.selectDefaultEvent');
|
|
||||||
}
|
|
||||||
PerformanceMonitor.end('CalendarPage.loadEventsAsync');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sélectionne automatiquement l'événement le plus proche de maintenant
|
/// Sélectionne automatiquement l'événement le plus proche de maintenant
|
||||||
void _selectDefaultEvent() {
|
void _selectDefaultEvent() {
|
||||||
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
final eventProvider = Provider.of<EventProvider>(context, listen: false);
|
||||||
@@ -188,9 +183,15 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filtre les événements selon l'utilisateur sélectionné (si filtre actif)
|
@override
|
||||||
/// TEMPORAIREMENT DÉSACTIVÉ - À réactiver quand permission ajoutée dans Firestore
|
void dispose() {
|
||||||
List<EventModel> _getFilteredEvents(List<EventModel> allEvents) {
|
_searchDebounce?.cancel();
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filtre les événements selon l'utilisateur sélectionné (si filtre actif).
|
||||||
|
List<EventModel> _filterEventsByUser(List<EventModel> allEvents) {
|
||||||
if (_selectedUserId == null) {
|
if (_selectedUserId == null) {
|
||||||
return allEvents; // Pas de filtre, retourner tous les événements
|
return allEvents; // Pas de filtre, retourner tous les événements
|
||||||
}
|
}
|
||||||
@@ -208,6 +209,524 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isSameDay(DateTime left, DateTime right) {
|
||||||
|
return left.year == right.year &&
|
||||||
|
left.month == right.month &&
|
||||||
|
left.day == right.day;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<EventModel> _getEventsForDay(
|
||||||
|
List<EventModel> events,
|
||||||
|
DateTime? day, {
|
||||||
|
EventModel? selectedEvent,
|
||||||
|
}) {
|
||||||
|
if (day == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final dayEvents = events
|
||||||
|
.where((event) => _isSameDay(event.startDateTime, day))
|
||||||
|
.toList()
|
||||||
|
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||||
|
|
||||||
|
if (selectedEvent != null &&
|
||||||
|
_isSameDay(selectedEvent.startDateTime, day) &&
|
||||||
|
!dayEvents.any((event) => event.id == selectedEvent.id)) {
|
||||||
|
dayEvents.add(selectedEvent);
|
||||||
|
dayEvents.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dayEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<EventModel> _getDetailsEvents(List<EventModel> events) {
|
||||||
|
final mergedEvents = [...events];
|
||||||
|
|
||||||
|
if (_selectedEvent != null &&
|
||||||
|
!mergedEvents.any((event) => event.id == _selectedEvent!.id)) {
|
||||||
|
mergedEvents.add(_selectedEvent!);
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedEvents.sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
||||||
|
return mergedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatSearchResultDate(DateTime dateTime) {
|
||||||
|
return DateFormat('EEE d MMM yyyy • HH:mm', 'fr_FR').format(dateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getStatusColor(EventStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case EventStatus.confirmed:
|
||||||
|
return Colors.green;
|
||||||
|
case EventStatus.canceled:
|
||||||
|
return Colors.red;
|
||||||
|
case EventStatus.waitingForApproval:
|
||||||
|
default:
|
||||||
|
return Colors.amber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combine uniquement le filtre utilisateur avec la vue calendrier.
|
||||||
|
List<EventModel> _getFilteredEvents(List<EventModel> allEvents) {
|
||||||
|
return _filterEventsByUser(allEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelPendingSearch() {
|
||||||
|
_searchDebounce?.cancel();
|
||||||
|
_searchDebounce = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scheduleSearch(String value) {
|
||||||
|
_cancelPendingSearch();
|
||||||
|
|
||||||
|
_searchDebounce = Timer(const Duration(milliseconds: 300), () {
|
||||||
|
_runSearch(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSearchChanged(String value) {
|
||||||
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||||
|
|
||||||
|
if (isMobile && value.isNotEmpty && !_isMobileSearchVisible) {
|
||||||
|
setState(() {
|
||||||
|
_isMobileSearchVisible = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_searchQuery = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (value.trim().isEmpty) {
|
||||||
|
_cancelPendingSearch();
|
||||||
|
setState(() {
|
||||||
|
_searchResults = [];
|
||||||
|
_searchError = null;
|
||||||
|
_isSearching = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_scheduleSearch(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _clearSearch() {
|
||||||
|
_cancelPendingSearch();
|
||||||
|
|
||||||
|
if (_searchController.text.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_searchController.clear();
|
||||||
|
setState(() {
|
||||||
|
_searchQuery = '';
|
||||||
|
_searchResults = [];
|
||||||
|
_searchError = null;
|
||||||
|
_isSearching = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _runSearch(String value) async {
|
||||||
|
final query = value.trim();
|
||||||
|
if (query.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final localUserProvider = context.read<LocalUserProvider>();
|
||||||
|
final userId = localUserProvider.uid;
|
||||||
|
if (userId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final searchId = ++_searchRequestId;
|
||||||
|
setState(() {
|
||||||
|
_isSearching = true;
|
||||||
|
_searchError = null;
|
||||||
|
_searchResults = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final eventProvider = context.read<EventProvider>();
|
||||||
|
final results = await eventProvider.searchEvents(
|
||||||
|
userId: userId,
|
||||||
|
query: query,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_searchQuery.trim() != query) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchId != _searchRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_searchResults = results;
|
||||||
|
_searchError = null;
|
||||||
|
_isSearching = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
if (!mounted || _searchQuery.trim() != query) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_searchResults = [];
|
||||||
|
_searchError = 'Erreur lors de la recherche : $e';
|
||||||
|
_isSearching = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDesktopFiltersBar({required bool canViewAllUserEvents}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
color: Colors.grey[100],
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
onChanged: _onSearchChanged,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Rechercher (titre, description, lieu)',
|
||||||
|
prefixIcon: const Icon(Icons.search, color: AppColors.rouge),
|
||||||
|
suffixIcon: _searchQuery.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
tooltip: 'Effacer la recherche',
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: _clearSearch,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
isDense: true,
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (canViewAllUserEvents) ...[
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_buildCompactUserFilter(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCompactUserFilter() {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 300),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: UserFilterDropdown(
|
||||||
|
selectedUserId: _selectedUserId,
|
||||||
|
onUserSelected: (userId) {
|
||||||
|
setState(() {
|
||||||
|
_selectedUserId = userId;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMobileSearchBar() {
|
||||||
|
return Container(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_isMobileSearchVisible ? Icons.search_off : Icons.search,
|
||||||
|
color: AppColors.rouge,
|
||||||
|
),
|
||||||
|
tooltip: _isMobileSearchVisible
|
||||||
|
? 'Masquer la recherche'
|
||||||
|
: 'Afficher la recherche',
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isMobileSearchVisible = !_isMobileSearchVisible;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_searchQuery.isEmpty
|
||||||
|
? 'Rechercher un événement'
|
||||||
|
: 'Recherche active',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_searchQuery.isNotEmpty)
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
tooltip: 'Effacer la recherche',
|
||||||
|
onPressed: _clearSearch,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: _isMobileSearchVisible
|
||||||
|
? Padding(
|
||||||
|
key: const ValueKey('mobile-search-visible'),
|
||||||
|
padding: const EdgeInsets.only(top: 4, left: 8, right: 8),
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
onChanged: _onSearchChanged,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Titre, description ou lieu',
|
||||||
|
prefixIcon:
|
||||||
|
const Icon(Icons.search, color: AppColors.rouge),
|
||||||
|
isDense: true,
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.white,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(
|
||||||
|
key: ValueKey('mobile-search-hidden'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSearchResultsPanel({required bool isMobile}) {
|
||||||
|
final hasQuery = _searchQuery.trim().isNotEmpty;
|
||||||
|
|
||||||
|
if (!hasQuery && !_isSearching && _searchError == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final panelPadding = EdgeInsets.symmetric(
|
||||||
|
horizontal: isMobile ? 8 : 16,
|
||||||
|
vertical: 8,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: panelPadding,
|
||||||
|
color: Colors.grey[50],
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.manage_search, color: AppColors.rouge, size: 20),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
hasQuery
|
||||||
|
? 'Résultats pour "$_searchQuery"'
|
||||||
|
: 'Recherche d’événements',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_isSearching)
|
||||||
|
const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_searchError != null) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
_searchError!,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
] else if (!hasQuery) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Saisissez un titre, une description ou un lieu pour lancer la recherche.',
|
||||||
|
style: TextStyle(color: Colors.grey.shade700),
|
||||||
|
),
|
||||||
|
] else if (!_isSearching) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (_searchResults.isEmpty)
|
||||||
|
Text(
|
||||||
|
'Aucun résultat trouvé.',
|
||||||
|
style: TextStyle(color: Colors.grey.shade700),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: isMobile ? 240 : 280,
|
||||||
|
),
|
||||||
|
child: ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: _searchResults.length,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
separatorBuilder: (context, index) =>
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final event = _searchResults[index];
|
||||||
|
final isSelected = _selectedEvent?.id == event.id;
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: isSelected
|
||||||
|
? AppColors.rouge.withOpacity(0.08)
|
||||||
|
: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
onTap: () => _onSearchResultSelected(event),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getStatusColor(event.status),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
event.name,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
_formatSearchResultDate(
|
||||||
|
event.startDateTime),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade700,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (event.address.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
event.address,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Icon(Icons.chevron_right,
|
||||||
|
color: Colors.grey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onSearchResultSelected(EventModel event) async {
|
||||||
|
final localUserProvider = context.read<LocalUserProvider>();
|
||||||
|
final eventProvider = context.read<EventProvider>();
|
||||||
|
final userId = localUserProvider.uid;
|
||||||
|
|
||||||
|
if (userId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final canViewAllEvents = localUserProvider.hasPermission('view_all_events');
|
||||||
|
final selectedDay = DateTime(
|
||||||
|
event.startDateTime.year,
|
||||||
|
event.startDateTime.month,
|
||||||
|
event.startDateTime.day,
|
||||||
|
);
|
||||||
|
final shouldLoadMonth = _focusedDay.year != event.startDateTime.year ||
|
||||||
|
_focusedDay.month != event.startDateTime.month ||
|
||||||
|
eventProvider.events.isEmpty;
|
||||||
|
|
||||||
|
if (shouldLoadMonth) {
|
||||||
|
await eventProvider.loadMonthEvents(
|
||||||
|
userId,
|
||||||
|
event.startDateTime.year,
|
||||||
|
event.startDateTime.month,
|
||||||
|
canViewAllEvents: canViewAllEvents,
|
||||||
|
);
|
||||||
|
|
||||||
|
eventProvider.preloadAdjacentMonths(
|
||||||
|
userId,
|
||||||
|
event.startDateTime.year,
|
||||||
|
event.startDateTime.month,
|
||||||
|
canViewAllEvents: canViewAllEvents,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final eventsForSelectedDay = _getEventsForDay(
|
||||||
|
eventProvider.events,
|
||||||
|
selectedDay,
|
||||||
|
selectedEvent: event,
|
||||||
|
);
|
||||||
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_focusedDay = selectedDay;
|
||||||
|
_selectedDay = selectedDay;
|
||||||
|
_selectedEvent = event;
|
||||||
|
_selectedEventIndex =
|
||||||
|
eventsForSelectedDay.indexWhere((e) => e.id == event.id);
|
||||||
|
if (_selectedEventIndex < 0) {
|
||||||
|
_selectedEventIndex = 0;
|
||||||
|
}
|
||||||
|
_calendarCollapsed = false;
|
||||||
|
if (isMobile) {
|
||||||
|
_isMobileSearchVisible = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _changeWeek(int delta) {
|
void _changeWeek(int delta) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_focusedDay = _focusedDay.add(Duration(days: 7 * delta));
|
_focusedDay = _focusedDay.add(Duration(days: 7 * delta));
|
||||||
@@ -238,10 +757,12 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
|
|
||||||
Widget _buildDesktopDetailsPane(List<EventModel> filteredEvents) {
|
Widget _buildDesktopDetailsPane(List<EventModel> filteredEvents) {
|
||||||
if (_selectedEvent != null) {
|
if (_selectedEvent != null) {
|
||||||
|
final detailsEvents = _getDetailsEvents(filteredEvents);
|
||||||
|
|
||||||
return EventDetails(
|
return EventDetails(
|
||||||
event: _selectedEvent!,
|
event: _selectedEvent!,
|
||||||
selectedDate: _selectedDay,
|
selectedDate: _selectedDay,
|
||||||
events: filteredEvents,
|
events: detailsEvents,
|
||||||
onSelectEvent: (event, date) {
|
onSelectEvent: (event, date) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedEvent = event;
|
_selectedEvent = event;
|
||||||
@@ -296,6 +817,8 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
final canViewAllUserEvents =
|
final canViewAllUserEvents =
|
||||||
localUserProvider.hasPermission('view_all_user_events');
|
localUserProvider.hasPermission('view_all_user_events');
|
||||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
final isMobile = MediaQuery.of(context).size.width < 600;
|
||||||
|
final showSearchResults =
|
||||||
|
_searchQuery.trim().isNotEmpty || _isSearching || _searchError != null;
|
||||||
|
|
||||||
// Appliquer le filtre utilisateur si actif
|
// Appliquer le filtre utilisateur si actif
|
||||||
final filteredEvents = _getFilteredEvents(eventProvider.events);
|
final filteredEvents = _getFilteredEvents(eventProvider.events);
|
||||||
@@ -343,33 +866,11 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
drawer: const MainDrawer(currentPage: '/calendar'),
|
drawer: const MainDrawer(currentPage: '/calendar'),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Filtre utilisateur dans le corps de la page
|
if (isMobile)
|
||||||
if (canViewAllUserEvents && !isMobile)
|
_buildMobileSearchBar()
|
||||||
Container(
|
else
|
||||||
padding: const EdgeInsets.all(16),
|
_buildDesktopFiltersBar(canViewAllUserEvents: canViewAllUserEvents),
|
||||||
color: Colors.grey[100],
|
if (showSearchResults) _buildSearchResultsPanel(isMobile: isMobile),
|
||||||
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
|
// Corps du calendrier
|
||||||
Expanded(
|
Expanded(
|
||||||
child: isMobile
|
child: isMobile
|
||||||
@@ -426,18 +927,19 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMobileLayout(List<EventModel> filteredEvents) {
|
Widget _buildMobileLayout(List<EventModel> filteredEvents) {
|
||||||
final eventsForSelectedDay = _selectedDay == null
|
final eventsForSelectedDay = _getEventsForDay(
|
||||||
? []
|
filteredEvents,
|
||||||
: filteredEvents
|
_selectedDay,
|
||||||
.where((e) =>
|
selectedEvent: _selectedEvent,
|
||||||
e.startDateTime.year == _selectedDay!.year &&
|
);
|
||||||
e.startDateTime.month == _selectedDay!.month &&
|
|
||||||
e.startDateTime.day == _selectedDay!.day)
|
|
||||||
.toList()
|
|
||||||
..sort((a, b) => a.startDateTime.compareTo(b.startDateTime));
|
|
||||||
final hasEvents = eventsForSelectedDay.isNotEmpty;
|
final hasEvents = eventsForSelectedDay.isNotEmpty;
|
||||||
final currentEvent =
|
final selectedEventIndex = _selectedEvent == null
|
||||||
hasEvents && _selectedEventIndex < eventsForSelectedDay.length
|
? -1
|
||||||
|
: eventsForSelectedDay
|
||||||
|
.indexWhere((event) => event.id == _selectedEvent!.id);
|
||||||
|
final currentEvent = hasEvents && selectedEventIndex >= 0
|
||||||
|
? eventsForSelectedDay[selectedEventIndex]
|
||||||
|
: hasEvents && _selectedEventIndex < eventsForSelectedDay.length
|
||||||
? eventsForSelectedDay[_selectedEventIndex]
|
? eventsForSelectedDay[_selectedEventIndex]
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -581,7 +1083,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
child: EventDetails(
|
child: EventDetails(
|
||||||
event: eventsForSelectedDay[_selectedEventIndex],
|
event: eventsForSelectedDay[_selectedEventIndex],
|
||||||
selectedDate: _selectedDay,
|
selectedDate: _selectedDay,
|
||||||
events: eventsForSelectedDay.cast<EventModel>(),
|
events: eventsForSelectedDay,
|
||||||
onSelectEvent: (event, date) {
|
onSelectEvent: (event, date) {
|
||||||
final idx = eventsForSelectedDay
|
final idx = eventsForSelectedDay
|
||||||
.indexWhere((e) => e.id == event.id);
|
.indexWhere((e) => e.id == event.id);
|
||||||
@@ -600,7 +1102,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Vue détail (prend tout l'espace quand calendrier caché)
|
// Vue détail (prend tout l'espace quand calendrier cache)
|
||||||
if (_calendarCollapsed && _selectedDay != null)
|
if (_calendarCollapsed && _selectedDay != null)
|
||||||
AnimatedPositioned(
|
AnimatedPositioned(
|
||||||
duration: const Duration(milliseconds: 400),
|
duration: const Duration(milliseconds: 400),
|
||||||
@@ -647,7 +1149,7 @@ class _CalendarPageState extends State<CalendarPage> {
|
|||||||
child: EventDetails(
|
child: EventDetails(
|
||||||
event: currentEvent,
|
event: currentEvent,
|
||||||
selectedDate: _selectedDay,
|
selectedDate: _selectedDay,
|
||||||
events: eventsForSelectedDay.cast<EventModel>(),
|
events: eventsForSelectedDay,
|
||||||
onSelectEvent: (event, date) {
|
onSelectEvent: (event, date) {
|
||||||
final idx = eventsForSelectedDay
|
final idx = eventsForSelectedDay
|
||||||
.indexWhere((e) => e.id == event.id);
|
.indexWhere((e) => e.id == event.id);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:em2rp/providers/local_user_provider.dart';
|
|||||||
import 'package:em2rp/services/equipment_service.dart';
|
import 'package:em2rp/services/equipment_service.dart';
|
||||||
import 'package:em2rp/services/qr_code_service.dart';
|
import 'package:em2rp/services/qr_code_service.dart';
|
||||||
import 'package:em2rp/utils/colors.dart';
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
import 'package:em2rp/utils/equipment_delete_utils.dart';
|
||||||
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
import 'package:em2rp/views/widgets/nav/custom_app_bar.dart';
|
||||||
import 'package:em2rp/views/equipment_form_page.dart';
|
import 'package:em2rp/views/equipment_form_page.dart';
|
||||||
import 'package:em2rp/views/widgets/equipment/equipment_referencing_containers.dart';
|
import 'package:em2rp/views/widgets/equipment/equipment_referencing_containers.dart';
|
||||||
@@ -45,7 +46,8 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
|
|||||||
|
|
||||||
Future<void> _loadMaintenances() async {
|
Future<void> _loadMaintenances() async {
|
||||||
try {
|
try {
|
||||||
final maintenances = await _equipmentService.getMaintenancesForEquipment(widget.equipment.id);
|
final maintenances = await _equipmentService
|
||||||
|
.getMaintenancesForEquipment(widget.equipment.id);
|
||||||
setState(() {
|
setState(() {
|
||||||
_maintenances = maintenances;
|
_maintenances = maintenances;
|
||||||
_isLoadingMaintenances = false;
|
_isLoadingMaintenances = false;
|
||||||
@@ -57,8 +59,6 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final screenWidth = MediaQuery.of(context).size.width;
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
@@ -103,7 +103,8 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// 3. Notes
|
// 3. Notes
|
||||||
if (widget.equipment.notes != null && widget.equipment.notes!.isNotEmpty) ...[
|
if (widget.equipment.notes != null &&
|
||||||
|
widget.equipment.notes!.isNotEmpty) ...[
|
||||||
EquipmentNotesSection(notes: widget.equipment.notes!),
|
EquipmentNotesSection(notes: widget.equipment.notes!),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
],
|
],
|
||||||
@@ -185,7 +186,6 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void _showQRCode() {
|
void _showQRCode() {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -249,10 +249,12 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'${widget.equipment.brand ?? ''} ${widget.equipment.model ?? ''}'.trim(),
|
'${widget.equipment.brand ?? ''} ${widget.equipment.model ?? ''}'
|
||||||
|
.trim(),
|
||||||
style: TextStyle(color: Colors.grey[700]),
|
style: TextStyle(color: Colors.grey[700]),
|
||||||
),
|
),
|
||||||
if (widget.equipment.subCategory != null && widget.equipment.subCategory!.isNotEmpty) ...[
|
if (widget.equipment.subCategory != null &&
|
||||||
|
widget.equipment.subCategory!.isNotEmpty) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'📁 ${widget.equipment.subCategory}',
|
'📁 ${widget.equipment.subCategory}',
|
||||||
@@ -389,7 +391,8 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
|
|||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('Vous n\'avez pas la permission de gérer les maintenances'),
|
content:
|
||||||
|
Text('Vous n\'avez pas la permission de gérer les maintenances'),
|
||||||
backgroundColor: Colors.orange,
|
backgroundColor: Colors.orange,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -423,31 +426,50 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _deleteEquipment() {
|
void _deleteEquipment() {
|
||||||
|
final pageContext = context;
|
||||||
|
final equipmentLabel = EquipmentDeleteUtils.resolveEquipmentLabel(
|
||||||
|
id: widget.equipment.id,
|
||||||
|
name: widget.equipment.name,
|
||||||
|
);
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: pageContext,
|
||||||
builder: (context) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
title: const Text('Confirmer la suppression'),
|
title: const Text(EquipmentDeleteUtils.deleteDialogTitle),
|
||||||
content: Text(
|
content: Text(
|
||||||
'Voulez-vous vraiment supprimer "${widget.equipment.id}" ?\n\nCette action est irréversible.',
|
EquipmentDeleteUtils.buildSingleDeleteConfirmationMessage(
|
||||||
|
equipmentLabel,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(dialogContext),
|
||||||
child: const Text('Annuler'),
|
child: const Text(EquipmentDeleteUtils.deleteDialogCancelLabel),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
// Fermer le dialog
|
// Fermer le dialog
|
||||||
Navigator.pop(context);
|
Navigator.pop(dialogContext);
|
||||||
|
|
||||||
// Capturer le ScaffoldMessenger avant la suppression
|
// Capturer le ScaffoldMessenger avant la suppression
|
||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(pageContext);
|
||||||
final navigator = Navigator.of(context);
|
final navigator = Navigator.of(pageContext);
|
||||||
|
final provider = pageContext.read<EquipmentProvider>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await context
|
final deleted =
|
||||||
.read<EquipmentProvider>()
|
await EquipmentDeleteUtils.deleteWithFutureAssignmentCheck(
|
||||||
.deleteEquipment(widget.equipment.id);
|
context: pageContext,
|
||||||
|
equipmentLabel: equipmentLabel,
|
||||||
|
deleteEquipment: ({bool forceDelete = false}) {
|
||||||
|
return provider.deleteEquipment(
|
||||||
|
widget.equipment.id,
|
||||||
|
forceDelete: forceDelete,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!deleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Revenir à la page précédente
|
// Revenir à la page précédente
|
||||||
navigator.pop();
|
navigator.pop();
|
||||||
@@ -455,22 +477,26 @@ class _EquipmentDetailPageState extends State<EquipmentDetailPage> {
|
|||||||
// Afficher le snackbar (même si le widget est démonté)
|
// Afficher le snackbar (même si le widget est démonté)
|
||||||
scaffoldMessenger.showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('Équipement supprimé avec succès'),
|
content: Text(EquipmentDeleteUtils.deleteSuccessMessage),
|
||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Afficher l'erreur
|
// Afficher l'erreur
|
||||||
scaffoldMessenger.showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(content: Text('Erreur: $e')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
EquipmentDeleteUtils.buildDeleteErrorMessage(e),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
child: const Text('Supprimer'),
|
child: const Text(EquipmentDeleteUtils.deleteDialogConfirmLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import 'package:em2rp/views/widgets/common/qr_code_scanner_dialog.dart';
|
|||||||
import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart';
|
import 'package:em2rp/views/widgets/common/qr_code_format_selector_dialog.dart';
|
||||||
import 'package:em2rp/views/widgets/equipment/equipment_status_badge.dart';
|
import 'package:em2rp/views/widgets/equipment/equipment_status_badge.dart';
|
||||||
import 'package:em2rp/utils/debug_log.dart';
|
import 'package:em2rp/utils/debug_log.dart';
|
||||||
|
import 'package:em2rp/utils/equipment_delete_utils.dart';
|
||||||
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
import 'package:em2rp/mixins/selection_mode_mixin.dart';
|
||||||
import 'package:em2rp/views/widgets/common/search_actions_bar.dart';
|
import 'package:em2rp/views/widgets/common/search_actions_bar.dart';
|
||||||
import 'package:em2rp/views/widgets/notification_badge.dart';
|
import 'package:em2rp/views/widgets/notification_badge.dart';
|
||||||
@@ -28,7 +29,6 @@ class EquipmentManagementPage extends StatefulWidget {
|
|||||||
_EquipmentManagementPageState();
|
_EquipmentManagementPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
||||||
with SelectionModeMixin<EquipmentManagementPage> {
|
with SelectionModeMixin<EquipmentManagementPage> {
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
@@ -66,7 +66,6 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
if (_scrollController.hasClients &&
|
if (_scrollController.hasClients &&
|
||||||
_scrollController.position.pixels >=
|
_scrollController.position.pixels >=
|
||||||
_scrollController.position.maxScrollExtent - 300) {
|
_scrollController.position.maxScrollExtent - 300) {
|
||||||
|
|
||||||
// Vérifier qu'on peut charger plus
|
// Vérifier qu'on peut charger plus
|
||||||
if (provider.hasMore && !provider.isLoadingMore) {
|
if (provider.hasMore && !provider.isLoadingMore) {
|
||||||
// ✅ Pas de setState ici pour éviter les rebuilds pendant le scroll
|
// ✅ Pas de setState ici pour éviter les rebuilds pendant le scroll
|
||||||
@@ -76,7 +75,8 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
_isLoadingMore = false;
|
_isLoadingMore = false;
|
||||||
}).catchError((error) {
|
}).catchError((error) {
|
||||||
_isLoadingMore = false;
|
_isLoadingMore = false;
|
||||||
DebugLog.error('[EquipmentManagementPage] Error loading next page', error);
|
DebugLog.error(
|
||||||
|
'[EquipmentManagementPage] Error loading next page', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -456,11 +456,13 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
Widget _buildEquipmentList() {
|
Widget _buildEquipmentList() {
|
||||||
return Consumer<EquipmentProvider>(
|
return Consumer<EquipmentProvider>(
|
||||||
builder: (context, provider, child) {
|
builder: (context, provider, child) {
|
||||||
DebugLog.info('[EquipmentManagementPage] Building list - isLoading: ${provider.isLoading}, equipment count: ${provider.equipment.length}');
|
DebugLog.info(
|
||||||
|
'[EquipmentManagementPage] Building list - isLoading: ${provider.isLoading}, equipment count: ${provider.equipment.length}');
|
||||||
|
|
||||||
// Afficher l'indicateur de chargement initial uniquement
|
// Afficher l'indicateur de chargement initial uniquement
|
||||||
if (provider.isLoading && provider.equipment.isEmpty) {
|
if (provider.isLoading && provider.equipment.isEmpty) {
|
||||||
DebugLog.info('[EquipmentManagementPage] Showing initial loading indicator');
|
DebugLog.info(
|
||||||
|
'[EquipmentManagementPage] Showing initial loading indicator');
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,7 +492,8 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
DebugLog.info('[EquipmentManagementPage] Building list with ${equipments.length} items');
|
DebugLog.info(
|
||||||
|
'[EquipmentManagementPage] Building list with ${equipments.length} items');
|
||||||
|
|
||||||
// Calculer le nombre total d'items (équipements + indicateur de chargement)
|
// Calculer le nombre total d'items (équipements + indicateur de chargement)
|
||||||
final itemCount = equipments.length + (provider.hasMore ? 1 : 0);
|
final itemCount = equipments.length + (provider.hasMore ? 1 : 0);
|
||||||
@@ -526,124 +529,127 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
|
|
||||||
// ✅ RepaintBoundary pour isoler le repaint de chaque carte
|
// ✅ RepaintBoundary pour isoler le repaint de chaque carte
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
key: ValueKey(equipment.id),
|
key: ValueKey(equipment.id),
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
color: isSelectionMode && isSelected
|
color: isSelectionMode && isSelected
|
||||||
? AppColors.rouge.withValues(alpha: 0.1)
|
? AppColors.rouge.withValues(alpha: 0.1)
|
||||||
: null,
|
: null,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: isSelectionMode
|
leading: isSelectionMode
|
||||||
? Checkbox(
|
? Checkbox(
|
||||||
value: isSelected,
|
value: isSelected,
|
||||||
onChanged: (value) => toggleItemSelection(equipment.id),
|
onChanged: (value) => toggleItemSelection(equipment.id),
|
||||||
activeColor: AppColors.rouge,
|
activeColor: AppColors.rouge,
|
||||||
)
|
)
|
||||||
: CircleAvatar(
|
: CircleAvatar(
|
||||||
backgroundColor: equipment.category.color.withValues(alpha: 0.2),
|
backgroundColor:
|
||||||
child: equipment.category.getIcon(
|
equipment.category.color.withValues(alpha: 0.2),
|
||||||
size: 20,
|
child: equipment.category.getIcon(
|
||||||
color: equipment.category.color,
|
size: 20,
|
||||||
),
|
color: equipment.category.color,
|
||||||
),
|
),
|
||||||
title: Row(
|
),
|
||||||
children: [
|
title: Row(
|
||||||
Expanded(
|
children: [
|
||||||
child: Text(
|
Expanded(
|
||||||
equipment.id,
|
child: Text(
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
equipment.id,
|
||||||
),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
|
||||||
// Afficher le badge de statut calculé dynamiquement
|
|
||||||
if (equipment.category != EquipmentCategory.consumable &&
|
|
||||||
equipment.category != EquipmentCategory.cable)
|
|
||||||
EquipmentStatusBadge(equipment: equipment),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'${equipment.brand ?? ''} ${equipment.model ?? ''}'
|
|
||||||
.trim()
|
|
||||||
.isNotEmpty
|
|
||||||
? '${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim()
|
|
||||||
: 'Marque/Modèle non défini',
|
|
||||||
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
|
||||||
),
|
|
||||||
// Afficher la sous-catégorie si elle existe
|
|
||||||
if (equipment.subCategory != null && equipment.subCategory!.isNotEmpty) ...[
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
'📁 ${equipment.subCategory}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[500],
|
|
||||||
fontSize: 12,
|
|
||||||
fontStyle: FontStyle.italic,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Afficher le badge de statut calculé dynamiquement
|
||||||
|
if (equipment.category != EquipmentCategory.consumable &&
|
||||||
|
equipment.category != EquipmentCategory.cable)
|
||||||
|
EquipmentStatusBadge(equipment: equipment),
|
||||||
],
|
],
|
||||||
// Afficher la quantité disponible pour les consommables/câbles
|
),
|
||||||
if (equipment.category == EquipmentCategory.consumable ||
|
subtitle: Column(
|
||||||
equipment.category == EquipmentCategory.cable) ...[
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
_buildQuantityDisplay(equipment),
|
Text(
|
||||||
],
|
'${equipment.brand ?? ''} ${equipment.model ?? ''}'
|
||||||
],
|
.trim()
|
||||||
),
|
.isNotEmpty
|
||||||
trailing: isSelectionMode
|
? '${equipment.brand ?? ''} ${equipment.model ?? ''}'
|
||||||
? null
|
.trim()
|
||||||
: Row(
|
: 'Marque/Modèle non défini',
|
||||||
mainAxisSize: MainAxisSize.min,
|
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
||||||
children: [
|
),
|
||||||
// Bouton Restock (uniquement pour consommables/câbles avec permission)
|
// Afficher la sous-catégorie si elle existe
|
||||||
if (equipment.category == EquipmentCategory.consumable ||
|
if (equipment.subCategory != null &&
|
||||||
equipment.category == EquipmentCategory.cable)
|
equipment.subCategory!.isNotEmpty) ...[
|
||||||
PermissionGate(
|
const SizedBox(height: 2),
|
||||||
requiredPermissions: const ['manage_equipment'],
|
Text(
|
||||||
child: IconButton(
|
'📁 ${equipment.subCategory}',
|
||||||
icon: const Icon(Icons.add_shopping_cart,
|
style: TextStyle(
|
||||||
color: AppColors.rouge),
|
color: Colors.grey[500],
|
||||||
tooltip: 'Restock',
|
fontSize: 12,
|
||||||
onPressed: () => _showRestockDialog(equipment),
|
fontStyle: FontStyle.italic,
|
||||||
),
|
|
||||||
),
|
|
||||||
// Bouton QR Code
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.qr_code, color: AppColors.rouge),
|
|
||||||
tooltip: 'QR Code',
|
|
||||||
onPressed: () => showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => QRCodeDialog.forEquipment(equipment),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Bouton Modifier (permission required)
|
|
||||||
PermissionGate(
|
|
||||||
requiredPermissions: const ['manage_equipment'],
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.edit, color: AppColors.rouge),
|
|
||||||
tooltip: 'Modifier',
|
|
||||||
onPressed: () => _editEquipment(equipment),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Bouton Supprimer (permission required)
|
|
||||||
PermissionGate(
|
|
||||||
requiredPermissions: const ['manage_equipment'],
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.delete, color: Colors.red),
|
|
||||||
tooltip: 'Supprimer',
|
|
||||||
onPressed: () => _deleteEquipment(equipment),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
// Afficher la quantité disponible pour les consommables/câbles
|
||||||
onTap: isSelectionMode
|
if (equipment.category == EquipmentCategory.consumable ||
|
||||||
? () => toggleItemSelection(equipment.id)
|
equipment.category == EquipmentCategory.cable) ...[
|
||||||
: () => _viewEquipmentDetails(equipment),
|
const SizedBox(height: 4),
|
||||||
),
|
_buildQuantityDisplay(equipment),
|
||||||
)
|
],
|
||||||
);
|
],
|
||||||
|
),
|
||||||
|
trailing: isSelectionMode
|
||||||
|
? null
|
||||||
|
: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Bouton Restock (uniquement pour consommables/câbles avec permission)
|
||||||
|
if (equipment.category == EquipmentCategory.consumable ||
|
||||||
|
equipment.category == EquipmentCategory.cable)
|
||||||
|
PermissionGate(
|
||||||
|
requiredPermissions: const ['manage_equipment'],
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.add_shopping_cart,
|
||||||
|
color: AppColors.rouge),
|
||||||
|
tooltip: 'Restock',
|
||||||
|
onPressed: () => _showRestockDialog(equipment),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Bouton QR Code
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code, color: AppColors.rouge),
|
||||||
|
tooltip: 'QR Code',
|
||||||
|
onPressed: () => showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
QRCodeDialog.forEquipment(equipment),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Bouton Modifier (permission required)
|
||||||
|
PermissionGate(
|
||||||
|
requiredPermissions: const ['manage_equipment'],
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.edit, color: AppColors.rouge),
|
||||||
|
tooltip: 'Modifier',
|
||||||
|
onPressed: () => _editEquipment(equipment),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Bouton Supprimer (permission required)
|
||||||
|
PermissionGate(
|
||||||
|
requiredPermissions: const ['manage_equipment'],
|
||||||
|
child: IconButton(
|
||||||
|
icon: const Icon(Icons.delete, color: Colors.red),
|
||||||
|
tooltip: 'Supprimer',
|
||||||
|
onPressed: () => _deleteEquipment(equipment),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: isSelectionMode
|
||||||
|
? () => toggleItemSelection(equipment.id)
|
||||||
|
: () => _viewEquipmentDetails(equipment),
|
||||||
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildQuantityDisplay(EquipmentModel equipment) {
|
Widget _buildQuantityDisplay(EquipmentModel equipment) {
|
||||||
@@ -705,7 +711,6 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
void _createNewEquipment() {
|
void _createNewEquipment() {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
@@ -726,39 +731,64 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _deleteEquipment(EquipmentModel equipment) {
|
void _deleteEquipment(EquipmentModel equipment) {
|
||||||
|
final pageContext = context;
|
||||||
|
final equipmentLabel = EquipmentDeleteUtils.resolveEquipmentLabel(
|
||||||
|
id: equipment.id,
|
||||||
|
name: equipment.name,
|
||||||
|
);
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: pageContext,
|
||||||
builder: (context) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
title: const Text('Confirmer la suppression'),
|
title: const Text(EquipmentDeleteUtils.deleteDialogTitle),
|
||||||
content: Text('Voulez-vous vraiment supprimer "${equipment.name}" ?'),
|
content: Text(
|
||||||
|
EquipmentDeleteUtils.buildSingleDeleteConfirmationMessage(
|
||||||
|
equipmentLabel,
|
||||||
|
),
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(dialogContext),
|
||||||
child: const Text('Annuler'),
|
child: const Text(EquipmentDeleteUtils.deleteDialogCancelLabel),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.pop(context);
|
Navigator.pop(dialogContext);
|
||||||
|
final scaffoldMessenger = ScaffoldMessenger.of(pageContext);
|
||||||
|
final provider = pageContext.read<EquipmentProvider>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await context
|
final deleted =
|
||||||
.read<EquipmentProvider>()
|
await EquipmentDeleteUtils.deleteWithFutureAssignmentCheck(
|
||||||
.deleteEquipment(equipment.id);
|
context: pageContext,
|
||||||
if (mounted) {
|
equipmentLabel: equipmentLabel,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
deleteEquipment: ({bool forceDelete = false}) {
|
||||||
const SnackBar(
|
return provider.deleteEquipment(
|
||||||
content: Text('Équipement supprimé avec succès')),
|
equipment.id,
|
||||||
);
|
forceDelete: forceDelete,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!deleted) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
scaffoldMessenger.showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(EquipmentDeleteUtils.deleteSuccessMessage),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
scaffoldMessenger.showSnackBar(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
SnackBar(
|
||||||
SnackBar(content: Text('Erreur: $e')),
|
content: Text(
|
||||||
);
|
EquipmentDeleteUtils.buildDeleteErrorMessage(e),
|
||||||
}
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
child: const Text('Supprimer'),
|
child: const Text(EquipmentDeleteUtils.deleteDialogConfirmLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -768,46 +798,78 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
void _deleteSelectedEquipment() async {
|
void _deleteSelectedEquipment() async {
|
||||||
if (!hasSelection) return;
|
if (!hasSelection) return;
|
||||||
|
|
||||||
|
final pageContext = context;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: pageContext,
|
||||||
builder: (context) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
title: const Text('Confirmer la suppression'),
|
title: const Text(EquipmentDeleteUtils.deleteDialogTitle),
|
||||||
content: Text(
|
content: Text(
|
||||||
'Voulez-vous vraiment supprimer $selectedCount équipement(s) ?',
|
EquipmentDeleteUtils.buildBulkDeleteConfirmationMessage(
|
||||||
|
selectedCount,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(context),
|
onPressed: () => Navigator.pop(dialogContext),
|
||||||
child: const Text('Annuler'),
|
child: const Text(EquipmentDeleteUtils.deleteDialogCancelLabel),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.pop(context);
|
Navigator.pop(dialogContext);
|
||||||
|
final scaffoldMessenger = ScaffoldMessenger.of(pageContext);
|
||||||
|
final provider = pageContext.read<EquipmentProvider>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final provider = context.read<EquipmentProvider>();
|
final equipmentById = {
|
||||||
|
for (final equipment
|
||||||
|
in provider.equipment)
|
||||||
|
equipment.id: equipment,
|
||||||
|
};
|
||||||
|
|
||||||
|
var deletedCount = 0;
|
||||||
for (final id in selectedIds) {
|
for (final id in selectedIds) {
|
||||||
await provider.deleteEquipment(id);
|
final label = EquipmentDeleteUtils.resolveEquipmentLabel(
|
||||||
|
id: id,
|
||||||
|
name: equipmentById[id]?.name,
|
||||||
|
);
|
||||||
|
final deleted = await EquipmentDeleteUtils
|
||||||
|
.deleteWithFutureAssignmentCheck(
|
||||||
|
context: pageContext,
|
||||||
|
equipmentLabel: label,
|
||||||
|
deleteEquipment: ({bool forceDelete = false}) {
|
||||||
|
return provider.deleteEquipment(
|
||||||
|
id,
|
||||||
|
forceDelete: forceDelete,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (deleted) {
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
disableSelectionMode();
|
disableSelectionMode();
|
||||||
if (mounted) {
|
scaffoldMessenger.showSnackBar(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
SnackBar(
|
||||||
SnackBar(
|
content: Text(
|
||||||
content: Text(
|
EquipmentDeleteUtils.buildBulkDeleteSuccessMessage(
|
||||||
'$selectedCount équipement(s) supprimé(s) avec succès'),
|
deletedCount,
|
||||||
backgroundColor: Colors.green,
|
),
|
||||||
),
|
),
|
||||||
);
|
backgroundColor: Colors.green,
|
||||||
}
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (mounted) {
|
scaffoldMessenger.showSnackBar(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
SnackBar(
|
||||||
SnackBar(content: Text('Erreur: $e')),
|
content: Text(
|
||||||
);
|
EquipmentDeleteUtils.buildDeleteErrorMessage(e),
|
||||||
}
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
child: const Text('Supprimer'),
|
child: const Text(EquipmentDeleteUtils.deleteDialogConfirmLabel),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -853,7 +915,8 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => QRCodeDialog.forEquipment(selectedEquipment.first),
|
builder: (context) =>
|
||||||
|
QRCodeDialog.forEquipment(selectedEquipment.first),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1046,7 +1109,9 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
|
|
||||||
await context.read<EquipmentProvider>().updateEquipment(updatedEquipment);
|
await context
|
||||||
|
.read<EquipmentProvider>()
|
||||||
|
.updateEquipment(updatedEquipment);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -1184,7 +1249,8 @@ class _EquipmentManagementPageState extends State<EquipmentManagementPage>
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('Aucun équipement ou conteneur trouvé avec l\'ID : $scannedCode'),
|
content: Text(
|
||||||
|
'Aucun équipement ou conteneur trouvé avec l\'ID : $scannedCode'),
|
||||||
backgroundColor: Colors.orange,
|
backgroundColor: Colors.orange,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.1.20",
|
"version": "1.1.21",
|
||||||
"updateUrl": "https://app.em2events.fr",
|
"updateUrl": "https://app.em2events.fr",
|
||||||
"forceUpdate": true,
|
"forceUpdate": true,
|
||||||
"releaseNotes": "Patch bug envoi d'alerte equipement perdu, date dans les alertes, recherche par ID d'équipement.",
|
"releaseNotes": "Ajout de la recherche d'événements et gestion avancée de la suppression d'équipement",
|
||||||
"timestamp": "2026-03-30T15:04:34.073Z"
|
"timestamp": "2026-04-22T10:22:26.036Z"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user