164 lines
5.3 KiB
Dart
164 lines
5.3 KiB
Dart
import 'package:uto_manager/domain/entities/personnel_record.dart';
|
|
import 'package:uto_manager/domain/entities/position_slot.dart';
|
|
import 'package:uto_manager/domain/services/unit_tree_service.dart';
|
|
|
|
/// Domain service for assigning and unassigning soldiers to position slots.
|
|
///
|
|
/// Encapsulates all business rules around personnel assignment, including
|
|
/// MOS compatibility checks and rank validation. This service is pure domain
|
|
/// logic with no infrastructure dependencies.
|
|
class AssignmentService {
|
|
const AssignmentService({required this.treeService});
|
|
|
|
/// The [UnitTreeService] used for validation.
|
|
final UnitTreeService treeService;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Assignment Operations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Attempts to assign [soldier] to [slot].
|
|
///
|
|
/// Returns a [ValidationResult] indicating success or the reason for failure.
|
|
/// On success, returns updated copies of both the slot and the soldier record.
|
|
AssignmentOutcome assign(PositionSlot slot, PersonnelRecord soldier) {
|
|
final validation = treeService.validateAssignment(slot, soldier);
|
|
if (!validation.isValid) {
|
|
return AssignmentOutcome.failure(validation.reason!);
|
|
}
|
|
|
|
final mosMismatch = soldier.primaryMos != slot.dutyMos;
|
|
final rankMismatch = !soldier.payGrade.meetsMinimum(slot.authorizedRank);
|
|
|
|
final updatedSlot = slot.copyWith(assignedPersonnelId: soldier.soldierId);
|
|
final updatedSoldier = soldier.copyWith(assignedSlotId: slot.slotId);
|
|
|
|
return AssignmentOutcome.success(
|
|
updatedSlot: updatedSlot,
|
|
updatedSoldier: updatedSoldier,
|
|
hasMosMismatch: mosMismatch,
|
|
hasRankMismatch: rankMismatch,
|
|
);
|
|
}
|
|
|
|
/// Unassigns the soldier currently in [slot].
|
|
///
|
|
/// Returns updated copies of the slot and the soldier record, or null
|
|
/// for the soldier if [soldier] is null (slot was already vacant).
|
|
UnassignmentOutcome unassign(PositionSlot slot, PersonnelRecord? soldier) {
|
|
if (slot.isVacant) {
|
|
return UnassignmentOutcome(
|
|
updatedSlot: slot,
|
|
updatedSoldier: null,
|
|
);
|
|
}
|
|
|
|
final updatedSlot = slot.copyWith(clearAssignment: true);
|
|
final updatedSoldier = soldier?.copyWith(clearAssignment: true);
|
|
|
|
return UnassignmentOutcome(
|
|
updatedSlot: updatedSlot,
|
|
updatedSoldier: updatedSoldier,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Readiness Calculations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Returns the aggregate readiness color for a list of [personnel].
|
|
///
|
|
/// - All FMC → [ReadinessColor.green]
|
|
/// - Any NMC → [ReadinessColor.red]
|
|
/// - Otherwise → [ReadinessColor.amber]
|
|
ReadinessColor aggregateReadiness(List<PersonnelRecord> personnel) {
|
|
if (personnel.isEmpty) return ReadinessColor.grey;
|
|
final hasNmc = personnel.any((p) => p.readinessState == ReadinessState.notMissionCapable);
|
|
if (hasNmc) return ReadinessColor.red;
|
|
final allFmc = personnel.every((p) => p.readinessState == ReadinessState.fullyMissionCapable);
|
|
if (allFmc) return ReadinessColor.green;
|
|
return ReadinessColor.amber;
|
|
}
|
|
}
|
|
|
|
/// Color coding for readiness display.
|
|
enum ReadinessColor {
|
|
/// All soldiers fully mission capable.
|
|
green,
|
|
|
|
/// Mixed readiness states.
|
|
amber,
|
|
|
|
/// One or more soldiers not mission capable.
|
|
red,
|
|
|
|
/// No personnel data available.
|
|
grey,
|
|
}
|
|
|
|
/// The outcome of an assignment attempt.
|
|
class AssignmentOutcome {
|
|
const AssignmentOutcome._({
|
|
required this.isSuccess,
|
|
this.updatedSlot,
|
|
this.updatedSoldier,
|
|
this.failureReason,
|
|
this.hasMosMismatch = false,
|
|
this.hasRankMismatch = false,
|
|
});
|
|
|
|
/// Creates a successful assignment outcome.
|
|
factory AssignmentOutcome.success({
|
|
required PositionSlot updatedSlot,
|
|
required PersonnelRecord updatedSoldier,
|
|
bool hasMosMismatch = false,
|
|
bool hasRankMismatch = false,
|
|
}) {
|
|
return AssignmentOutcome._(
|
|
isSuccess: true,
|
|
updatedSlot: updatedSlot,
|
|
updatedSoldier: updatedSoldier,
|
|
hasMosMismatch: hasMosMismatch,
|
|
hasRankMismatch: hasRankMismatch,
|
|
);
|
|
}
|
|
|
|
/// Creates a failed assignment outcome with a [reason].
|
|
factory AssignmentOutcome.failure(String reason) {
|
|
return AssignmentOutcome._(isSuccess: false, failureReason: reason);
|
|
}
|
|
|
|
/// Whether the assignment succeeded.
|
|
final bool isSuccess;
|
|
|
|
/// The updated slot (non-null on success).
|
|
final PositionSlot? updatedSlot;
|
|
|
|
/// The updated soldier record (non-null on success).
|
|
final PersonnelRecord? updatedSoldier;
|
|
|
|
/// The reason for failure (non-null on failure).
|
|
final String? failureReason;
|
|
|
|
/// Whether the assigned soldier's MOS does not match the slot's duty MOS.
|
|
final bool hasMosMismatch;
|
|
|
|
/// Whether the assigned soldier's rank does not meet the slot's minimum.
|
|
final bool hasRankMismatch;
|
|
}
|
|
|
|
/// The outcome of an unassignment operation.
|
|
class UnassignmentOutcome {
|
|
const UnassignmentOutcome({
|
|
required this.updatedSlot,
|
|
required this.updatedSoldier,
|
|
});
|
|
|
|
/// The updated slot with the assignment cleared.
|
|
final PositionSlot updatedSlot;
|
|
|
|
/// The updated soldier record with the assignment cleared, or null if
|
|
/// no soldier was assigned.
|
|
final PersonnelRecord? updatedSoldier;
|
|
}
|