Implementation
Future<List<BudgetItem>> aggregate(ElectricalNode root) async {
final Map<String, double> cableQuantities = {};
final Map<String, int> protectionQuantities = {};
final Map<String, String> protectionNames = {};
final Map<String, double> protectionRatings = {};
int enclosureCount = 0;
int enclosureModules = 0;
// Helper to traverse tree
void traverse(ElectricalNode node) {
node.map(
source: (_) {},
panel: (n) {
enclosureCount++;
for (var child in n.children) {
child.maybeMap(
protection: (p) => enclosureModules += p.poles,
orElse: () {},
);
}
_processCable(n.inputCable, true, cableQuantities);
},
protection: (n) {
String key;
String name;
if (n.catalogData != null) {
key = n.catalogData!.componentId;
name = "Ref: $key";
} else {
// Generic
String typeShort = _getShortType(n.protectionType);
String curve = n.curve;
int amps = n.ratingAmps.toInt();
key = 'GEN-$typeShort-$amps$curve';
name = _buildGenericName(n);
}
protectionQuantities.update(key, (val) => val + 1, ifAbsent: () => 1);
protectionRatings[key] = n.ratingAmps;
if (!protectionNames.containsKey(key)) {
protectionNames[key] = name;
}
},
load: (n) {
_processCable(n.inputCable, n.isThreePhase, cableQuantities);
},
);
// Recursive
node.maybeMap(
source: (n) => n.children.forEach(traverse),
panel: (n) => n.children.forEach(traverse),
protection: (n) => n.children.forEach(traverse),
orElse: () {},
);
}
traverse(root);
List<BudgetItem> items = [];
// 0. Pre-fetch library cables for smart matching
List<ComponentTemplate> libraryCables = [];
if (componentRepository != null) {
final result = await componentRepository!.getByType(ComponentType.cable);
result.fold((l) => null, (r) => libraryCables = r);
}
// 1. Enclosures (First)
if (enclosureCount > 0) {
String sizeDesc =
"Est. ${enclosureModules + (enclosureModules * 0.2).toInt()} módulos";
items.add(BudgetItem(
id: 'ENCLOSURE-STD',
name: 'Cuadro Eléctrico / Envolvente',
description: '$enclosureCount unidades ($sizeDesc)',
quantity: enclosureCount.toDouble(),
unitPrice: 150.0,
category: BudgetCategory.enclosure,
));
}
// 2. Protections (Second, Sorted by Amperage)
var protKeys = protectionQuantities.keys.toList();
protKeys.sort((a, b) {
double ratingA = protectionRatings[a] ?? 0;
double ratingB = protectionRatings[b] ?? 0;
return ratingA.compareTo(ratingB);
});
// Fetch all protection details in parallel for better performance
final catalogKeys = protKeys.where((k) => !k.startsWith('GEN-')).toList();
final repository = componentRepository; // Capture repository reference for async callbacks
final protectionDetailsFutures = catalogKeys.map((key) async {
if (repository == null) return null;
try {
final result = await repository.getById(key);
return result.fold(
(l) => null,
(comp) => MapEntry(key, comp),
);
} catch (_) {
return null;
}
});
final protectionDetailsResults = await Future.wait(protectionDetailsFutures);
final protectionDetailsMap = Map.fromEntries(
protectionDetailsResults.whereType<MapEntry<String, ComponentTemplate>>(),
);
for (var key in protKeys) {
final count = protectionQuantities[key]!;
var name = protectionNames[key] ?? key;
double price = 0.0;
// Lookup real name if it's a catalog ID
if (!key.startsWith('GEN-') && protectionDetailsMap.containsKey(key)) {
final comp = protectionDetailsMap[key];
if (comp != null) {
final manu = comp.manufacturer ?? "";
final model = comp.name;
name = "$manu $model".trim();
// Capture price from library
comp.maybeMap(
protection: (p) => price = p.price ?? 0.0,
orElse: () {},
);
}
}
items.add(BudgetItem(
id: key,
name: name,
description: '$count unidades',
quantity: count.toDouble(),
unitPrice: price,
category: BudgetCategory.protection,
));
}
// 3. Cables (Third, Sorted by Section)
var cableKeys = cableQuantities.keys.toList();
cableKeys.sort((a, b) {
try {
double secA = double.parse(a.split('-')[2]);
double secB = double.parse(b.split('-')[2]);
return secA.compareTo(secB);
} catch (e) {
return 0;
}
});
for (var key in cableKeys) {
final length = cableQuantities[key]!;
final parts = key.split('-');
String materialCode = parts[1];
String sectionStr = parts[2];
String polesNum = parts[3];
String wiresStr = "${polesNum}G";
// 3 -> 3G (F+N+T), 5 -> 5G (3F+N+T)
String matName = materialCode == 'CU' ? 'Manguera' : 'Manguera Al';
String name = "$matName $wiresStr$sectionStr mm²";
double price = 0.0;
try {
final targetMat = materialCode == 'CU'
? CableMaterial.copper
: CableMaterial.aluminum;
final targetSec = double.tryParse(sectionStr) ?? 0.0;
final match = libraryCables.firstWhere(
(c) => c.maybeMap(
cable: (cab) =>
cab.material == targetMat && cab.section == targetSec,
orElse: () => false,
),
orElse: () => const ComponentTemplate.cable(
id: '',
name: '',
section: 0,
material: CableMaterial.copper,
insulationType: '',
maxOperatingTemp: 0),
);
if (match.id.isNotEmpty) {
match.mapOrNull(cable: (c) {
name = "${c.name} ($wiresStr)";
price = c.price ?? 0.0;
});
}
} catch (e) {
// Ignore errors during sorting or lookup
}
name = _cleanName(name);
items.add(BudgetItem(
id: key,
name: name,
description: 'Total: ${length.toStringAsFixed(1)} m',
quantity: length,
unitPrice: price,
category: BudgetCategory.cable,
));
}
return items;
}