feat: Intégration d'un assistant IA logisticien basé sur Gemini

- Ajout d'une Cloud Function `aiEquipmentProposal` utilisant le modèle Gemini avec function calling pour suggérer du matériel et des containers.
- Implémentation de plusieurs outils (tools) côté serveur pour permettre à l'IA d'interagir avec Firestore : `search_equipment`, `check_availability_batch`, `get_past_events`, `search_event_reference` et `search_containers`.
- Ajout de la dépendance `@google/generative-ai` dans le backend.
- Création d'un service Flutter `AiEquipmentAssistantService` pour communiquer avec la nouvelle Cloud Function.
- Ajout d'une interface de dialogue `AiEquipmentAssistantDialog` permettant aux utilisateurs de discuter avec l'IA pour affiner les propositions de matériel.
- Intégration de l'assistant IA dans la section de gestion du matériel des événements (`EventAssignedEquipmentSection`).
- Mise à jour de `DataService` avec de nouvelles méthodes de recherche et de vérification de disponibilité optimisées pour l'assistant.
- Activation du mode développement et configuration des identifiants de test dans `env.dart`.
- Optimisation des paramètres de la Cloud Function (timeout de 300s et 1GiB de RAM) pour supporter les traitements IA.
This commit is contained in:
ElPoyo
2026-03-24 12:00:30 +01:00
parent ecf4a5cede
commit 84c882ac0b
12 changed files with 2193 additions and 107 deletions

View File

@@ -27,7 +27,8 @@ class DataService {
if (eventTypes == null) return [];
return eventTypes.map((e) => e as Map<String, dynamic>).toList();
} 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 {
final data = <String, dynamic>{'eventId': eventId};
if (assignedEquipment != null) data['assignedEquipment'] = assignedEquipment;
if (preparationStatus != null) data['preparationStatus'] = preparationStatus;
if (assignedEquipment != null)
data['assignedEquipment'] = assignedEquipment;
if (preparationStatus != null)
data['preparationStatus'] = preparationStatus;
if (loadingStatus != null) data['loadingStatus'] = loadingStatus;
if (unloadingStatus != null) data['unloadingStatus'] = unloadingStatus;
if (returnStatus != null) data['returnStatus'] = returnStatus;
await _apiService.call('updateEventEquipment', data);
} 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};
if (status != null) data['status'] = status;
if (availableQuantity != null) data['availableQuantity'] = availableQuantity;
if (availableQuantity != null)
data['availableQuantity'] = availableQuantity;
await _apiService.call('updateEquipmentStatusOnly', data);
} 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
Future<void> createEquipment(String equipmentId, Map<String, dynamic> data) async {
Future<void> createEquipment(
String equipmentId, Map<String, dynamic> data) async {
try {
// S'assurer que l'ID est dans les données
final equipmentData = Map<String, dynamic>.from(data);
@@ -119,7 +126,8 @@ class DataService {
}
/// 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 {
await _apiService.call('updateEquipment', {
'equipmentId': equipmentId,
@@ -140,9 +148,11 @@ class DataService {
}
/// 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 {
final result = await _apiService.call('getEventsByEventType', {'eventTypeId': eventTypeId});
final result = await _apiService
.call('getEventsByEventType', {'eventTypeId': eventTypeId});
final events = result['events'] as List<dynamic>?;
if (events == null) return [];
return events.map((e) => e as Map<String, dynamic>).toList();
@@ -271,7 +281,8 @@ class DataService {
final events = result['events'] as List<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 {
'events': events.map((e) => e as Map<String, dynamic>).toList(),
@@ -279,7 +290,8 @@ class DataService {
};
} catch (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');
}
}
@@ -299,7 +311,8 @@ class DataService {
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 {
'event': event,
@@ -308,7 +321,8 @@ class DataService {
};
} catch (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 +346,13 @@ class DataService {
}
/// 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 {
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', {
'equipmentIds': equipmentIds,
});
@@ -366,11 +382,13 @@ class DataService {
}
/// 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 {
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', {
'containerIds': containerIds,
});
@@ -415,22 +433,25 @@ class DataService {
params['searchQuery'] = searchQuery;
}
final result = await (_apiService as FirebaseFunctionsApiService).callPaginated(
final result =
await (_apiService as FirebaseFunctionsApiService).callPaginated(
'getEquipmentsPaginated',
params,
);
return {
'equipments': (result['equipments'] as List<dynamic>?)
?.map((e) => e as Map<String, dynamic>)
.toList() ?? [],
?.map((e) => e as Map<String, dynamic>)
.toList() ??
[],
'hasMore': result['hasMore'] as bool? ?? false,
'lastVisible': result['lastVisible'] as String?,
'total': result['total'] as int? ?? 0,
};
} catch (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 +481,25 @@ class DataService {
params['searchQuery'] = searchQuery;
}
final result = await (_apiService as FirebaseFunctionsApiService).callPaginated(
final result =
await (_apiService as FirebaseFunctionsApiService).callPaginated(
'getContainersPaginated',
params,
);
return {
'containers': (result['containers'] as List<dynamic>?)
?.map((e) => e as Map<String, dynamic>)
.toList() ?? [],
?.map((e) => e as Map<String, dynamic>)
.toList() ??
[],
'hasMore': result['hasMore'] as bool? ?? false,
'lastVisible': result['lastVisible'] as String?,
'total': result['total'] as int? ?? 0,
};
} catch (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');
}
}
@@ -499,6 +523,156 @@ class DataService {
}
}
/// Recherche des équipements pour l'assistant IA avec fallback paginé.
Future<List<Map<String, dynamic>>> searchEquipmentsForAssistant({
required String query,
int limit = 12,
}) async {
final normalizedQuery = query.trim();
if (normalizedQuery.isEmpty) {
return [];
}
try {
final quickResults = await quickSearch(
normalizedQuery,
limit: limit,
includeEquipments: true,
includeContainers: false,
);
final equipmentResults = quickResults
.where((item) =>
(item['type']?.toString().toLowerCase() ?? '') == 'equipment')
.map(_normalizeAssistantEquipment)
.toList();
if (equipmentResults.isNotEmpty) {
return equipmentResults;
}
final paginated = await getEquipmentsPaginated(
limit: limit,
searchQuery: normalizedQuery,
sortBy: 'id',
sortOrder: 'asc',
);
final equipments =
paginated['equipments'] as List<Map<String, dynamic>>? ?? [];
return equipments.map(_normalizeAssistantEquipment).toList();
} catch (e) {
DebugLog.error('[DataService] Error in searchEquipmentsForAssistant', e);
throw Exception('Erreur lors de la recherche de matériel: $e');
}
}
/// Vérifie la disponibilité d'un équipement dans un format normalisé pour l'IA.
Future<Map<String, dynamic>> checkEquipmentAvailabilityForAssistant({
required String equipmentId,
required DateTime startDate,
required DateTime endDate,
String? excludeEventId,
}) async {
try {
final result = await checkEquipmentAvailability(
equipmentId: equipmentId,
startDate: startDate,
endDate: endDate,
excludeEventId: excludeEventId,
);
final available = result['available'] as bool? ?? true;
final conflicts = (result['conflicts'] as List<dynamic>? ?? const [])
.whereType<Map<String, dynamic>>()
.map((conflict) {
final eventData =
conflict['eventData'] as Map<String, dynamic>? ?? const {};
final eventName =
(eventData['Name'] ?? conflict['eventName'] ?? '').toString();
return {
'eventId': conflict['eventId']?.toString() ?? '',
'eventName': eventName,
'overlapDays': conflict['overlapDays'] as int? ?? 0,
};
}).toList();
return {
'equipmentId': equipmentId,
'available': available,
'conflictCount': conflicts.length,
'conflicts': conflicts,
};
} catch (e) {
DebugLog.error(
'[DataService] Error in checkEquipmentAvailabilityForAssistant', e);
throw Exception('Erreur lors de la vérification de disponibilité: $e');
}
}
/// Retourne des événements passés, idéalement filtrés par type d'événement.
Future<List<Map<String, dynamic>>> getPastEventsForAssistant({
String? eventTypeId,
int limit = 10,
}) async {
try {
final now = DateTime.now();
final events = eventTypeId != null && eventTypeId.isNotEmpty
? await getEventsByEventType(eventTypeId)
: (await getEvents())['events'] as List<Map<String, dynamic>>? ?? [];
final pastEvents = events.where((event) {
final endDate = _parseEventDate(event['EndDateTime']);
return endDate != null && endDate.isBefore(now);
}).toList();
pastEvents.sort((a, b) {
final aDate = _parseEventDate(a['StartDateTime']) ??
DateTime.fromMillisecondsSinceEpoch(0);
final bDate = _parseEventDate(b['StartDateTime']) ??
DateTime.fromMillisecondsSinceEpoch(0);
return bDate.compareTo(aDate);
});
return pastEvents.take(limit).map((event) {
final assignedEquipment =
event['assignedEquipment'] as List<dynamic>? ?? const [];
return {
'id': event['id']?.toString() ?? '',
'name': (event['Name'] ?? '').toString(),
'startDate': event['StartDateTime']?.toString() ?? '',
'endDate': event['EndDateTime']?.toString() ?? '',
'assignedEquipment': assignedEquipment,
'assignedEquipmentCount': assignedEquipment.length,
};
}).toList();
} catch (e) {
DebugLog.error('[DataService] Error in getPastEventsForAssistant', e);
throw Exception(
'Erreur lors de la récupération des événements passés: $e');
}
}
Map<String, dynamic> _normalizeAssistantEquipment(Map<String, dynamic> item) {
return {
'id': (item['id'] ?? '').toString(),
'name': (item['name'] ?? item['id'] ?? '').toString(),
'category': (item['category'] ?? '').toString(),
'status': (item['status'] ?? '').toString(),
'brand': item['brand']?.toString(),
'model': item['model']?.toString(),
'availableQuantity': item['availableQuantity'],
'totalQuantity': item['totalQuantity'],
};
}
DateTime? _parseEventDate(dynamic rawValue) {
if (rawValue is String) {
return DateTime.tryParse(rawValue);
}
return null;
}
// ============================================================================
// USER - Current User
// ============================================================================
@@ -512,7 +686,8 @@ class DataService {
return result['user'] as Map<String, dynamic>;
} catch (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 +768,8 @@ class DataService {
});
return result;
} 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 +778,8 @@ class DataService {
// ============================================================================
/// 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 {
final data = <String, dynamic>{};
if (equipmentId != null) data['equipmentId'] = equipmentId;
@@ -619,14 +796,16 @@ class DataService {
/// Supprime une maintenance
Future<void> deleteMaintenance(String maintenanceId) async {
try {
await _apiService.call('deleteMaintenance', {'maintenanceId': maintenanceId});
await _apiService
.call('deleteMaintenance', {'maintenanceId': maintenanceId});
} catch (e) {
throw Exception('Erreur lors de la suppression de la maintenance: $e');
}
}
/// 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 {
final result = await _apiService.call('getContainersByEquipment', {
'equipmentId': equipmentId,