refactor: Passage à la pagination côté serveur pour les équipements et containers
Cette mise à jour refactorise en profondeur le chargement des données pour les équipements et les containers, en remplaçant la récupération complète de la collection par un système de pagination côté serveur. Ce changement améliore considérablement les performances, réduit la consommation de mémoire et accélère le temps de chargement initial, en particulier pour les larges inventaires.
**Changements Backend (Cloud Functions) :**
- **Nouveaux Endpoints Paginés :**
- `getEquipmentsPaginated` et `getContainersPaginated` ont été créés pour remplacer les anciens `getEquipments` et `getContainers`.
- Ces nouvelles fonctions supportent le filtrage (catégorie, statut, type), la recherche textuelle et le tri directement côté serveur, limitant la quantité de données transférées.
- La pagination est gérée via les paramètres `limit` et `startAfter`, assurant un chargement par lots efficace.
- **Optimisation de `getContainersPaginated` :**
- Peuple désormais les containers avec leurs équipements enfants via une requête `in` optimisée, réduisant le nombre de lectures Firestore.
- **Suppression des Anciens Endpoints :** Les fonctions `getEquipments` et `getContainers`, qui chargeaient l'intégralité des collections, ont été supprimées.
- **Nouveau Script de Migration :** Ajout d'un script (`migrate_equipment_ids.js`) pour s'assurer que chaque équipement dans Firestore possède un champ `id` correspondant à son ID de document, ce qui est crucial pour le tri et la pagination.
**Changements Frontend (Flutter) :**
- **`EquipmentProvider` et `ContainerProvider` :**
- La logique de chargement a été entièrement réécrite pour utiliser les nouveaux endpoints paginés.
- Introduction d'un mode `usePagination` pour basculer entre le chargement paginé (pour les pages de gestion) et le chargement complet (pour les dialogues de sélection).
- Implémentation de `loadFirstPage` et `loadNextPage` pour gérer le scroll infini.
- Ajout d'un "debouncing" sur la recherche pour éviter les appels API excessifs lors de la saisie.
- **Pages de Gestion (`EquipmentManagementPage`, `ContainerManagementPage`) :**
- Utilisent désormais un `ScrollController` pour déclencher `loadNextPage` et implémenter un scroll infini.
- Le chargement initial et les rechargements (après filtre) sont beaucoup plus rapides.
- Refonte de l'UI avec un nouveau widget `SearchActionsBar` pour uniformiser la barre de recherche et les actions.
- **Dialogue de Sélection d'Équipement (`EquipmentSelectionDialog`) :**
- Passe également à un système de lazy loading basé sur des `ChoiceChip` pour afficher soit les équipements, soit les containers.
- Charge les pages de manière asynchrone au fur et à mesure du scroll, améliorant drastiquement la réactivité du dialogue.
- La logique de chargement des données a été fiabilisée pour attendre la disponibilité des données avant l'affichage.
- **Optimisations diverses :**
- Les sections qui listent les événements associés à un équipement (`EquipmentCurrentEventsSection`, etc.) chargent désormais uniquement les containers pertinents via `getContainersByIds` au lieu de toute la collection.
- Le calcul du statut d'un équipement (`EquipmentStatusBadge`) est maintenant synchrone, simplifiant le code et évitant des `FutureBuilder`.
**Correction mineure :**
- **Nom de l'application :** Le nom de l'application a été mis à jour de "EM2 ERP" à "EM2 Hub" dans `main.dart` et dans les exports ICS.
This commit is contained in:
@@ -49,19 +49,28 @@ class _EquipmentAssociatedEventsSectionState
|
||||
|
||||
final events = <EventModel>[];
|
||||
|
||||
// Récupérer toutes les boîtes pour vérifier leur contenu via l'API
|
||||
final containersData = await _dataService.getContainers();
|
||||
// Collecter tous les IDs de containers utilisés dans les événements
|
||||
final allContainerIds = <String>{};
|
||||
for (var eventData in eventsData) {
|
||||
final assignedContainers = eventData['assignedContainers'] as List<dynamic>? ?? [];
|
||||
allContainerIds.addAll(assignedContainers.map((id) => id.toString()));
|
||||
}
|
||||
|
||||
// Charger SEULEMENT les containers utilisés (au lieu de TOUS les charger)
|
||||
final containersWithEquipment = <String>[];
|
||||
for (var containerData in containersData) {
|
||||
try {
|
||||
final equipmentIds = List<String>.from(containerData['equipmentIds'] ?? []);
|
||||
if (allContainerIds.isNotEmpty) {
|
||||
final containersData = await _dataService.getContainersByIds(allContainerIds.toList());
|
||||
|
||||
if (equipmentIds.contains(widget.equipment.id)) {
|
||||
containersWithEquipment.add(containerData['id'] as String);
|
||||
for (var containerData in containersData) {
|
||||
try {
|
||||
final equipmentIds = List<String>.from(containerData['equipmentIds'] ?? []);
|
||||
|
||||
if (equipmentIds.contains(widget.equipment.id)) {
|
||||
containersWithEquipment.add(containerData['id'] as String);
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLog.error('[EquipmentAssociatedEventsSection] Error parsing container ${containerData['id']}', e);
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLog.error('[EquipmentAssociatedEventsSection] Error parsing container ${containerData['id']}', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,19 +43,28 @@ class _EquipmentCurrentEventsSectionState
|
||||
|
||||
final events = <EventModel>[];
|
||||
|
||||
// Récupérer toutes les boîtes pour vérifier leur contenu via l'API
|
||||
final containersData = await _dataService.getContainers();
|
||||
// Collecter tous les IDs de containers utilisés dans les événements
|
||||
final allContainerIds = <String>{};
|
||||
for (var eventData in eventsData) {
|
||||
final assignedContainers = eventData['assignedContainers'] as List<dynamic>? ?? [];
|
||||
allContainerIds.addAll(assignedContainers.map((id) => id.toString()));
|
||||
}
|
||||
|
||||
// Charger SEULEMENT les containers utilisés (au lieu de TOUS les charger)
|
||||
final containersWithEquipment = <String>[];
|
||||
for (var containerData in containersData) {
|
||||
try {
|
||||
final equipmentIds = List<String>.from(containerData['equipmentIds'] ?? []);
|
||||
if (allContainerIds.isNotEmpty) {
|
||||
final containersData = await _dataService.getContainersByIds(allContainerIds.toList());
|
||||
|
||||
if (equipmentIds.contains(widget.equipment.id)) {
|
||||
containersWithEquipment.add(containerData['id'] as String);
|
||||
for (var containerData in containersData) {
|
||||
try {
|
||||
final equipmentIds = List<String>.from(containerData['equipmentIds'] ?? []);
|
||||
|
||||
if (equipmentIds.contains(widget.equipment.id)) {
|
||||
containersWithEquipment.add(containerData['id'] as String);
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLog.error('[EquipmentCurrentEventsSection] Error parsing container ${containerData['id']}', e);
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLog.error('[EquipmentCurrentEventsSection] Error parsing container ${containerData['id']}', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,35 +16,25 @@ class EquipmentStatusBadge extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = Provider.of<EquipmentProvider>(context, listen: false);
|
||||
// Calculer le statut réel (synchrone maintenant)
|
||||
final status = provider.calculateRealStatus(equipment);
|
||||
|
||||
// Logs désactivés en production
|
||||
|
||||
return FutureBuilder<EquipmentStatus>(
|
||||
// On calcule le statut réel de manière asynchrone
|
||||
future: provider.calculateRealStatus(equipment),
|
||||
// En attendant, on affiche le statut stocké
|
||||
initialData: equipment.status,
|
||||
builder: (context, snapshot) {
|
||||
// Utiliser le statut calculé s'il est disponible, sinon le statut stocké
|
||||
final status = snapshot.data ?? equipment.status;
|
||||
// Logs désactivés en production
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: status.color.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: status.color),
|
||||
),
|
||||
child: Text(
|
||||
status.label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: status.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: status.color.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: status.color),
|
||||
),
|
||||
child: Text(
|
||||
status.label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: status.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,435 +0,0 @@
|
||||
import 'package:em2rp/utils/debug_log.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/container_model.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
|
||||
/// Widget pour sélectionner les boîtes parentes d'un équipement
|
||||
class ParentBoxesSelector extends StatefulWidget {
|
||||
final List<ContainerModel> availableBoxes;
|
||||
final List<String> selectedBoxIds;
|
||||
final Function(List<String>) onSelectionChanged;
|
||||
|
||||
const ParentBoxesSelector({
|
||||
super.key,
|
||||
required this.availableBoxes,
|
||||
required this.selectedBoxIds,
|
||||
required this.onSelectionChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ParentBoxesSelector> createState() => _ParentBoxesSelectorState();
|
||||
}
|
||||
|
||||
class _ParentBoxesSelectorState extends State<ParentBoxesSelector> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String _searchQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ParentBoxesSelector oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<ContainerModel> get _filteredBoxes {
|
||||
if (_searchQuery.isEmpty) {
|
||||
return widget.availableBoxes;
|
||||
}
|
||||
|
||||
final query = _searchQuery.toLowerCase();
|
||||
return widget.availableBoxes.where((box) {
|
||||
return box.name.toLowerCase().contains(query) ||
|
||||
box.id.toLowerCase().contains(query) ||
|
||||
box.type.label.toLowerCase().contains(query);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
void _toggleSelection(String boxId) {
|
||||
final newSelection = List<String>.from(widget.selectedBoxIds);
|
||||
if (newSelection.contains(boxId)) {
|
||||
newSelection.remove(boxId);
|
||||
} else {
|
||||
newSelection.add(boxId);
|
||||
}
|
||||
widget.onSelectionChanged(newSelection);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.availableBoxes.isEmpty && widget.selectedBoxIds.isEmpty) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Aucune boîte disponible',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final filteredBoxes = _filteredBoxes;
|
||||
final selectedCount = widget.selectedBoxIds.length;
|
||||
|
||||
// Vérifier s'il y a des boîtes sélectionnées qui ne sont pas dans la liste
|
||||
final missingBoxIds = widget.selectedBoxIds
|
||||
.where((id) => !widget.availableBoxes.any((box) => box.id == id))
|
||||
.toList();
|
||||
|
||||
return Card(
|
||||
elevation: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// En-tête avec titre et compteur
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.inventory_2, color: AppColors.rouge, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Boîtes parentes',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (selectedCount > 0)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.rouge.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppColors.rouge.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
'$selectedCount sélectionné${selectedCount > 1 ? 's' : ''}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.rouge,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
|
||||
// Message d'avertissement si des boîtes sélectionnées sont manquantes
|
||||
if (missingBoxIds.isNotEmpty)
|
||||
Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.orange.shade300),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.warning_amber, color: Colors.orange.shade700),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Boîtes introuvables',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange.shade900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Les boîtes suivantes sont sélectionnées mais n\'existent plus : ${missingBoxIds.join(", ")}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.orange.shade800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
// Retirer les boîtes manquantes de la sélection
|
||||
final newSelection = widget.selectedBoxIds
|
||||
.where((id) => !missingBoxIds.contains(id))
|
||||
.toList();
|
||||
widget.onSelectionChanged(newSelection);
|
||||
},
|
||||
tooltip: 'Retirer ces boîtes',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Barre de recherche
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_searchQuery = value;
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher par nom, ID ou type...',
|
||||
prefixIcon: const Icon(Icons.search, color: AppColors.rouge),
|
||||
suffixIcon: _searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_searchController.clear();
|
||||
_searchQuery = '';
|
||||
});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: AppColors.rouge, width: 2),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.grey.shade50,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Message si aucun résultat
|
||||
if (filteredBoxes.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(Icons.search_off, size: 48, color: Colors.grey.shade400),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Aucune boîte trouvée',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey.shade600,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Essayez une autre recherche',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
// Liste des boîtes
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
itemCount: filteredBoxes.length,
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final box = filteredBoxes[index];
|
||||
final isSelected = widget.selectedBoxIds.contains(box.id);
|
||||
if (index == 0) {
|
||||
DebugLog.info('[ParentBoxesSelector] Building item $index');
|
||||
DebugLog.info('[ParentBoxesSelector] Box ID: ${box.id}');
|
||||
DebugLog.info('[ParentBoxesSelector] Selected IDs: ${widget.selectedBoxIds}');
|
||||
DebugLog.info('[ParentBoxesSelector] Is selected: $isSelected');
|
||||
}
|
||||
return _buildBoxCard(box, isSelected);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBoxCard(ContainerModel box, bool isSelected) {
|
||||
return Card(
|
||||
elevation: isSelected ? 3 : 1,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(
|
||||
color: isSelected ? AppColors.rouge : Colors.grey.shade300,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () => _toggleSelection(box.id),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
// Checkbox
|
||||
Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (value) => _toggleSelection(box.id),
|
||||
activeColor: AppColors.rouge,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Icône du type de container
|
||||
CircleAvatar(
|
||||
backgroundColor: isSelected
|
||||
? AppColors.rouge.withValues(alpha: 0.15)
|
||||
: Colors.grey.shade200,
|
||||
radius: 24,
|
||||
child: box.type.getIconForAvatar(
|
||||
size: 24,
|
||||
color: isSelected ? AppColors.rouge : Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Informations de la boîte
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
box.name,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 15,
|
||||
color: isSelected ? AppColors.rouge : Colors.black87,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
box.type.label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// Badges
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
_buildInfoChip(
|
||||
icon: Icons.inventory,
|
||||
label: '${box.itemCount} équip.',
|
||||
color: Colors.blue,
|
||||
),
|
||||
if (box.weight != null)
|
||||
_buildInfoChip(
|
||||
icon: Icons.scale,
|
||||
label: '${box.weight!.toStringAsFixed(1)} kg',
|
||||
color: Colors.orange,
|
||||
),
|
||||
_buildInfoChip(
|
||||
icon: Icons.tag,
|
||||
label: box.id,
|
||||
color: Colors.grey,
|
||||
isCompact: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Indicateur de sélection
|
||||
if (isSelected)
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: AppColors.rouge,
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoChip({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
bool isCompact = false,
|
||||
}) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: isCompact ? 6 : 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: color.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: isCompact ? 10 : 12,
|
||||
color: color.withValues(alpha: 0.8),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: isCompact ? 9 : 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color.withValues(alpha: 0.9),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user