uto_manager/lib/application/blocs/personnel_cubit.dart

226 lines
6.9 KiB
Dart

import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:uto_manager/domain/entities/personnel_record.dart';
import 'package:uto_manager/domain/entities/position_slot.dart';
import 'package:uto_manager/domain/repositories/personnel_repository.dart';
import 'package:uto_manager/domain/services/assignment_service.dart';
import 'package:uto_manager/infrastructure/repositories/drift_position_slot_repository.dart';
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
/// State for the personnel cubit.
sealed class PersonnelState extends Equatable {
const PersonnelState();
}
class PersonnelInitial extends PersonnelState {
const PersonnelInitial();
@override
List<Object?> get props => [];
}
class PersonnelLoading extends PersonnelState {
const PersonnelLoading();
@override
List<Object?> get props => [];
}
class PersonnelLoaded extends PersonnelState {
const PersonnelLoaded({
required this.allPersonnel,
required this.unassigned,
this.filterQuery = '',
});
/// All personnel records.
final List<PersonnelRecord> allPersonnel;
/// Unassigned soldiers (available for assignment).
final List<PersonnelRecord> unassigned;
/// Current search/filter query string.
final String filterQuery;
/// Returns [unassigned] filtered by [filterQuery].
List<PersonnelRecord> get filteredUnassigned {
if (filterQuery.isEmpty) return unassigned;
final q = filterQuery.toLowerCase();
return unassigned.where((p) {
return p.displayLabel.toLowerCase().contains(q) ||
p.primaryMos.code.toLowerCase().contains(q) ||
p.payGrade.payGrade.toLowerCase().contains(q);
}).toList();
}
PersonnelLoaded copyWith({
List<PersonnelRecord>? allPersonnel,
List<PersonnelRecord>? unassigned,
String? filterQuery,
}) {
return PersonnelLoaded(
allPersonnel: allPersonnel ?? this.allPersonnel,
unassigned: unassigned ?? this.unassigned,
filterQuery: filterQuery ?? this.filterQuery,
);
}
@override
List<Object?> get props => [allPersonnel, unassigned, filterQuery];
}
class PersonnelError extends PersonnelState {
const PersonnelError(this.message);
final String message;
@override
List<Object?> get props => [message];
}
// ---------------------------------------------------------------------------
// Cubit
// ---------------------------------------------------------------------------
/// Manages personnel state, including loading, filtering, and assignment.
class PersonnelCubit extends Cubit<PersonnelState> {
PersonnelCubit({
required this.personnelRepository,
required this.slotRepository,
required this.assignmentService,
}) : super(const PersonnelInitial());
final PersonnelRepository personnelRepository;
final DriftPositionSlotRepository slotRepository;
final AssignmentService assignmentService;
StreamSubscription<List<PersonnelRecord>>? _allSubscription;
StreamSubscription<List<PersonnelRecord>>? _unassignedSubscription;
List<PersonnelRecord> _all = [];
List<PersonnelRecord> _unassigned = [];
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
/// Starts watching the database for personnel changes.
void startWatching() {
emit(const PersonnelLoading());
_allSubscription = personnelRepository.watchAll().listen(
(all) {
_all = all;
_rebuild();
},
onError: (Object e) => emit(PersonnelError(e.toString())),
);
_unassignedSubscription = personnelRepository.watchUnassigned().listen(
(unassigned) {
_unassigned = unassigned;
_rebuild();
},
onError: (Object e) => emit(PersonnelError(e.toString())),
);
}
void _rebuild() {
final currentFilter = state is PersonnelLoaded ? (state as PersonnelLoaded).filterQuery : '';
emit(PersonnelLoaded(allPersonnel: _all, unassigned: _unassigned, filterQuery: currentFilter));
}
// ---------------------------------------------------------------------------
// Filtering
// ---------------------------------------------------------------------------
/// Updates the search filter query.
void setFilter(String query) {
if (state is PersonnelLoaded) {
emit((state as PersonnelLoaded).copyWith(filterQuery: query));
}
}
// ---------------------------------------------------------------------------
// Assignment
// ---------------------------------------------------------------------------
/// Assigns [soldier] to [slot] after validation.
///
/// Returns true on success, false if validation failed.
Future<bool> assignSoldierToSlot(PersonnelRecord soldier, PositionSlot slot) async {
final outcome = assignmentService.assign(slot, soldier);
if (!outcome.isSuccess) return false;
try {
await personnelRepository.assignToSlot(soldier.soldierId, slot.slotId);
await slotRepository.assignPersonnel(slot.slotId, soldier.soldierId);
return true;
} catch (e) {
emit(PersonnelError(e.toString()));
return false;
}
}
/// Unassigns the soldier from [slot].
Future<void> unassignFromSlot(PositionSlot slot) async {
if (slot.isVacant) return;
try {
await personnelRepository.unassignFromSlot(slot.assignedPersonnelId!);
await slotRepository.clearAssignment(slot.slotId);
} catch (e) {
emit(PersonnelError(e.toString()));
}
}
/// Adds a new [record] to the personnel roster.
Future<void> addPersonnel(PersonnelRecord record) async {
try {
await personnelRepository.insert(record);
} catch (e) {
emit(PersonnelError(e.toString()));
}
}
/// Updates an existing [record] in the personnel roster.
Future<void> updatePersonnel(PersonnelRecord record) async {
try {
await personnelRepository.update(record);
} catch (e) {
emit(PersonnelError(e.toString()));
}
}
/// Deletes the soldier with [soldierId] from the roster.
///
/// If the soldier is currently assigned to a slot, the slot assignment
/// is cleared first.
Future<void> deletePersonnel(String soldierId) async {
try {
// If assigned, clear the slot assignment first
if (state is PersonnelLoaded) {
final loaded = state as PersonnelLoaded;
final record = loaded.allPersonnel.where((p) => p.soldierId == soldierId).firstOrNull;
if (record != null && record.isAssigned) {
await slotRepository.clearAssignment(record.assignedSlotId!);
}
}
await personnelRepository.delete(soldierId);
} catch (e) {
emit(PersonnelError(e.toString()));
}
}
@override
Future<void> close() {
_allSubscription?.cancel();
_unassignedSubscription?.cancel();
return super.close();
}
}