feat: merge branche IA (beta) - Intégration assistant IA logisticien Gemini
This commit is contained in:
@@ -222,7 +222,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialise la sélection avec le matériel déjà assigné
|
||||
/// Initialise la slection avec le matriel dj assign
|
||||
Future<void> _initializeAlreadyAssigned() async {
|
||||
final Map<String, SelectedItem> initialSelection = {};
|
||||
|
||||
@@ -304,7 +304,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
|
||||
try {
|
||||
final result = await _dataService.getEquipmentsPaginated(
|
||||
limit: 25,
|
||||
limit: 50,
|
||||
startAfter: _lastEquipmentId,
|
||||
searchQuery: _searchQuery.isNotEmpty ? _searchQuery : null,
|
||||
category: _selectedCategory != null
|
||||
@@ -331,7 +331,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
DebugLog.info(
|
||||
'[EquipmentSelectionDialog] Loaded ${newEquipments.length} equipments, total: ${_paginatedEquipments.length}, hasMore: $_hasMoreEquipments');
|
||||
|
||||
// Charger les quantités pour les consommables/câbles de cette page
|
||||
// Charger les quantites pour les consommables/cbles de cette page
|
||||
await _loadAvailableQuantities(newEquipments);
|
||||
|
||||
// Si la liste ne peut pas scroller, précharger la page suivante.
|
||||
@@ -354,7 +354,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
|
||||
try {
|
||||
final result = await _dataService.getContainersPaginated(
|
||||
limit: 25,
|
||||
limit: 50,
|
||||
startAfter: _lastContainerId,
|
||||
searchQuery: _searchQuery.isNotEmpty ? _searchQuery : null,
|
||||
category: _selectedCategory?.name, // Filtre par catégorie d'équipements
|
||||
@@ -421,7 +421,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
DebugLog.info(
|
||||
'[EquipmentSelectionDialog] Cached ${allEquipmentsToCache.length} equipment(s) from containers, total cache: ${_cachedEquipment.length}');
|
||||
|
||||
// Mettre à jour les statuts de conflit pour les nouveaux containers
|
||||
// Mettre jour les statuts de conflit pour les nouveaux containers
|
||||
await _updateContainerConflictStatus();
|
||||
|
||||
// Si la liste ne peut pas scroller, précharger la page suivante.
|
||||
@@ -454,6 +454,40 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
void _checkIfMoreItemsNeeded() {
|
||||
if (!mounted || _isLoadingMore) return;
|
||||
|
||||
int visibleItems = 0;
|
||||
if (_displayType == SelectionType.equipment) {
|
||||
visibleItems = _paginatedEquipments.where((eq) {
|
||||
return _showConflictingItems || !_conflictingEquipmentIds.contains(eq.id);
|
||||
}).length;
|
||||
|
||||
if (visibleItems < 15 && _hasMoreEquipments) {
|
||||
_loadNextEquipmentPage();
|
||||
} else if (_scrollController.hasClients && _scrollController.position.maxScrollExtent <= 0 && _hasMoreEquipments) {
|
||||
_loadNextEquipmentPage();
|
||||
}
|
||||
} else {
|
||||
visibleItems = _paginatedContainers.where((container) {
|
||||
if (!_showConflictingItems) {
|
||||
if (_conflictingContainerIds.contains(container.id)) return false;
|
||||
final hasConflictingChildren = container.equipmentIds.any(
|
||||
(eqId) => _conflictingEquipmentIds.contains(eqId),
|
||||
);
|
||||
if (hasConflictingChildren) return false;
|
||||
}
|
||||
return true;
|
||||
}).length;
|
||||
|
||||
if (visibleItems < 15 && _hasMoreContainers) {
|
||||
_loadNextContainerPage();
|
||||
} else if (_scrollController.hasClients && _scrollController.position.maxScrollExtent <= 0 && _hasMoreContainers) {
|
||||
_loadNextContainerPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
@@ -1539,7 +1573,8 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -0,0 +1,755 @@
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/services/ai_equipment_assistant_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
/// Résultat retourné par le dialog après confirmation de la proposition IA.
|
||||
class AiProposalResult {
|
||||
final List<EventEquipment> equipment;
|
||||
final List<String> containerIds;
|
||||
|
||||
const AiProposalResult({
|
||||
required this.equipment,
|
||||
required this.containerIds,
|
||||
});
|
||||
}
|
||||
|
||||
class AiEquipmentAssistantDialog extends StatefulWidget {
|
||||
final DateTime startDate;
|
||||
final DateTime endDate;
|
||||
final String? eventTypeId;
|
||||
final String? excludeEventId;
|
||||
final List<EventEquipment> currentAssignedEquipment;
|
||||
|
||||
const AiEquipmentAssistantDialog({
|
||||
super.key,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.currentAssignedEquipment,
|
||||
this.eventTypeId,
|
||||
this.excludeEventId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AiEquipmentAssistantDialog> createState() =>
|
||||
_AiEquipmentAssistantDialogState();
|
||||
}
|
||||
|
||||
class _AiEquipmentAssistantDialogState
|
||||
extends State<AiEquipmentAssistantDialog> {
|
||||
final TextEditingController _messageController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final ScrollController _proposalScrollController = ScrollController();
|
||||
final List<_AssistantChatMessage> _messages = [];
|
||||
|
||||
late final AiEquipmentAssistantService _assistantService;
|
||||
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
AiEquipmentProposal? _latestProposal;
|
||||
late List<EventEquipment> _workingEquipment;
|
||||
AiEquipmentDocument? _selectedDocument;
|
||||
List<String> _sessionLogs = [];
|
||||
Set<String> _selectedContainerIds = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_assistantService = AiEquipmentAssistantService();
|
||||
_workingEquipment = List<EventEquipment>.from(widget.currentAssignedEquipment);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_messageController.dispose();
|
||||
_scrollController.dispose();
|
||||
_proposalScrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
bool get _isChatEmpty => _messages.isEmpty;
|
||||
|
||||
String get _actionButtonLabel {
|
||||
return _isChatEmpty ? 'Generer la liste automatiquement' : 'Envoyer';
|
||||
}
|
||||
|
||||
Future<void> _sendMessage() async {
|
||||
if (_isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
final rawInput = _messageController.text.trim();
|
||||
final isAutoMode = _isChatEmpty;
|
||||
final userMessage = isAutoMode
|
||||
? (rawInput.isNotEmpty
|
||||
? rawInput
|
||||
: 'Genere automatiquement une proposition de materiel pour cet evenement.')
|
||||
: rawInput;
|
||||
|
||||
if (userMessage.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_messageController.clear();
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
_messages.add(_AssistantChatMessage.user(userMessage));
|
||||
if (_selectedDocument != null) {
|
||||
_messages.add(_AssistantChatMessage.user('[Document joint : ${_selectedDocument!.fileName ?? "Document"}]'));
|
||||
}
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
_scrollToBottom();
|
||||
|
||||
try {
|
||||
final documentToSend = _selectedDocument;
|
||||
_selectedDocument = null; // Clear after sending
|
||||
final response = await _assistantService
|
||||
.generateProposal(
|
||||
startDate: widget.startDate,
|
||||
endDate: widget.endDate,
|
||||
eventTypeId: widget.eventTypeId,
|
||||
excludeEventId: widget.excludeEventId,
|
||||
currentAssignedEquipment: widget.currentAssignedEquipment,
|
||||
workingProposalEquipment: _workingEquipment,
|
||||
userMessage: userMessage,
|
||||
document: documentToSend,
|
||||
history: _messages
|
||||
.map((message) => AiAssistantChatTurn(
|
||||
isUser: message.isUser, text: message.text))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_messages
|
||||
.add(_AssistantChatMessage.assistant(response.assistantMessage));
|
||||
_latestProposal = response.proposal;
|
||||
if (response.proposal != null) {
|
||||
_workingEquipment = List<EventEquipment>.from(
|
||||
response.proposal!.asEventEquipment,
|
||||
);
|
||||
// Préselectionner les containers non partiels
|
||||
_selectedContainerIds = {
|
||||
for (final c in response.proposal!.containers)
|
||||
if (!c.partial) c.containerId
|
||||
};
|
||||
}
|
||||
_sessionLogs.addAll(response.debugLogs);
|
||||
_isLoading = false;
|
||||
});
|
||||
_scrollToBottom();
|
||||
} on FormatException catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = 'Reponse IA invalide: ${error.message}';
|
||||
});
|
||||
} catch (error) {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = 'Erreur IA: $error';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!_scrollController.hasClients) {
|
||||
return;
|
||||
}
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pickDocument() async {
|
||||
try {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['pdf', 'txt', 'jpg', 'jpeg', 'png'],
|
||||
withData: true,
|
||||
);
|
||||
|
||||
if (result != null && result.files.isNotEmpty) {
|
||||
final file = result.files.first;
|
||||
if (file.bytes != null) {
|
||||
final base64String = base64Encode(file.bytes!);
|
||||
String mimeType = 'application/octet-stream';
|
||||
if (file.extension == 'pdf') mimeType = 'application/pdf';
|
||||
else if (file.extension == 'txt') mimeType = 'text/plain';
|
||||
else if (file.extension == 'jpg' || file.extension == 'jpeg') mimeType = 'image/jpeg';
|
||||
else if (file.extension == 'png') mimeType = 'image/png';
|
||||
|
||||
setState(() {
|
||||
_selectedDocument = AiEquipmentDocument(
|
||||
base64Data: base64String,
|
||||
mimeType: mimeType,
|
||||
fileName: file.name,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_errorMessage = 'Erreur lors de la selection du document : $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showLogsDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Logs de l\'IA'),
|
||||
content: SizedBox(
|
||||
width: 800,
|
||||
height: 600,
|
||||
child: ListView.builder(
|
||||
itemCount: _sessionLogs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final log = _sessionLogs[index];
|
||||
final isError = log.startsWith('[ERROR]');
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Text(
|
||||
log,
|
||||
style: TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
color: isError ? Colors.red : Colors.black87,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final fullLogs = _sessionLogs.join('\n');
|
||||
Clipboard.setData(ClipboardData(text: fullLogs));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Logs copiés dans le presse-papiers')),
|
||||
);
|
||||
},
|
||||
child: const Text('Copier tout'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
||||
child: SizedBox(
|
||||
width: 760,
|
||||
height: 640,
|
||||
child: Column(
|
||||
children: [
|
||||
AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
title: const Text('(BETA) Assistant IA Logisticien'),
|
||||
actions: [
|
||||
if (_sessionLogs.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bug_report),
|
||||
tooltip: 'Voir les logs',
|
||||
onPressed: _showLogsDialog,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed:
|
||||
_isLoading ? null : () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: Colors.grey.shade50,
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = _messages[index];
|
||||
return _buildMessageBubble(message);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isLoading)
|
||||
const Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: const Text(
|
||||
'Generation en cours... verification du materiel et disponibilites. (Cela peut prendre jusqu\'a une minute en cas de forte affluence)',
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_errorMessage != null)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.red.shade200),
|
||||
),
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(color: Colors.red.shade800),
|
||||
),
|
||||
),
|
||||
if (_latestProposal != null)
|
||||
_buildProposalSummary(_latestProposal!),
|
||||
if (_selectedDocument != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.attach_file, color: Colors.blue, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_selectedDocument!.fileName ?? 'Document joint',
|
||||
style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_selectedDocument = null;
|
||||
});
|
||||
},
|
||||
tooltip: 'Retirer le document',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.attach_file),
|
||||
onPressed: _isLoading ? null : _pickDocument,
|
||||
tooltip: 'Joindre un devis ou document',
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _messageController,
|
||||
enabled: !_isLoading,
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
decoration: const InputDecoration(
|
||||
hintText:
|
||||
'Precisez votre besoin (style, jauge, contraintes...)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _sendMessage,
|
||||
child: Text(_actionButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageBubble(_AssistantChatMessage message) {
|
||||
final bubbleColor = message.isUser ? Colors.blue.shade600 : Colors.white;
|
||||
final textColor = message.isUser ? Colors.white : Colors.black87;
|
||||
|
||||
return Align(
|
||||
alignment: message.isUser ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: bubbleColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(16),
|
||||
topRight: const Radius.circular(16),
|
||||
bottomLeft: Radius.circular(message.isUser ? 16 : 4),
|
||||
bottomRight: Radius.circular(message.isUser ? 4 : 16),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
border:
|
||||
message.isUser ? null : Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: message.isUser
|
||||
? Text(message.text, style: TextStyle(color: textColor))
|
||||
: _buildAssistantMessageContent(message.text),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAssistantMessageContent(String text) {
|
||||
// Si le message semble structuré par l'IA avec nos nouvelles règles
|
||||
if (text.contains('Matériel ajouté :') || text.contains('Matériel non trouvé')) {
|
||||
final sections = text.split('\n\n');
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: sections.map((section) {
|
||||
final isAdded = section.contains('Matériel ajouté :');
|
||||
final isMissing = section.contains('Matériel non trouvé');
|
||||
|
||||
if (isAdded) {
|
||||
return _buildStatusSection(
|
||||
title: section.split('\n').first,
|
||||
content: section.split('\n').skip(1).join('\n'),
|
||||
icon: Icons.check_circle_outline,
|
||||
color: Colors.green.shade700,
|
||||
bgColor: Colors.green.shade50,
|
||||
);
|
||||
} else if (isMissing) {
|
||||
return _buildStatusSection(
|
||||
title: section.split('\n').first,
|
||||
content: section.split('\n').skip(1).join('\n'),
|
||||
icon: Icons.warning_amber_rounded,
|
||||
color: Colors.orange.shade800,
|
||||
bgColor: Colors.orange.shade50,
|
||||
);
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(section),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
return Text(text);
|
||||
}
|
||||
|
||||
Widget _buildStatusSection({
|
||||
required String title,
|
||||
required String content,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required Color bgColor,
|
||||
}) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withValues(alpha: 0.3)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: color),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title.replaceAll(':', '').trim(),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (content.trim().isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
content.trim(),
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey.shade800),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmProposal({bool excludeAlternatives = false}) {
|
||||
if (_latestProposal == null) return;
|
||||
|
||||
List<EventEquipment> equipment = List.from(_latestProposal!.asEventEquipment);
|
||||
// Ne renvoyer que les containerIds sélectionnés (par défaut les containers complets)
|
||||
final List<String> containerIds = _selectedContainerIds.isNotEmpty
|
||||
? _selectedContainerIds.toList()
|
||||
: List.from(_latestProposal!.containerIds);
|
||||
|
||||
if (excludeAlternatives) {
|
||||
// On utilise la liste des items d'origine pour savoir lesquels exclure
|
||||
// car ils contiennent le champ rationale (avant conversion en EventEquipment)
|
||||
final idsToExclude = _latestProposal!.items
|
||||
.where((item) {
|
||||
final rationale = item.rationale.toLowerCase();
|
||||
return rationale.contains('alternative') ||
|
||||
rationale.contains('remplacement') ||
|
||||
rationale.contains('indisponible');
|
||||
})
|
||||
.map((item) => item.equipmentId)
|
||||
.toSet();
|
||||
|
||||
equipment = equipment.where((eq) => !idsToExclude.contains(eq.equipmentId)).toList();
|
||||
}
|
||||
|
||||
Navigator.of(context).pop(
|
||||
AiProposalResult(
|
||||
equipment: equipment,
|
||||
containerIds: containerIds,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProposalSummary(AiEquipmentProposal proposal) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
padding: const EdgeInsets.all(16),
|
||||
constraints: const BoxConstraints(maxHeight: 280),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.assignment_turned_in, color: Colors.indigo),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Récapitulatif de la proposition IA',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: Colors.indigo,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Flexible(
|
||||
child: Scrollbar(
|
||||
controller: _proposalScrollController,
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
controller: _proposalScrollController,
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
proposal.summary,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
if (proposal.items.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Matériel individuel :',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
...proposal.items.map((item) {
|
||||
final isAlt = item.rationale.toLowerCase().contains('alternative') || item.rationale.toLowerCase().contains('remplacement');
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6, left: 4),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
isAlt ? Icons.swap_horiz : Icons.add_circle_outline,
|
||||
size: 14,
|
||||
color: isAlt ? Colors.orange : Colors.indigo,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${item.equipmentId} x${item.quantity}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w500)
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
if (proposal.containers.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Fly-cases & Boîtes :',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
...proposal.containers.map((c) {
|
||||
final isPartial = c.partial;
|
||||
final isSelected = _selectedContainerIds.contains(c.containerId);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6, left: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inventory_2_outlined,
|
||||
size: 14,
|
||||
color: c.available == false ? Colors.red : Colors.indigo,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: Text('${c.containerId} ${c.rationale.isNotEmpty ? "- ${c.rationale}" : ""}', style: const TextStyle(fontWeight: FontWeight.w500))),
|
||||
if (c.available == false)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Icon(Icons.block, color: Colors.red.shade700, size: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isPartial) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text('Contenu partiel : ${c.matchingEquipmentIds.length}/${c.equipmentIds.length} items utilisés.', style: TextStyle(fontSize: 12, color: Colors.grey.shade700)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (isPartial)
|
||||
Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
if (v == true) _selectedContainerIds.add(c.containerId);
|
||||
else _selectedContainerIds.remove(c.containerId);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isLoading ? null : () => _confirmProposal(),
|
||||
icon: const Icon(Icons.check),
|
||||
label: const Text('Tout ajouter'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.indigo,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: _isLoading ? null : () => _confirmProposal(excludeAlternatives: true),
|
||||
icon: const Icon(Icons.filter_list_off),
|
||||
label: const Text('Ajouter sans alternatives'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.indigo,
|
||||
side: const BorderSide(color: Colors.indigo),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AssistantChatMessage {
|
||||
final bool isUser;
|
||||
final String text;
|
||||
|
||||
const _AssistantChatMessage._({required this.isUser, required this.text});
|
||||
|
||||
factory _AssistantChatMessage.user(String text) {
|
||||
return _AssistantChatMessage._(isUser: true, text: text);
|
||||
}
|
||||
|
||||
factory _AssistantChatMessage.assistant(String text) {
|
||||
return _AssistantChatMessage._(isUser: false, text: text);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import 'package:em2rp/providers/equipment_provider.dart';
|
||||
import 'package:em2rp/providers/container_provider.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
import 'package:em2rp/views/widgets/event/equipment_selection_dialog.dart';
|
||||
import 'package:em2rp/views/widgets/event_form/ai_equipment_assistant_dialog.dart';
|
||||
|
||||
/// Section pour afficher et gérer le matériel assigné à un événement
|
||||
class EventAssignedEquipmentSection extends StatefulWidget {
|
||||
@@ -17,6 +18,7 @@ class EventAssignedEquipmentSection extends StatefulWidget {
|
||||
final DateTime? endDate;
|
||||
final Function(List<EventEquipment>, List<String>) onChanged;
|
||||
final String? eventId; // Pour exclure l'événement actuel de la vérification
|
||||
final String? eventTypeId;
|
||||
|
||||
const EventAssignedEquipmentSection({
|
||||
super.key,
|
||||
@@ -26,14 +28,18 @@ class EventAssignedEquipmentSection extends StatefulWidget {
|
||||
required this.endDate,
|
||||
required this.onChanged,
|
||||
this.eventId,
|
||||
this.eventTypeId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EventAssignedEquipmentSection> createState() => _EventAssignedEquipmentSectionState();
|
||||
State<EventAssignedEquipmentSection> createState() =>
|
||||
_EventAssignedEquipmentSectionState();
|
||||
}
|
||||
|
||||
class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSection> {
|
||||
bool get _canAddMaterial => widget.startDate != null && widget.endDate != null;
|
||||
class _EventAssignedEquipmentSectionState
|
||||
extends State<EventAssignedEquipmentSection> {
|
||||
bool get _canAddMaterial =>
|
||||
widget.startDate != null && widget.endDate != null;
|
||||
final Map<String, EquipmentModel> _equipmentCache = {};
|
||||
final Map<String, ContainerModel> _containerCache = {};
|
||||
bool _isLoading = true;
|
||||
@@ -61,19 +67,24 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
final equipmentProvider = context.read<EquipmentProvider>();
|
||||
final containerProvider = context.read<ContainerProvider>();
|
||||
|
||||
DebugLog.info('[EventAssignedEquipmentSection] Loading caches from assigned lists');
|
||||
DebugLog.info(
|
||||
'[EventAssignedEquipmentSection] Loading caches from assigned lists');
|
||||
|
||||
// Toujours partir des données locales du formulaire pour éviter les décalages visuels.
|
||||
final equipmentIds = widget.assignedEquipment.map((eq) => eq.equipmentId).toList();
|
||||
final containers = await containerProvider.getContainersByIds(widget.assignedContainers);
|
||||
final equipmentIds =
|
||||
widget.assignedEquipment.map((eq) => eq.equipmentId).toList();
|
||||
final containers =
|
||||
await containerProvider.getContainersByIds(widget.assignedContainers);
|
||||
|
||||
final childEquipmentIds = <String>[];
|
||||
for (final container in containers) {
|
||||
childEquipmentIds.addAll(container.equipmentIds);
|
||||
}
|
||||
|
||||
final allEquipmentIds = <String>{...equipmentIds, ...childEquipmentIds}.toList();
|
||||
final equipment = await equipmentProvider.getEquipmentsByIds(allEquipmentIds);
|
||||
final allEquipmentIds =
|
||||
<String>{...equipmentIds, ...childEquipmentIds}.toList();
|
||||
final equipment =
|
||||
await equipmentProvider.getEquipmentsByIds(allEquipmentIds);
|
||||
|
||||
_equipmentCache.clear();
|
||||
_containerCache.clear();
|
||||
@@ -110,7 +121,9 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
_containerCache[containerId] = container;
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLog.error('[EventAssignedEquipmentSection] Error loading equipment and containers', e);
|
||||
DebugLog.error(
|
||||
'[EventAssignedEquipmentSection] Error loading equipment and containers',
|
||||
e);
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
@@ -138,7 +151,8 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
}
|
||||
|
||||
Future<void> _processSelection(Map<String, SelectedItem> selection) async {
|
||||
DebugLog.info('[EventAssignedEquipmentSection] Processing selection of ${selection.length} items');
|
||||
DebugLog.info(
|
||||
'[EventAssignedEquipmentSection] Processing selection of ${selection.length} items');
|
||||
|
||||
// Séparer équipements et conteneurs
|
||||
final newEquipment = <EventEquipment>[];
|
||||
@@ -155,23 +169,27 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
}
|
||||
}
|
||||
|
||||
DebugLog.info('[EventAssignedEquipmentSection] Found ${newEquipment.length} equipment(s) and ${newContainers.length} container(s)');
|
||||
DebugLog.info(
|
||||
'[EventAssignedEquipmentSection] Found ${newEquipment.length} equipment(s) and ${newContainers.length} container(s)');
|
||||
|
||||
// 🔧 FIX: Pour chaque container sélectionné, ajouter aussi ses équipements enfants
|
||||
if (newContainers.isNotEmpty) {
|
||||
final containerProvider = context.read<ContainerProvider>();
|
||||
final containers = await containerProvider.getContainersByIds(newContainers);
|
||||
final containers =
|
||||
await containerProvider.getContainersByIds(newContainers);
|
||||
|
||||
for (var container in containers) {
|
||||
for (var childEquipmentId in container.equipmentIds) {
|
||||
// Vérifier si l'équipement enfant n'est pas déjà dans la liste
|
||||
final existsInNew = newEquipment.any((eq) => eq.equipmentId == childEquipmentId);
|
||||
final existsInNew =
|
||||
newEquipment.any((eq) => eq.equipmentId == childEquipmentId);
|
||||
if (!existsInNew) {
|
||||
newEquipment.add(EventEquipment(
|
||||
equipmentId: childEquipmentId,
|
||||
quantity: 1,
|
||||
));
|
||||
DebugLog.info('[EventAssignedEquipmentSection] Adding child equipment $childEquipmentId from container ${container.id}');
|
||||
DebugLog.info(
|
||||
'[EventAssignedEquipmentSection] Adding child equipment $childEquipmentId from container ${container.id}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,11 +201,12 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
// Fusionner avec l'existant
|
||||
final updatedEquipment = [...widget.assignedEquipment];
|
||||
final updatedContainers = [...widget.assignedContainers];
|
||||
|
||||
|
||||
// Pour chaque nouvel équipement
|
||||
for (var eq in newEquipment) {
|
||||
final existingIndex = updatedEquipment.indexWhere((e) => e.equipmentId == eq.equipmentId);
|
||||
|
||||
final existingIndex =
|
||||
updatedEquipment.indexWhere((e) => e.equipmentId == eq.equipmentId);
|
||||
|
||||
if (existingIndex != -1) {
|
||||
// L'équipement existe déjà : mettre à jour la quantité
|
||||
updatedEquipment[existingIndex] = EventEquipment(
|
||||
@@ -204,17 +223,85 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
updatedEquipment.add(eq);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (var containerId in newContainers) {
|
||||
if (!updatedContainers.contains(containerId)) {
|
||||
updatedContainers.add(containerId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Notifier le changement
|
||||
widget.onChanged(updatedEquipment, updatedContainers);
|
||||
}
|
||||
|
||||
Future<void> _openAiAssistantDialog() async {
|
||||
if (widget.startDate == null || widget.endDate == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await showDialog<AiProposalResult>(
|
||||
context: context,
|
||||
builder: (context) => AiEquipmentAssistantDialog(
|
||||
startDate: widget.startDate!,
|
||||
endDate: widget.endDate!,
|
||||
eventTypeId: widget.eventTypeId,
|
||||
excludeEventId: widget.eventId,
|
||||
currentAssignedEquipment: widget.assignedEquipment,
|
||||
),
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_applyAiProposal(result);
|
||||
}
|
||||
|
||||
void _applyAiProposal(AiProposalResult result) async {
|
||||
final existingById = {
|
||||
for (final equipment in widget.assignedEquipment)
|
||||
equipment.equipmentId: equipment,
|
||||
};
|
||||
|
||||
final updatedEquipment = result.equipment.map((proposed) {
|
||||
final existing = existingById[proposed.equipmentId];
|
||||
if (existing == null) {
|
||||
return proposed;
|
||||
}
|
||||
return existing.copyWith(quantity: proposed.quantity, rationale: proposed.rationale);
|
||||
}).toList();
|
||||
|
||||
// 🔧 FIX: Pour chaque container ajouté par l'IA, ajouter aussi ses équipements enfants
|
||||
if (result.containerIds.isNotEmpty) {
|
||||
final containerProvider = context.read<ContainerProvider>();
|
||||
final containers = await containerProvider.getContainersByIds(result.containerIds);
|
||||
|
||||
for (var container in containers) {
|
||||
for (var childEquipmentId in container.equipmentIds) {
|
||||
// Vérifier si l'équipement enfant n'est pas déjà dans la liste (ou déjà ajouté par la proposition)
|
||||
final exists = updatedEquipment.any((eq) => eq.equipmentId == childEquipmentId);
|
||||
if (!exists) {
|
||||
updatedEquipment.add(EventEquipment(
|
||||
equipmentId: childEquipmentId,
|
||||
quantity: 1,
|
||||
rationale: 'Inclus dans ${container.id}',
|
||||
));
|
||||
DebugLog.info('[EventAssignedEquipmentSection] AI adding child equipment $childEquipmentId from container ${container.id}');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final updatedContainers = [...widget.assignedContainers];
|
||||
for (final containerId in result.containerIds) {
|
||||
if (!updatedContainers.contains(containerId)) {
|
||||
updatedContainers.add(containerId);
|
||||
}
|
||||
}
|
||||
|
||||
widget.onChanged(updatedEquipment, updatedContainers);
|
||||
}
|
||||
|
||||
void _removeEquipment(String equipmentId) {
|
||||
final updated = widget.assignedEquipment
|
||||
.where((eq) => eq.equipmentId != equipmentId)
|
||||
@@ -231,9 +318,8 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
final container = _containerCache[containerId];
|
||||
|
||||
// Retirer le conteneur de la liste
|
||||
final updatedContainers = widget.assignedContainers
|
||||
.where((id) => id != containerId)
|
||||
.toList();
|
||||
final updatedContainers =
|
||||
widget.assignedContainers.where((id) => id != containerId).toList();
|
||||
|
||||
// 🔧 FIX: Ne supprimer les équipements enfants QUE s'ils ne sont pas dans un autre container
|
||||
final updatedEquipment = <EventEquipment>[];
|
||||
@@ -252,8 +338,10 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
// 1. Ne sont PAS dans le container supprimé OU
|
||||
// 2. Sont dans le container supprimé MAIS aussi dans un autre container
|
||||
for (var eq in widget.assignedEquipment) {
|
||||
final isInRemovedContainer = container.equipmentIds.contains(eq.equipmentId);
|
||||
final isInOtherContainer = equipmentIdsInOtherContainers.contains(eq.equipmentId);
|
||||
final isInRemovedContainer =
|
||||
container.equipmentIds.contains(eq.equipmentId);
|
||||
final isInOtherContainer =
|
||||
equipmentIdsInOtherContainers.contains(eq.equipmentId);
|
||||
|
||||
if (!isInRemovedContainer || isInOtherContainer) {
|
||||
updatedEquipment.add(eq);
|
||||
@@ -271,7 +359,8 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
_containerCache.remove(containerId);
|
||||
// Nettoyer le cache uniquement pour les équipements effectivement supprimés
|
||||
if (container != null) {
|
||||
final remainingEquipmentIds = updatedEquipment.map((eq) => eq.equipmentId).toSet();
|
||||
final remainingEquipmentIds =
|
||||
updatedEquipment.map((eq) => eq.equipmentId).toSet();
|
||||
for (var equipmentId in container.equipmentIds) {
|
||||
if (!remainingEquipmentIds.contains(equipmentId)) {
|
||||
_equipmentCache.remove(equipmentId);
|
||||
@@ -301,7 +390,8 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final totalItems = widget.assignedEquipment.length + widget.assignedContainers.length;
|
||||
final totalItems =
|
||||
widget.assignedEquipment.length + widget.assignedContainers.length;
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
@@ -350,15 +440,25 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
],
|
||||
),
|
||||
),
|
||||
ActionChip(
|
||||
onPressed: _canAddMaterial ? _openAiAssistantDialog : null,
|
||||
avatar: const Icon(Icons.auto_fix_high, size: 18),
|
||||
label: const Text('Assistant IA'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _canAddMaterial ? _openSelectionDialog : null,
|
||||
icon: Icon(Icons.add, color: _canAddMaterial ? Colors.white : Colors.grey),
|
||||
icon: Icon(Icons.add,
|
||||
color: _canAddMaterial ? Colors.white : Colors.grey),
|
||||
label: Text(
|
||||
'Ajouter',
|
||||
style: TextStyle(color: _canAddMaterial ? Colors.white : Colors.grey),
|
||||
style: TextStyle(
|
||||
color: _canAddMaterial ? Colors.white : Colors.grey),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _canAddMaterial ? AppColors.rouge : Colors.grey.shade300,
|
||||
backgroundColor: _canAddMaterial
|
||||
? AppColors.rouge
|
||||
: Colors.grey.shade300,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -512,7 +612,8 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -537,7 +638,8 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
eq.category.getIcon(size: 16, color: eq.category.color),
|
||||
eq.category
|
||||
.getIcon(size: 16, color: eq.category.color),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -562,7 +664,8 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEquipmentItem(EquipmentModel? equipment, EventEquipment eventEq) {
|
||||
Widget _buildEquipmentItem(
|
||||
EquipmentModel? equipment, EventEquipment eventEq) {
|
||||
if (equipment == null) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
@@ -585,17 +688,15 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
}
|
||||
|
||||
final isConsumable = equipment.category == EquipmentCategory.consumable ||
|
||||
equipment.category == EquipmentCategory.cable;
|
||||
equipment.category == EquipmentCategory.cable;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: equipment.category.color.withValues(alpha: 0.2),
|
||||
child: equipment.category.getIconForAvatar(
|
||||
size: 24,
|
||||
color: equipment.category.color
|
||||
),
|
||||
child: equipment.category
|
||||
.getIconForAvatar(size: 24, color: equipment.category.color),
|
||||
),
|
||||
title: Text(
|
||||
equipment.id,
|
||||
@@ -634,4 +735,3 @@ class _EventAssignedEquipmentSectionState extends State<EventAssignedEquipmentSe
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user