uto_manager/lib/presentation/widgets/unit_node_card.dart

165 lines
5.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:uto_manager/domain/entities/unit_node.dart';
import 'package:uto_manager/domain/services/unit_tree_service.dart';
/// Displays a single unit node as a card in the org chart canvas.
///
/// Shows the unit's echelon, branch, name, and fill rate indicator.
class UnitNodeCard extends StatelessWidget {
const UnitNodeCard({
super.key,
required this.treeNode,
required this.isSelected,
required this.onTap,
});
final UnitTreeNode treeNode;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final unit = treeNode.unit;
final fillRate = treeNode.fillRate;
final theme = Theme.of(context);
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
width: 160,
decoration: BoxDecoration(
color: isSelected
? theme.colorScheme.primaryContainer
: theme.colorScheme.surfaceContainerHighest,
border: Border.all(
color: isSelected ? theme.colorScheme.primary : _echelonColor(unit),
width: isSelected ? 2.5 : 1.5,
),
borderRadius: BorderRadius.circular(6),
boxShadow: isSelected
? [
BoxShadow(
color: theme.colorScheme.primary.withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header: echelon badge
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
decoration: BoxDecoration(
color: _echelonColor(unit).withValues(alpha: 0.85),
borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
unit.echelon.displayName.toUpperCase(),
style: theme.textTheme.labelSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
letterSpacing: 0.8,
),
),
Text(
unit.branch.displayName,
style: theme.textTheme.labelSmall?.copyWith(
color: Colors.white70,
fontSize: 9,
),
),
],
),
),
// Body: unit name
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Text(
unit.name,
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w600,
color: isSelected
? theme.colorScheme.onPrimaryContainer
: theme.colorScheme.onSurface,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
),
// Footer: fill rate bar
Padding(
padding: const EdgeInsets.fromLTRB(8, 0, 8, 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Fill',
style: theme.textTheme.labelSmall?.copyWith(fontSize: 9),
),
Text(
'${fillRate.toStringAsFixed(0)}%',
style: theme.textTheme.labelSmall?.copyWith(
fontSize: 9,
color: _fillRateColor(fillRate),
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 2),
ClipRRect(
borderRadius: BorderRadius.circular(2),
child: LinearProgressIndicator(
value: fillRate / 100.0,
minHeight: 4,
backgroundColor: theme.colorScheme.surfaceContainerLow,
valueColor: AlwaysStoppedAnimation<Color>(_fillRateColor(fillRate)),
),
),
],
),
),
],
),
),
);
}
Color _echelonColor(UnitNode unit) {
switch (unit.echelon.level) {
case 1:
return const Color(0xFF1B3A6B); // Brigade — Army blue
case 2:
return const Color(0xFF2E5FA3); // Battalion
case 3:
return const Color(0xFF3A7BD5); // Company
case 4:
return const Color(0xFF4A90D9); // Platoon
case 5:
return const Color(0xFF5BA3E0); // Section
case 6:
return const Color(0xFF6CB4E8); // Squad
default:
return const Color(0xFF7EC8F0); // Team
}
}
Color _fillRateColor(double rate) {
if (rate >= 90) return const Color(0xFF4CAF50); // Green
if (rate >= 70) return const Color(0xFFFF9800); // Amber
return const Color(0xFFF44336); // Red
}
}