196 lines
6.7 KiB
Dart
196 lines
6.7 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:uto_manager/domain/entities/authorized_rank.dart';
|
|
import 'package:uto_manager/domain/entities/doctrinal_branch.dart';
|
|
import 'package:uto_manager/domain/entities/echelon.dart';
|
|
import 'package:uto_manager/domain/entities/personnel_record.dart';
|
|
import 'package:uto_manager/domain/entities/position_slot.dart';
|
|
import 'package:uto_manager/domain/entities/unit_node.dart';
|
|
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
|
|
|
|
/// Codec for serializing and deserializing the full unit tree (units +
|
|
/// position slots + personnel) to/from a structured JSON representation.
|
|
///
|
|
/// The JSON schema is:
|
|
/// ```json
|
|
/// {
|
|
/// "version": 1,
|
|
/// "exportedAt": "<ISO-8601>",
|
|
/// "units": [ { ...UnitNode fields } ],
|
|
/// "slots": [ { ...PositionSlot fields } ],
|
|
/// "personnel": [ { ...PersonnelRecord fields } ]
|
|
/// }
|
|
/// ```
|
|
class UnitTreeCodec {
|
|
const UnitTreeCodec();
|
|
|
|
static const int _schemaVersion = 1;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Encode
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Encodes [units], [slots], and [personnel] into a JSON string.
|
|
String encode({
|
|
required List<UnitNode> units,
|
|
required List<PositionSlot> slots,
|
|
required List<PersonnelRecord> personnel,
|
|
}) {
|
|
final map = encodeToMap(units: units, slots: slots, personnel: personnel);
|
|
return const JsonEncoder.withIndent(' ').convert(map);
|
|
}
|
|
|
|
/// Encodes [units], [slots], and [personnel] into a [Map].
|
|
Map<String, dynamic> encodeToMap({
|
|
required List<UnitNode> units,
|
|
required List<PositionSlot> slots,
|
|
required List<PersonnelRecord> personnel,
|
|
}) {
|
|
return {
|
|
'version': _schemaVersion,
|
|
'exportedAt': DateTime.now().toUtc().toIso8601String(),
|
|
'units': units.map(_encodeUnit).toList(),
|
|
'slots': slots.map(_encodeSlot).toList(),
|
|
'personnel': personnel.map(_encodePersonnel).toList(),
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Decode
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Decodes a JSON string into a [UnitTreeSnapshot].
|
|
///
|
|
/// Throws [FormatException] if the JSON is malformed or the schema version
|
|
/// is unsupported.
|
|
UnitTreeSnapshot decode(String json) {
|
|
final dynamic raw = jsonDecode(json);
|
|
if (raw is! Map<String, dynamic>) {
|
|
throw const FormatException('Root JSON element must be an object.');
|
|
}
|
|
return decodeFromMap(raw);
|
|
}
|
|
|
|
/// Decodes a [Map] into a [UnitTreeSnapshot].
|
|
UnitTreeSnapshot decodeFromMap(Map<String, dynamic> map) {
|
|
final version = map['version'] as int?;
|
|
if (version == null || version != _schemaVersion) {
|
|
throw FormatException(
|
|
'Unsupported schema version: $version. Expected $_schemaVersion.',
|
|
);
|
|
}
|
|
|
|
final unitsList = (map['units'] as List<dynamic>? ?? [])
|
|
.cast<Map<String, dynamic>>()
|
|
.map(_decodeUnit)
|
|
.toList();
|
|
|
|
final slotsList = (map['slots'] as List<dynamic>? ?? [])
|
|
.cast<Map<String, dynamic>>()
|
|
.map(_decodeSlot)
|
|
.toList();
|
|
|
|
final personnelList = (map['personnel'] as List<dynamic>? ?? [])
|
|
.cast<Map<String, dynamic>>()
|
|
.map(_decodePersonnel)
|
|
.toList();
|
|
|
|
return UnitTreeSnapshot(
|
|
units: unitsList,
|
|
slots: slotsList,
|
|
personnel: personnelList,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Unit encoding/decoding
|
|
// ---------------------------------------------------------------------------
|
|
|
|
Map<String, dynamic> _encodeUnit(UnitNode unit) => {
|
|
'unitId': unit.unitId,
|
|
'echelon': unit.echelon.name,
|
|
'branch': unit.branch.name,
|
|
'parentUnitId': unit.parentUnitId,
|
|
'name': unit.name,
|
|
'isKeyLeader': unit.isKeyLeader,
|
|
};
|
|
|
|
UnitNode _decodeUnit(Map<String, dynamic> map) {
|
|
return UnitNode(
|
|
unitId: map['unitId'] as String,
|
|
echelon: Echelon.values.firstWhere((e) => e.name == map['echelon']),
|
|
branch: DoctrinalBranch.values.firstWhere((b) => b.name == map['branch']),
|
|
parentUnitId: map['parentUnitId'] as String?,
|
|
name: map['name'] as String,
|
|
isKeyLeader: map['isKeyLeader'] as bool? ?? false,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Slot encoding/decoding
|
|
// ---------------------------------------------------------------------------
|
|
|
|
Map<String, dynamic> _encodeSlot(PositionSlot slot) => {
|
|
'slotId': slot.slotId,
|
|
'unitId': slot.unitId,
|
|
'positionTitle': slot.positionTitle,
|
|
'dutyMos': slot.dutyMos.code,
|
|
'authorizedRank': slot.authorizedRank.name,
|
|
'isKeyLeader': slot.isKeyLeader,
|
|
'assignedPersonnelId': slot.assignedPersonnelId,
|
|
};
|
|
|
|
PositionSlot _decodeSlot(Map<String, dynamic> map) {
|
|
return PositionSlot(
|
|
slotId: map['slotId'] as String,
|
|
unitId: map['unitId'] as String,
|
|
positionTitle: map['positionTitle'] as String,
|
|
dutyMos: DutyMos.unvalidated(map['dutyMos'] as String),
|
|
authorizedRank: AuthorizedRank.values.firstWhere((r) => r.name == map['authorizedRank']),
|
|
isKeyLeader: map['isKeyLeader'] as bool? ?? false,
|
|
assignedPersonnelId: map['assignedPersonnelId'] as String?,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Personnel encoding/decoding
|
|
// ---------------------------------------------------------------------------
|
|
|
|
Map<String, dynamic> _encodePersonnel(PersonnelRecord record) => {
|
|
'soldierId': record.soldierId,
|
|
'name': record.name,
|
|
'primaryMos': record.primaryMos.code,
|
|
'payGrade': record.payGrade.name,
|
|
'qualifications': record.qualifications,
|
|
'readinessState': record.readinessState.name,
|
|
'assignedSlotId': record.assignedSlotId,
|
|
};
|
|
|
|
PersonnelRecord _decodePersonnel(Map<String, dynamic> map) {
|
|
return PersonnelRecord(
|
|
soldierId: map['soldierId'] as String,
|
|
name: map['name'] as String,
|
|
primaryMos: DutyMos.unvalidated(map['primaryMos'] as String),
|
|
payGrade: AuthorizedRank.values.firstWhere((r) => r.name == map['payGrade']),
|
|
qualifications: (map['qualifications'] as List<dynamic>? ?? []).cast<String>(),
|
|
readinessState: ReadinessState.values.firstWhere(
|
|
(s) => s.name == map['readinessState'],
|
|
),
|
|
assignedSlotId: map['assignedSlotId'] as String?,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A decoded snapshot of the full unit tree.
|
|
class UnitTreeSnapshot {
|
|
const UnitTreeSnapshot({
|
|
required this.units,
|
|
required this.slots,
|
|
required this.personnel,
|
|
});
|
|
|
|
final List<UnitNode> units;
|
|
final List<PositionSlot> slots;
|
|
final List<PersonnelRecord> personnel;
|
|
}
|