feat: merge branche IA (beta) - Intégration assistant IA logisticien Gemini

This commit is contained in:
ElPoyo
2026-05-25 23:35:40 +02:00
15 changed files with 3394 additions and 163 deletions
+125 -86
View File
@@ -7,6 +7,8 @@ import 'package:em2rp/providers/container_provider.dart';
import 'package:em2rp/providers/equipment_provider.dart';
import 'package:em2rp/utils/debug_log.dart';
import 'package:em2rp/utils/id_generator.dart';
import 'package:em2rp/services/data_service.dart';
import 'package:em2rp/services/api_service.dart';
class ContainerFormPage extends StatefulWidget {
final ContainerModel? container;
@@ -650,25 +652,86 @@ class _EquipmentSelectorDialog extends StatefulWidget {
class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
final TextEditingController _searchController = TextEditingController();
final ScrollController _scrollController = ScrollController();
final DataService _dataService = DataService(FirebaseFunctionsApiService());
EquipmentCategory? _filterCategory;
String _searchQuery = '';
late Set<String> _tempSelectedIds;
late final Future<void> _loadingFuture;
final List<EquipmentModel> _paginatedEquipments = [];
bool _isLoadingMore = false;
bool _hasMoreEquipments = true;
String? _lastEquipmentId;
@override
void initState() {
super.initState();
// Créer une copie temporaire des IDs sélectionnés
_tempSelectedIds = Set<String>.from(widget.selectedIds);
_loadingFuture = widget.equipmentProvider.loadEquipments();
_scrollController.addListener(_onScroll);
_loadNextPage();
}
@override
void dispose() {
_searchController.dispose();
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
if (_isLoadingMore) return;
if (_scrollController.hasClients &&
_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 300) {
if (_hasMoreEquipments) {
_loadNextPage();
}
}
}
Future<void> _loadNextPage() async {
if (_isLoadingMore || !_hasMoreEquipments) return;
setState(() => _isLoadingMore = true);
try {
final result = await _dataService.getEquipmentsPaginated(
limit: 50,
startAfter: _lastEquipmentId,
searchQuery: _searchQuery.isNotEmpty ? _searchQuery : null,
category: _filterCategory != null ? equipmentCategoryToString(_filterCategory!) : null,
sortBy: 'id',
sortOrder: 'asc',
);
final newEquipments = (result['equipments'] as List<dynamic>)
.map((data) => EquipmentModel.fromMap(data as Map<String, dynamic>, data['id'] as String))
.toList();
if (mounted) {
setState(() {
_paginatedEquipments.addAll(newEquipments);
_hasMoreEquipments = result['hasMore'] as bool? ?? false;
_lastEquipmentId = result['lastVisible'] as String?;
_isLoadingMore = false;
});
}
} catch (e) {
if (mounted) {
setState(() => _isLoadingMore = false);
}
}
}
Future<void> _reloadData() async {
setState(() {
_paginatedEquipments.clear();
_lastEquipmentId = null;
_hasMoreEquipments = true;
});
await _loadNextPage();
}
@override
Widget build(BuildContext context) {
return Dialog(
@@ -718,6 +781,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
setState(() {
_searchQuery = '';
});
_reloadData();
},
)
: null,
@@ -726,6 +790,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
setState(() {
_searchQuery = value;
});
_reloadData();
},
),
const SizedBox(height: 16),
@@ -743,6 +808,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
setState(() {
_filterCategory = null;
});
_reloadData();
},
selectedColor: AppColors.rouge,
labelStyle: TextStyle(
@@ -761,6 +827,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
setState(() {
_filterCategory = selected ? category : null;
});
_reloadData();
},
selectedColor: AppColors.rouge,
labelStyle: TextStyle(
@@ -780,7 +847,7 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.rouge.withOpacity(0.1),
color: AppColors.rouge.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
@@ -798,90 +865,62 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> {
// Liste des équipements
Expanded(
child: FutureBuilder<void>(
future: _loadingFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('Erreur: ${snapshot.error}'));
}
var equipment = List<EquipmentModel>.from(
widget.equipmentProvider.allEquipment,
);
// Filtrer par catégorie
if (_filterCategory != null) {
equipment = equipment
.where((e) => e.category == _filterCategory)
.toList();
}
// Filtrer par recherche
if (_searchQuery.isNotEmpty) {
final query = _searchQuery.toLowerCase();
equipment = equipment.where((e) {
return e.id.toLowerCase().contains(query) ||
(e.brand?.toLowerCase().contains(query) ?? false) ||
(e.model?.toLowerCase().contains(query) ?? false);
}).toList();
}
if (equipment.isEmpty) {
return const Center(
child: Text('Aucun équipement trouvé'),
);
}
return ListView.builder(
itemCount: equipment.length,
itemBuilder: (context, index) {
final item = equipment[index];
final isSelected = _tempSelectedIds.contains(item.id);
return CheckboxListTile(
value: isSelected,
onChanged: (selected) {
setState(() {
if (selected == true) {
_tempSelectedIds.add(item.id);
} else {
_tempSelectedIds.remove(item.id);
}
});
},
title: Text(
item.id,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.brand != null || item.model != null)
Text('${item.brand ?? ''} ${item.model ?? ''}'),
const SizedBox(height: 4),
Text(
_getCategoryLabel(item.category),
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
child: _paginatedEquipments.isEmpty && !_isLoadingMore
? const Center(child: Text('Aucun équipement trouvé'))
: ListView.builder(
controller: _scrollController,
itemCount: _paginatedEquipments.length + (_isLoadingMore ? 1 : 0),
itemBuilder: (context, index) {
if (index == _paginatedEquipments.length) {
return const Center(
child: Padding(
padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
),
],
),
secondary: Icon(
_getCategoryIcon(item.category),
color: AppColors.rouge,
),
activeColor: AppColors.rouge,
);
},
);
},
),
);
}
final item = _paginatedEquipments[index];
final isSelected = _tempSelectedIds.contains(item.id);
return CheckboxListTile(
value: isSelected,
onChanged: (selected) {
setState(() {
if (selected == true) {
_tempSelectedIds.add(item.id);
} else {
_tempSelectedIds.remove(item.id);
}
});
},
title: Text(
item.id,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (item.brand != null || item.model != null)
Text('${item.brand ?? ''} ${item.model ?? ''}'),
const SizedBox(height: 4),
Text(
_getCategoryLabel(item.category),
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
secondary: Icon(
_getCategoryIcon(item.category),
color: AppColors.rouge,
),
activeColor: AppColors.rouge,
);
},
),
),
// Boutons d'action