131 lines
4.5 KiB
Dart
131 lines
4.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:uto_manager/domain/entities/personnel_record.dart';
|
|
import 'package:uto_manager/domain/entities/position_slot.dart';
|
|
|
|
/// Displays a single position slot as a card in the inspector panel.
|
|
///
|
|
/// Shows the slot's position title, MOS, rank, and assigned soldier (or VACANT).
|
|
/// Supports drag-and-drop target for soldier assignment.
|
|
class PositionSlotCard extends StatelessWidget {
|
|
const PositionSlotCard({
|
|
required this.slot,
|
|
super.key,
|
|
this.assignedSoldier,
|
|
this.onUnassign,
|
|
this.onAcceptDrop,
|
|
});
|
|
|
|
final PositionSlot slot;
|
|
final PersonnelRecord? assignedSoldier;
|
|
final VoidCallback? onUnassign;
|
|
final void Function(PersonnelRecord soldier)? onAcceptDrop;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return DragTarget<PersonnelRecord>(
|
|
onWillAcceptWithDetails: (details) => slot.isVacant,
|
|
onAcceptWithDetails: (details) => onAcceptDrop?.call(details.data),
|
|
builder: (context, candidateData, rejectedData) {
|
|
final isDragOver = candidateData.isNotEmpty;
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 120),
|
|
margin: const EdgeInsets.symmetric(vertical: 3, horizontal: 8),
|
|
decoration: BoxDecoration(
|
|
color: isDragOver
|
|
? theme.colorScheme.primaryContainer.withValues(alpha: 0.6)
|
|
: slot.isFilled
|
|
? theme.colorScheme.surfaceContainerHighest
|
|
: theme.colorScheme.surfaceContainerLow,
|
|
border: Border.all(
|
|
color: isDragOver
|
|
? theme.colorScheme.primary
|
|
: slot.isKeyLeader
|
|
? const Color(0xFFFFD700) // Gold for key leaders
|
|
: theme.colorScheme.outlineVariant,
|
|
width: isDragOver ? 2 : 1,
|
|
),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: ListTile(
|
|
dense: true,
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 2),
|
|
leading: _buildMosBadge(theme),
|
|
title: Text(
|
|
slot.positionTitle,
|
|
style: theme.textTheme.bodySmall?.copyWith(
|
|
fontWeight: slot.isKeyLeader ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
),
|
|
subtitle: Text(
|
|
slot.isFilled ? (assignedSoldier?.name ?? 'Assigned') : 'VACANT',
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
color: slot.isFilled
|
|
? _readinessColor(assignedSoldier?.readinessState)
|
|
: theme.colorScheme.error,
|
|
fontWeight: slot.isVacant ? FontWeight.bold : FontWeight.normal,
|
|
),
|
|
),
|
|
trailing: slot.isFilled && onUnassign != null
|
|
? IconButton(
|
|
icon: const Icon(Icons.person_remove, size: 16),
|
|
onPressed: onUnassign,
|
|
tooltip: 'Unassign soldier',
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
|
|
)
|
|
: null,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildMosBadge(ThemeData theme) {
|
|
return Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.primary.withValues(alpha: 0.15),
|
|
borderRadius: BorderRadius.circular(4),
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
slot.dutyMos.code,
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 10,
|
|
color: theme.colorScheme.primary,
|
|
),
|
|
),
|
|
Text(
|
|
slot.authorizedRank.payGrade,
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
fontSize: 9,
|
|
color: theme.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Color _readinessColor(ReadinessState? state) {
|
|
switch (state) {
|
|
case ReadinessState.fullyMissionCapable:
|
|
return const Color(0xFF4CAF50);
|
|
case ReadinessState.partiallyMissionCapable:
|
|
return const Color(0xFFFF9800);
|
|
case ReadinessState.notMissionCapable:
|
|
return const Color(0xFFF44336);
|
|
case ReadinessState.unknown:
|
|
return Colors.grey;
|
|
case null:
|
|
return Colors.grey;
|
|
}
|
|
}
|
|
}
|