222 lines
7.5 KiB
Dart
222 lines
7.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:uto_manager/domain/entities/authorized_rank.dart';
|
|
import 'package:uto_manager/domain/entities/position_slot.dart';
|
|
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
/// Dialog for creating or editing a [PositionSlot] within a unit.
|
|
///
|
|
/// Pass [existing] to edit; leave null to create a new slot.
|
|
/// [unitId] is required and identifies which unit owns this slot.
|
|
class AddEditSlotDialog extends StatefulWidget {
|
|
const AddEditSlotDialog({
|
|
required this.unitId,
|
|
super.key,
|
|
this.existing,
|
|
});
|
|
|
|
final String unitId;
|
|
final PositionSlot? existing;
|
|
|
|
@override
|
|
State<AddEditSlotDialog> createState() => _AddEditSlotDialogState();
|
|
}
|
|
|
|
class _AddEditSlotDialogState extends State<AddEditSlotDialog> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
late final TextEditingController _titleCtrl;
|
|
late final TextEditingController _mosCtrl;
|
|
|
|
late AuthorizedRank _rank;
|
|
late bool _isKeyLeader;
|
|
List<String> _mosSuggestions = [];
|
|
|
|
bool get _isEditing => widget.existing != null;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final e = widget.existing;
|
|
_titleCtrl = TextEditingController(text: e?.positionTitle ?? '');
|
|
_mosCtrl = TextEditingController(text: e?.dutyMos.code ?? '');
|
|
_rank = e?.authorizedRank ?? AuthorizedRank.e4;
|
|
_isKeyLeader = e?.isKeyLeader ?? false;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_titleCtrl.dispose();
|
|
_mosCtrl.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _updateMosSuggestions(String query) {
|
|
if (query.isEmpty) {
|
|
setState(() => _mosSuggestions = []);
|
|
return;
|
|
}
|
|
final q = query.toUpperCase();
|
|
final matches =
|
|
DutyMos.allCodes.where((c) => c.startsWith(q) || c.contains(q)).take(12).toList()..sort();
|
|
setState(() => _mosSuggestions = matches);
|
|
}
|
|
|
|
void _submit() {
|
|
if (!_formKey.currentState!.validate()) return;
|
|
|
|
final mosCode = _mosCtrl.text.trim().toUpperCase();
|
|
DutyMos mos;
|
|
try {
|
|
mos = DutyMos(mosCode);
|
|
} catch (_) {
|
|
mos = DutyMos.unvalidated(mosCode);
|
|
}
|
|
|
|
final slot = PositionSlot(
|
|
slotId: widget.existing?.slotId ?? const Uuid().v4(),
|
|
unitId: widget.unitId,
|
|
positionTitle: _titleCtrl.text.trim(),
|
|
dutyMos: mos,
|
|
authorizedRank: _rank,
|
|
isKeyLeader: _isKeyLeader,
|
|
assignedPersonnelId: widget.existing?.assignedPersonnelId,
|
|
);
|
|
|
|
Navigator.of(context).pop(slot);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return AlertDialog(
|
|
title: Row(
|
|
children: [
|
|
Icon(_isEditing ? Icons.edit : Icons.add_circle_outline, size: 20),
|
|
const SizedBox(width: 8),
|
|
Text(_isEditing ? 'Edit Position Slot' : 'Add Position Slot'),
|
|
],
|
|
),
|
|
content: SizedBox(
|
|
width: 400,
|
|
child: Form(
|
|
key: _formKey,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Position Title
|
|
TextFormField(
|
|
controller: _titleCtrl,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Position Title',
|
|
hintText: 'e.g. Platoon Leader, Squad Leader, Medic',
|
|
prefixIcon: Icon(Icons.work_history_outlined),
|
|
),
|
|
textCapitalization: TextCapitalization.words,
|
|
validator: (v) {
|
|
if (v == null || v.trim().isEmpty) return 'Position title is required';
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Authorized Rank
|
|
DropdownButtonFormField<AuthorizedRank>(
|
|
initialValue: _rank,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Authorized Rank',
|
|
prefixIcon: Icon(Icons.military_tech_outlined),
|
|
),
|
|
items: AuthorizedRank.values.map((r) {
|
|
return DropdownMenuItem(
|
|
value: r,
|
|
child: Text('${r.payGrade} — ${r.rankTitle}'),
|
|
);
|
|
}).toList(),
|
|
onChanged: (v) => setState(() => _rank = v!),
|
|
),
|
|
const SizedBox(height: 12),
|
|
|
|
// Duty MOS with autocomplete
|
|
TextFormField(
|
|
controller: _mosCtrl,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Duty MOS',
|
|
hintText: 'e.g. 11B, 68W, 25U',
|
|
prefixIcon: Icon(Icons.badge_outlined),
|
|
),
|
|
textCapitalization: TextCapitalization.characters,
|
|
onChanged: _updateMosSuggestions,
|
|
validator: (v) {
|
|
if (v == null || v.trim().isEmpty) return 'Duty MOS is required';
|
|
final code = v.trim().toUpperCase();
|
|
if (!DutyMos.isKnown(code)) {
|
|
return 'Unknown MOS code (e.g. 11B, 68W, 25U)';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
if (_mosSuggestions.isNotEmpty) ...[
|
|
const SizedBox(height: 4),
|
|
Container(
|
|
padding: const EdgeInsets.all(4),
|
|
decoration: BoxDecoration(
|
|
color: theme.colorScheme.surfaceContainerHigh,
|
|
borderRadius: BorderRadius.circular(6),
|
|
border: Border.all(color: theme.colorScheme.outlineVariant),
|
|
),
|
|
child: Wrap(
|
|
spacing: 4,
|
|
runSpacing: 4,
|
|
children: _mosSuggestions.map((code) {
|
|
return ActionChip(
|
|
label: Text(
|
|
code,
|
|
style: theme.textTheme.labelSmall?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
onPressed: () {
|
|
_mosCtrl.text = code;
|
|
setState(() => _mosSuggestions = []);
|
|
},
|
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
visualDensity: VisualDensity.compact,
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 8),
|
|
|
|
// Key Leader toggle
|
|
SwitchListTile(
|
|
title: const Text('Key Leader Position'),
|
|
subtitle: const Text('e.g. Commander, XO, Platoon Leader, PSG'),
|
|
value: _isKeyLeader,
|
|
onChanged: (v) => setState(() => _isKeyLeader = v),
|
|
contentPadding: EdgeInsets.zero,
|
|
dense: true,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
FilledButton.icon(
|
|
icon: Icon(_isEditing ? Icons.save : Icons.add, size: 16),
|
|
label: Text(_isEditing ? 'Save Changes' : 'Add Slot'),
|
|
onPressed: _submit,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|