uto_manager/lib/presentation/screens/home_screen.dart

381 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:uto_manager/application/blocs/personnel_cubit.dart';
import 'package:uto_manager/application/blocs/unit_tree_cubit.dart';
import 'package:uto_manager/domain/entities/personnel_record.dart';
import 'package:uto_manager/domain/entities/position_slot.dart';
import 'package:uto_manager/presentation/widgets/personnel_list_tile.dart';
import 'package:uto_manager/presentation/widgets/position_slot_card.dart';
import 'package:uto_manager/presentation/widgets/uto_canvas_widget.dart';
/// The main three-pane home screen of the UTO Manager.
///
/// Layout:
/// - Left panel: Personnel roster (unassigned soldiers, draggable)
/// - Center: Interactive org chart canvas
/// - Right panel: Unit inspector (selected unit's position slots)
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: _buildAppBar(context),
body: _buildBody(context),
bottomNavigationBar: _buildStatusBar(context),
);
}
PreferredSizeWidget _buildAppBar(BuildContext context) {
return AppBar(
title: const Row(
children: [
Icon(Icons.military_tech, size: 20),
SizedBox(width: 8),
Text('UTO Manager', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(width: 8),
Text(
'U.S. Army 2026 Doctrine',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.normal),
),
],
),
backgroundColor: const Color(0xFF1B3A6B),
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.delete_sweep_outlined),
tooltip: 'Clear all units',
onPressed: () => _confirmClearAll(context),
),
const SizedBox(width: 8),
],
);
}
Widget _buildBody(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final isWide = constraints.maxWidth > 900;
if (isWide) {
return Row(
children: [
// Left panel: Personnel roster
SizedBox(
width: 260,
child: _buildPersonnelPanel(context),
),
const VerticalDivider(width: 1),
// Center: Canvas
Expanded(
child: _buildCanvas(context),
),
const VerticalDivider(width: 1),
// Right panel: Unit inspector
SizedBox(
width: 280,
child: _buildInspectorPanel(context),
),
],
);
}
// Narrow layout: canvas only, with drawer for panels
return _buildCanvas(context);
},
);
}
// ---------------------------------------------------------------------------
// Left Panel — Personnel Roster
// ---------------------------------------------------------------------------
Widget _buildPersonnelPanel(BuildContext context) {
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
color: theme.colorScheme.surfaceContainerHigh,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'PERSONNEL ROSTER',
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 6),
BlocBuilder<PersonnelCubit, PersonnelState>(
builder: (context, state) {
return TextField(
decoration: InputDecoration(
hintText: 'Search by name, MOS, rank…',
hintStyle: theme.textTheme.labelSmall,
prefixIcon: const Icon(Icons.search, size: 16),
isDense: true,
contentPadding: const EdgeInsets.symmetric(vertical: 8),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(6)),
),
style: theme.textTheme.bodySmall,
onChanged: (q) => context.read<PersonnelCubit>().setFilter(q),
);
},
),
],
),
),
Expanded(
child: BlocBuilder<PersonnelCubit, PersonnelState>(
builder: (context, state) {
if (state is PersonnelLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is PersonnelLoaded) {
final soldiers = state.filteredUnassigned;
if (soldiers.isEmpty) {
return Center(
child: Text(
'No unassigned soldiers',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
);
}
return ListView.builder(
itemCount: soldiers.length,
itemBuilder: (context, i) => PersonnelListTile(record: soldiers[i]),
);
}
return const SizedBox.shrink();
},
),
),
],
);
}
// ---------------------------------------------------------------------------
// Center — Canvas
// ---------------------------------------------------------------------------
Widget _buildCanvas(BuildContext context) {
return BlocBuilder<UnitTreeCubit, UnitTreeState>(
builder: (context, state) {
if (state is UnitTreeLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is UnitTreeLoaded) {
return UtoCanvasWidget(
roots: state.roots,
selectedUnitId: state.selectedUnitId,
onUnitSelected: (id) => context.read<UnitTreeCubit>().selectUnit(id),
onReparent: (unitId, newParentId) =>
context.read<UnitTreeCubit>().reparentUnit(unitId, newParentId),
);
}
if (state is UnitTreeError) {
return Center(child: Text('Error: ${state.message}'));
}
return const Center(child: CircularProgressIndicator());
},
);
}
// ---------------------------------------------------------------------------
// Right Panel — Unit Inspector
// ---------------------------------------------------------------------------
Widget _buildInspectorPanel(BuildContext context) {
final theme = Theme.of(context);
return BlocBuilder<UnitTreeCubit, UnitTreeState>(
builder: (context, treeState) {
if (treeState is! UnitTreeLoaded) {
return const SizedBox.shrink();
}
final selectedUnit = treeState.selectedUnit;
if (selectedUnit == null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Select a unit on the canvas to inspect its position slots.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
),
);
}
final slots = treeState.selectedUnitSlots;
final personnelById = {
for (final p in (context.read<PersonnelCubit>().state is PersonnelLoaded
? (context.read<PersonnelCubit>().state as PersonnelLoaded).allPersonnel
: <PersonnelRecord>[]))
p.soldierId: p,
};
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Inspector header
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
color: theme.colorScheme.surfaceContainerHigh,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'UNIT INSPECTOR',
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(
selectedUnit.name,
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
),
Text(
'${selectedUnit.echelon.displayName} • ${selectedUnit.branch.displayName}',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
),
// Slots list
Expanded(
child: slots.isEmpty
? Center(
child: Text(
'No position slots defined',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
)
: ListView.builder(
itemCount: slots.length,
itemBuilder: (context, i) {
final slot = slots[i];
final soldier = slot.assignedPersonnelId != null
? personnelById[slot.assignedPersonnelId]
: null;
return PositionSlotCard(
slot: slot,
assignedSoldier: soldier,
onUnassign: slot.isFilled ? () => _unassign(context, slot) : null,
onAcceptDrop: (soldier) => _assign(context, soldier, slot),
);
},
),
),
],
);
},
);
}
// ---------------------------------------------------------------------------
// Status Bar
// ---------------------------------------------------------------------------
Widget _buildStatusBar(BuildContext context) {
final theme = Theme.of(context);
return Container(
height: 28,
color: const Color(0xFF1B3A6B),
padding: const EdgeInsets.symmetric(horizontal: 12),
child: BlocBuilder<UnitTreeCubit, UnitTreeState>(
builder: (context, state) {
if (state is! UnitTreeLoaded) {
return const SizedBox.shrink();
}
final unitCount = state.units.length;
final slotCount = state.slots.length;
final filledCount = state.slots.where((s) => s.isFilled).length;
final fillPct = slotCount == 0 ? 0 : (filledCount * 100 ~/ slotCount);
return Row(
children: [
_statusChip(theme, Icons.account_tree, '$unitCount units'),
const SizedBox(width: 16),
_statusChip(theme, Icons.badge, '$filledCount / $slotCount slots ($fillPct%)'),
const Spacer(),
Text(
'Phase 2 — Canvas Engine',
style: theme.textTheme.labelSmall?.copyWith(color: Colors.white54, fontSize: 10),
),
],
);
},
),
);
}
Widget _statusChip(ThemeData theme, IconData icon, String label) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 12, color: Colors.white70),
const SizedBox(width: 4),
Text(
label,
style: theme.textTheme.labelSmall?.copyWith(color: Colors.white70, fontSize: 10),
),
],
);
}
// ---------------------------------------------------------------------------
// Actions
// ---------------------------------------------------------------------------
void _assign(BuildContext context, PersonnelRecord soldier, PositionSlot slot) {
context.read<PersonnelCubit>().assignSoldierToSlot(soldier, slot);
}
void _unassign(BuildContext context, PositionSlot slot) {
context.read<PersonnelCubit>().unassignFromSlot(slot);
}
void _confirmClearAll(BuildContext context) {
showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Clear All Units?'),
content: const Text(
'This will delete all units, position slots, and personnel assignments. '
'This action cannot be undone.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () {
Navigator.of(ctx).pop();
context.read<UnitTreeCubit>().clearAll();
},
child: const Text('Clear All'),
),
],
),
);
}
}