uto_manager/lib/domain/services/assignment_service.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;
}