197 lines
5.9 KiB
Dart
197 lines
5.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.name.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()));
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> close() {
|
|
_allSubscription?.cancel();
|
|
_unassignedSubscription?.cancel();
|
|
return super.close();
|
|
}
|
|
}
|