feat(phase-5): full CRUD UI, SoldierPosition entity, DB schema v2, web platform support

This commit is contained in:
UTO Manager Dev 2026-08-09 11:50:40 -04:00
parent 2f02ad4d49
commit e52d3406a6
29 changed files with 17201 additions and 891 deletions

590
.gitignore vendored
View File

@ -1,293 +1,297 @@
# =============================================================================
# UTO Manager — Flutter / Dart / VSCode / MkDocs .gitignore
# U.S. Army Unit Task Organization Enterprise System
# =============================================================================
# -----------------------------------------------------------------------------
# Flutter / Dart
# -----------------------------------------------------------------------------
# Dart tool cache
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
# Flutter build outputs
build/
*.apk
*.aab
*.ipa
*.app
*.exe
*.msix
*.msi
# Dart generated files
*.g.dart
*.freezed.dart
*.gr.dart
*.mocks.dart
# Drift / SQLite generated files
*.drift.dart
*.g.dart
# Protobuf generated files
*.pb.dart
*.pbenum.dart
*.pbgrpc.dart
*.pbjson.dart
# pubspec lock (keep for reproducible builds — comment out if desired)
# pubspec.lock
# Flutter web build
web/flutter_service_worker.js
web/flutter.js
web/flutter_bootstrap.js
web/version.json
web/main.dart.js
web/main.dart.js.map
# Flutter Windows build artifacts
windows/flutter/generated_plugin_registrant.cc
windows/flutter/generated_plugin_registrant.h
windows/flutter/generated_plugins.cmake
windows/flutter/ephemeral/
# Flutter Android build artifacts
android/.gradle/
android/local.properties
android/key.properties
android/app/google-services.json
android/app/debug.keystore
android/app/release.keystore
android/app/upload-keystore.jks
android/captures/
android/gradlew
android/gradlew.bat
android/gradle/
# Flutter iOS (future target)
ios/Flutter/App.framework
ios/Flutter/Flutter.framework
ios/Flutter/Flutter.podspec
ios/Flutter/Generated.xcconfig
ios/Flutter/ephemeral/
ios/Flutter/app.flx
ios/Flutter/app.zip
ios/Flutter/flutter_assets/
ios/ServiceDefinitions.json
ios/Runner/GeneratedPluginRegistrant.*
ios/Pods/
ios/.symlinks/
ios/Flutter/flutter_export_environment.sh
# Dart analysis cache
.dart_tool/
analysis_options.yaml.bak
# Coverage reports
coverage/
lcov.info
*.lcov
# Dart pub cache (local)
.pub-cache/
.pub/
# Flutter DevTools
.devtools
# -----------------------------------------------------------------------------
# Visual Studio Code
# -----------------------------------------------------------------------------
# VSCode workspace settings (keep .vscode/settings.json if team-shared)
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# VSCode local history
.history/
*.vsix
# VSCode test results
.vscode-test/
# -----------------------------------------------------------------------------
# MkDocs / Python Documentation
# -----------------------------------------------------------------------------
# MkDocs build output
site/
# Python virtual environments
.venv/
venv/
env/
ENV/
.env/
# Python cache
__pycache__/
*.py[cod]
*$py.class
*.pyo
*.pyd
.Python
# Python package build artifacts
dist/
*.egg-info/
*.egg
MANIFEST
# pip
pip-log.txt
pip-delete-this-directory.txt
# MkDocs cache
.cache/
.mkdocs_cache/
# Jupyter Notebooks (if used for documentation)
.ipynb_checkpoints/
# -----------------------------------------------------------------------------
# Operating System
# -----------------------------------------------------------------------------
# Windows
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
Desktop.ini
$RECYCLE.BIN/
*.lnk
*.stackdump
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Linux
*~
.fuse_hidden*
.directory
.Trash-*
.nfs*
# -----------------------------------------------------------------------------
# Security & Secrets
# -----------------------------------------------------------------------------
# Environment variable files
.env
.env.*
!.env.example
*.env
# Key files and certificates
*.pem
*.key
*.p12
*.pfx
*.jks
*.keystore
google-services.json
GoogleService-Info.plist
# SQLCipher / database encryption keys
*.db-key
*.sqlite-key
# Firebase
firebase.json
.firebaserc
firebase-debug.log
# -----------------------------------------------------------------------------
# CI/CD & Build Tools
# -----------------------------------------------------------------------------
# GitHub Actions local runner cache
.act/
# Gradle
.gradle/
gradle-app.setting
!gradle-wrapper.jar
!gradle-wrapper.properties
# CMake (Windows native build)
CMakeCache.txt
CMakeFiles/
CMakeScripts/
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps/
# Ninja build files
*.ninja
*.ninja_deps
*.ninja_log
# -----------------------------------------------------------------------------
# Logs & Temporary Files
# -----------------------------------------------------------------------------
# Log files
*.log
logs/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Temporary files
*.tmp
*.temp
*.swp
*.swo
*~
.~lock.*
# Test output
test-results/
test_results/
junit.xml
# -----------------------------------------------------------------------------
# Project-Specific
# -----------------------------------------------------------------------------
# UTO project save files (user data — not source controlled)
*.uto
# Exported organizational charts (generated artifacts)
exports/
# Local MOS dictionary overrides (use committed version)
local_mos_override.json
# Performance benchmark raw data
benchmarks/raw/
# DevTools snapshots
*.devtools_snapshot
*.heapsnapshot
# =============================================================================
# UTO Manager — Flutter / Dart / VSCode / MkDocs .gitignore
# U.S. Army Unit Task Organization Enterprise System
# =============================================================================
# -----------------------------------------------------------------------------
# Flutter / Dart
# -----------------------------------------------------------------------------
# Dart tool cache
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
# Flutter build outputs
build/
*.apk
*.aab
*.ipa
*.app
*.exe
*.msix
*.msi
# Dart generated files
*.g.dart
*.freezed.dart
*.gr.dart
*.mocks.dart
# Drift / SQLite generated files
*.drift.dart
*.g.dart
# Protobuf generated files
*.pb.dart
*.pbenum.dart
*.pbgrpc.dart
*.pbjson.dart
# pubspec lock (keep for reproducible builds — comment out if desired)
# pubspec.lock
# Flutter web build
web/flutter_service_worker.js
web/flutter.js
web/flutter_bootstrap.js
web/version.json
web/main.dart.js
web/main.dart.js.map
# Drift web worker build artifacts (keep drift_worker.js, sqlite3.wasm, drift_worker.dart)
web/drift_worker.js.deps
web/drift_worker.js.map
# Flutter Windows build artifacts
windows/flutter/generated_plugin_registrant.cc
windows/flutter/generated_plugin_registrant.h
windows/flutter/generated_plugins.cmake
windows/flutter/ephemeral/
# Flutter Android build artifacts
android/.gradle/
android/local.properties
android/key.properties
android/app/google-services.json
android/app/debug.keystore
android/app/release.keystore
android/app/upload-keystore.jks
android/captures/
android/gradlew
android/gradlew.bat
android/gradle/
# Flutter iOS (future target)
ios/Flutter/App.framework
ios/Flutter/Flutter.framework
ios/Flutter/Flutter.podspec
ios/Flutter/Generated.xcconfig
ios/Flutter/ephemeral/
ios/Flutter/app.flx
ios/Flutter/app.zip
ios/Flutter/flutter_assets/
ios/ServiceDefinitions.json
ios/Runner/GeneratedPluginRegistrant.*
ios/Pods/
ios/.symlinks/
ios/Flutter/flutter_export_environment.sh
# Dart analysis cache
.dart_tool/
analysis_options.yaml.bak
# Coverage reports
coverage/
lcov.info
*.lcov
# Dart pub cache (local)
.pub-cache/
.pub/
# Flutter DevTools
.devtools
# -----------------------------------------------------------------------------
# Visual Studio Code
# -----------------------------------------------------------------------------
# VSCode workspace settings (keep .vscode/settings.json if team-shared)
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# VSCode local history
.history/
*.vsix
# VSCode test results
.vscode-test/
# -----------------------------------------------------------------------------
# MkDocs / Python Documentation
# -----------------------------------------------------------------------------
# MkDocs build output
site/
# Python virtual environments
.venv/
venv/
env/
ENV/
.env/
# Python cache
__pycache__/
*.py[cod]
*$py.class
*.pyo
*.pyd
.Python
# Python package build artifacts
dist/
*.egg-info/
*.egg
MANIFEST
# pip
pip-log.txt
pip-delete-this-directory.txt
# MkDocs cache
.cache/
.mkdocs_cache/
# Jupyter Notebooks (if used for documentation)
.ipynb_checkpoints/
# -----------------------------------------------------------------------------
# Operating System
# -----------------------------------------------------------------------------
# Windows
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
Desktop.ini
$RECYCLE.BIN/
*.lnk
*.stackdump
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Linux
*~
.fuse_hidden*
.directory
.Trash-*
.nfs*
# -----------------------------------------------------------------------------
# Security & Secrets
# -----------------------------------------------------------------------------
# Environment variable files
.env
.env.*
!.env.example
*.env
# Key files and certificates
*.pem
*.key
*.p12
*.pfx
*.jks
*.keystore
google-services.json
GoogleService-Info.plist
# SQLCipher / database encryption keys
*.db-key
*.sqlite-key
# Firebase
firebase.json
.firebaserc
firebase-debug.log
# -----------------------------------------------------------------------------
# CI/CD & Build Tools
# -----------------------------------------------------------------------------
# GitHub Actions local runner cache
.act/
# Gradle
.gradle/
gradle-app.setting
!gradle-wrapper.jar
!gradle-wrapper.properties
# CMake (Windows native build)
CMakeCache.txt
CMakeFiles/
CMakeScripts/
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps/
# Ninja build files
*.ninja
*.ninja_deps
*.ninja_log
# -----------------------------------------------------------------------------
# Logs & Temporary Files
# -----------------------------------------------------------------------------
# Log files
*.log
logs/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Temporary files
*.tmp
*.temp
*.swp
*.swo
*~
.~lock.*
# Test output
test-results/
test_results/
junit.xml
# -----------------------------------------------------------------------------
# Project-Specific
# -----------------------------------------------------------------------------
# UTO project save files (user data — not source controlled)
*.uto
# Exported organizational charts (generated artifacts)
exports/
# Local MOS dictionary overrides (use committed version)
local_mos_override.json
# Performance benchmark raw data
benchmarks/raw/
# DevTools snapshots
*.devtools_snapshot
*.heapsnapshot

574
README.md
View File

@ -1,286 +1,288 @@
# UTO Manager
## Flutter-Based Military Unit Task Organization Enterprise System
### U.S. Army 2026 Doctrine — Cross-Platform Command & Control Application
---
[![Flutter](https://img.shields.io/badge/Flutter-3.x-02569B?logo=flutter)](https://flutter.dev)
[![Dart](https://img.shields.io/badge/Dart-3.x-0175C2?logo=dart)](https://dart.dev)
[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Web%20%7C%20Android-brightgreen)](https://flutter.dev/multi-platform)
[![License](https://img.shields.io/badge/License-Proprietary-red)](LICENSE)
[![Docs](https://img.shields.io/badge/Docs-MkDocs-526CFE?logo=materialformkdocs)](https://www.mkdocs.org)
---
## Overview
**UTO Manager** is a cross-platform command and control (C2) application for creating, visualizing, and validating U.S. Army unit task organizations (UTOs) in accordance with 2026 Army doctrine. Built with Flutter, the application targets **Windows x64**, **Web (WASM/CanvasKit)**, and **Android** from a single Dart codebase.
The system models military formations from individual soldier positions up to multi-domain Brigade Combat Teams, enforcing Modified Table of Organization and Equipment (MTOE) doctrinal rules in real time.
---
## Key Features
- **Interactive Visual Canvas** — GPU-accelerated `CustomPainter` engine with pan/zoom, viewport culling, and 60 FPS rendering at 5,000+ nodes
- **Drag-and-Drop Re-parenting** — Cross-platform state machine with doctrinal validation highlights (green/red) and undo/redo support
- **MIL-STD-2525D Symbology** — Hybrid rendering pipeline: `milsymbol.js` on Web, native SVG path engine on Windows/Android
- **2026 Doctrinal Templates** — Pre-configured JSON templates for MBCT, SBCT, ABCT, CAB-Heavy, and CAB-Light formations
- **Real-Time MTOE Validation** — Background Dart Isolate engine detecting MOS mismatches, rank violations, vacant leadership slots, and structural hierarchy errors
- **Offline-First Architecture** — Drift/SQLite persistence with delta synchronization for contested network environments
- **Multi-Format Export** — JSON, Protobuf, and vector PDF organizational chart export
---
## Platform Targets
| Platform | Deployment | Performance Target |
|---|---|---|
| Windows x64 | Native `.msi` / `.exe` | 60 FPS @ 5,000+ nodes |
| Web (WASM) | WebAssembly + CanvasKit | 60 FPS @ 2,000+ nodes |
| Android | Native `.apk` / `.aab` | 60 FPS @ 1,500+ nodes |
---
## Architecture
```text
[Presentation Layer: CustomPainter / InteractiveViewer / Responsive Shell]
│
▼
[Application State Layer: BLoC / Signals / Super Drag-and-Drop Pipeline]
│
▼
[Domain Layer: MTOE Doctrinal Rules Engine / Structural Validation]
│
▼
[Infrastructure Layer: Drift SQLite / IndexedDB / Protobuf Codecs]
```
### Project Structure
```text
lib/
├── domain/ # Entities, value objects, repository interfaces
│ ├── entities/ # UnitNode, PositionSlot, PersonnelRecord
│ ├── enums/ # Echelon, DoctrinalBranch, AuthorizedRank
│ └── repositories/ # Abstract repository contracts
├── application/ # BLoC / use cases / state management
│ ├── blocs/ # State management BLoCs
│ └── use_cases/ # Domain use case implementations
├── infrastructure/ # Drift DB, IndexedDB adapters, Protobuf codecs
│ ├── database/ # Drift table definitions and DAOs
│ ├── codecs/ # JSON and Protobuf serialization
│ └── sync/ # Delta synchronization engine
└── presentation/ # Widgets, painters, responsive shell
├── canvas/ # CustomPainter, InteractiveViewer, viewport
├── layout/ # Reingold-Tilford algorithm, Quadtree index
├── symbology/ # MIL-STD-2525D rendering pipeline
└── shell/ # Three-pane UI, responsive layouts
```
---
## Getting Started
### Prerequisites
- [Flutter SDK](https://docs.flutter.dev/get-started/install) 3.x or later
- [Dart SDK](https://dart.dev/get-dart) 3.x (included with Flutter)
- [Visual Studio 2022](https://visualstudio.microsoft.com/) with **Desktop development with C++** workload (Windows target)
- [Android Studio](https://developer.android.com/studio) with Android SDK (Android target)
- [Python 3.9+](https://www.python.org/) with `pip` (MkDocs documentation)
- [Git](https://git-scm.com/)
### Clone the Repository
```bash
git clone https://git.kellsupport.com/uto_manager.git
cd uto_manager
```
### Install Flutter Dependencies
```bash
flutter pub get
```
### Run the Application
```bash
# Windows desktop
flutter run -d windows
# Web (Chrome)
flutter run -d chrome
# Web (WASM mode)
flutter run -d chrome --wasm
# Android (connected device or emulator)
flutter run -d android
```
### Run Tests
```bash
# All tests
flutter test
# With coverage
flutter test --coverage
# Static analysis
flutter analyze
```
### Build Release Packages
```bash
# Windows
flutter build windows --release
# Web (WASM)
flutter build web --wasm --release
# Android APK
flutter build apk --release
# Android App Bundle (Google Play)
flutter build appbundle --release
```
---
## Documentation
Project documentation is built with [MkDocs](https://www.mkdocs.org/) using the [Material theme](https://squidfunk.github.io/mkdocs-material/).
### Install Documentation Dependencies
```bash
pip install -r docs/requirements.txt
```
### Serve Documentation Locally
```bash
mkdocs serve
```
Then open [http://127.0.0.1:8000](http://127.0.0.1:8000) in your browser.
### Build Documentation Site
```bash
mkdocs build
```
Output is written to the `site/` directory (excluded from version control).
---
## Development Workflow
### Branch Strategy
| Branch | Purpose |
|---|---|
| `main` | Production-ready releases only |
| `develop` | Integration branch for completed features |
| `feature/*` | Individual feature development |
| `fix/*` | Bug fixes |
| `release/*` | Release preparation and stabilization |
### Commit Convention
This project follows [Conventional Commits](https://www.conventionalcommits.org/):
```text
feat(canvas): implement viewport culling via inverse matrix transform
fix(validation): correct MOS mismatch detection for 68W slots
docs(readme): update platform prerequisites
chore(deps): upgrade drift to 2.x
```
### Code Quality
```bash
# Format code
dart format .
# Analyze code
flutter analyze
# Run linter
dart run dart_code_metrics:metrics analyze lib
```
---
## Technology Stack
| Layer | Technology | Purpose |
|---|---|---|
| UI Framework | Flutter (Dart) | Single codebase for Windows, Web, Android |
| Rendering | CustomPainter + InteractiveViewer | GPU-direct drawing; bypasses widget tree overhead |
| State Management | BLoC + Signals | Reactive atomic state |
| Drag-and-Drop | super_drag_and_drop | Cross-platform payload integrity |
| Local Database | Drift (SQLite / IndexedDB) | Type-safe reactive ORM; offline-first |
| Encryption | SQLCipher | Data-at-rest protection |
| Symbology (Web) | milsymbol.js via dart:js_interop | Full MIL-STD-2525D compliance |
| Symbology (Native) | path_drawing + SVG assets + LRU cache | Zero external runtime dependencies |
| Background Compute | Dart Isolates | Offload layout, validation, export |
| Serialization | JSON + Protobuf | Human-readable + compact binary export |
| PDF Export | pdf package | Vector-quality organizational chart output |
| Documentation | MkDocs + Material theme | User and developer documentation |
---
## Development Phases
| Phase | Title | Weeks | Status |
|---|---|---|---|
| Phase 1 | Architecture & Data Foundations | 1 – 4 | ⬜ Not Started |
| Phase 2 | Canvas Graphics Engine & Gesture Subsystem | 5 – 9 | ⬜ Not Started |
| Phase 3 | 2026 Doctrinal Templates & Symbology Engine | 10 – 14 | ⬜ Not Started |
| Phase 4 | Persistence, Validation Rules, & Export Engine | 15 – 19 | ⬜ Not Started |
| Phase 5 | Platform Optimization, Profiling, & Hardening | 20 – 24 | ⬜ Not Started |
See [`development_status.md`](development_status.md) for detailed task tracking and [`phased_development_plan.md`](phased_development_plan.md) for full phase specifications.
---
## Contributing
1. Fork the repository and create a `feature/*` branch from `develop`
2. Implement your changes with appropriate unit tests
3. Ensure `flutter analyze` passes with zero violations
4. Ensure `flutter test --coverage` meets the 80% minimum threshold
5. Submit a pull request targeting the `develop` branch
6. Pull requests require passing CI on all three platform targets before merge
---
## Security
This application is designed for use with U.S. Army organizational data. All local data is encrypted at rest using SQLCipher. Encryption keys are stored in platform keychains (Windows Credential Manager / Android Keystore).
**Do not commit sensitive data, encryption keys, or classified information to this repository.**
---
## License
Proprietary — All rights reserved. See [LICENSE](LICENSE) for details.
---
## Related Documents
- [`master_development_plan.md`](master_development_plan.md) — Full architectural specification
- [`phased_development_plan.md`](phased_development_plan.md) — Phase-by-phase implementation plan
- [`development_status.md`](development_status.md) — Sprint tracking and task status
- [Documentation Site](http://127.0.0.1:8000) — MkDocs user and developer documentation
# UTO Manager
## Flutter-Based Military Unit Task Organization Enterprise System
### U.S. Army 2026 Doctrine — Cross-Platform Command & Control Application
---
[![Flutter](https://img.shields.io/badge/Flutter-3.x-02569B?logo=flutter)](https://flutter.dev)
[![Dart](https://img.shields.io/badge/Dart-3.x-0175C2?logo=dart)](https://dart.dev)
[![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20Web%20%7C%20Android-brightgreen)](https://flutter.dev/multi-platform)
[![License](https://img.shields.io/badge/License-Proprietary-red)](LICENSE)
[![Docs](https://img.shields.io/badge/Docs-MkDocs-526CFE?logo=materialformkdocs)](https://www.mkdocs.org)
---
## Overview
**UTO Manager** is a cross-platform command and control (C2) application for creating, visualizing, and validating U.S. Army unit task organizations (UTOs) in accordance with 2026 Army doctrine. Built with Flutter, the application targets **Windows x64**, **Web (WASM/CanvasKit)**, and **Android** from a single Dart codebase.
The system models military formations from individual soldier positions up to multi-domain Brigade Combat Teams, enforcing Modified Table of Organization and Equipment (MTOE) doctrinal rules in real time.
---
## Key Features
- **Interactive Visual Canvas** — GPU-accelerated `CustomPainter` engine with pan/zoom, viewport culling, and 60 FPS rendering at 5,000+ nodes
- **Full CRUD UI** — Add, edit, and delete units, position slots, and soldiers via modal dialogs with live MOS autocomplete
- **Drag-and-Drop Re-parenting** — Cross-platform state machine with doctrinal validation highlights (green/red) and undo/redo support
- **MIL-STD-2525D Symbology** — Hybrid rendering pipeline: `milsymbol.js` on Web, native SVG path engine on Windows/Android *(planned)*
- **2026 Doctrinal Templates** — Pre-configured JSON templates for MBCT, SBCT, ABCT, CAB-Heavy, and CAB-Light formations
- **Real-Time MTOE Validation** — Background Dart Isolate engine detecting MOS mismatches, rank violations, vacant leadership slots, and structural hierarchy errors
- **Offline-First Architecture** — Drift/SQLite persistence with delta synchronization for contested network environments
- **Multi-Format Export** — JSON, Protobuf (`.utob`), and vector PDF organizational chart export
- **Web Platform Support** — Full WASM/IndexedDB persistence via `drift_worker.js` and `sqlite3.wasm`
---
## Platform Targets
| Platform | Deployment | Performance Target |
|---|---|---|
| Windows x64 | Native `.msi` / `.exe` | 60 FPS @ 5,000+ nodes |
| Web (WASM) | WebAssembly + CanvasKit | 60 FPS @ 2,000+ nodes |
| Android | Native `.apk` / `.aab` | 60 FPS @ 1,500+ nodes |
---
## Architecture
```text
[Presentation Layer: CustomPainter / InteractiveViewer / Responsive Shell]
│
▼
[Application State Layer: BLoC / Signals / Super Drag-and-Drop Pipeline]
│
▼
[Domain Layer: MTOE Doctrinal Rules Engine / Structural Validation]
│
▼
[Infrastructure Layer: Drift SQLite / IndexedDB / Protobuf Codecs]
```
### Project Structure
```text
lib/
├── domain/ # Entities, value objects, repository interfaces
│ ├── entities/ # UnitNode, PositionSlot, PersonnelRecord
│ ├── enums/ # Echelon, DoctrinalBranch, AuthorizedRank
│ └── repositories/ # Abstract repository contracts
├── application/ # BLoC / use cases / state management
│ ├── blocs/ # State management BLoCs
│ └── use_cases/ # Domain use case implementations
├── infrastructure/ # Drift DB, IndexedDB adapters, Protobuf codecs
│ ├── database/ # Drift table definitions and DAOs
│ ├── codecs/ # JSON and Protobuf serialization
│ └── sync/ # Delta synchronization engine
└── presentation/ # Widgets, painters, responsive shell
├── canvas/ # CustomPainter, InteractiveViewer, viewport
├── layout/ # Reingold-Tilford algorithm, Quadtree index
├── symbology/ # MIL-STD-2525D rendering pipeline
└── shell/ # Three-pane UI, responsive layouts
```
---
## Getting Started
### Prerequisites
- [Flutter SDK](https://docs.flutter.dev/get-started/install) 3.x or later
- [Dart SDK](https://dart.dev/get-dart) 3.x (included with Flutter)
- [Visual Studio 2022](https://visualstudio.microsoft.com/) with **Desktop development with C++** workload (Windows target)
- [Android Studio](https://developer.android.com/studio) with Android SDK (Android target)
- [Python 3.9+](https://www.python.org/) with `pip` (MkDocs documentation)
- [Git](https://git-scm.com/)
### Clone the Repository
```bash
git clone https://git.kellsupport.com/uto_manager.git
cd uto_manager
```
### Install Flutter Dependencies
```bash
flutter pub get
```
### Run the Application
```bash
# Windows desktop
flutter run -d windows
# Web (Chrome)
flutter run -d chrome
# Web (WASM mode)
flutter run -d chrome --wasm
# Android (connected device or emulator)
flutter run -d android
```
### Run Tests
```bash
# All tests
flutter test
# With coverage
flutter test --coverage
# Static analysis
flutter analyze
```
### Build Release Packages
```bash
# Windows
flutter build windows --release
# Web (WASM)
flutter build web --wasm --release
# Android APK
flutter build apk --release
# Android App Bundle (Google Play)
flutter build appbundle --release
```
---
## Documentation
Project documentation is built with [MkDocs](https://www.mkdocs.org/) using the [Material theme](https://squidfunk.github.io/mkdocs-material/).
### Install Documentation Dependencies
```bash
pip install -r docs/requirements.txt
```
### Serve Documentation Locally
```bash
mkdocs serve
```
Then open [http://127.0.0.1:8000](http://127.0.0.1:8000) in your browser.
### Build Documentation Site
```bash
mkdocs build
```
Output is written to the `site/` directory (excluded from version control).
---
## Development Workflow
### Branch Strategy
| Branch | Purpose |
|---|---|
| `main` | Production-ready releases only |
| `develop` | Integration branch for completed features |
| `feature/*` | Individual feature development |
| `fix/*` | Bug fixes |
| `release/*` | Release preparation and stabilization |
### Commit Convention
This project follows [Conventional Commits](https://www.conventionalcommits.org/):
```text
feat(canvas): implement viewport culling via inverse matrix transform
fix(validation): correct MOS mismatch detection for 68W slots
docs(readme): update platform prerequisites
chore(deps): upgrade drift to 2.x
```
### Code Quality
```bash
# Format code
dart format .
# Analyze code
flutter analyze
# Run linter
dart run dart_code_metrics:metrics analyze lib
```
---
## Technology Stack
| Layer | Technology | Purpose |
|---|---|---|
| UI Framework | Flutter (Dart) | Single codebase for Windows, Web, Android |
| Rendering | CustomPainter + InteractiveViewer | GPU-direct drawing; bypasses widget tree overhead |
| State Management | BLoC + Signals | Reactive atomic state |
| Drag-and-Drop | super_drag_and_drop | Cross-platform payload integrity |
| Local Database | Drift (SQLite / IndexedDB) | Type-safe reactive ORM; offline-first |
| Encryption | SQLCipher | Data-at-rest protection |
| Symbology (Web) | milsymbol.js via dart:js_interop | Full MIL-STD-2525D compliance |
| Symbology (Native) | path_drawing + SVG assets + LRU cache | Zero external runtime dependencies |
| Background Compute | Dart Isolates | Offload layout, validation, export |
| Serialization | JSON + Protobuf | Human-readable + compact binary export |
| PDF Export | pdf package | Vector-quality organizational chart output |
| Documentation | MkDocs + Material theme | User and developer documentation |
---
## Development Phases
| Phase | Title | Weeks | Status |
|---|---|---|---|
| Phase 1 | Architecture & Data Foundations | 1 – 4 | ✅ Complete |
| Phase 2 | Canvas Graphics Engine & Persistence | 5 – 9 | ✅ Complete |
| Phase 3 | Template Library & Import/Export | 10 – 14 | ✅ Complete |
| Phase 4 | Reporting & Analytics | 15 – 19 | ✅ Complete |
| Phase 5 | UI/CRUD Layer & Web Platform | 20 – 24 | ✅ Complete |
See [`development_status.md`](development_status.md) for detailed task tracking and [`phased_development_plan.md`](phased_development_plan.md) for full phase specifications.
---
## Contributing
1. Fork the repository and create a `feature/*` branch from `develop`
2. Implement your changes with appropriate unit tests
3. Ensure `flutter analyze` passes with zero violations
4. Ensure `flutter test --coverage` meets the 80% minimum threshold
5. Submit a pull request targeting the `develop` branch
6. Pull requests require passing CI on all three platform targets before merge
---
## Security
This application is designed for use with U.S. Army organizational data. All local data is encrypted at rest using SQLCipher. Encryption keys are stored in platform keychains (Windows Credential Manager / Android Keystore).
**Do not commit sensitive data, encryption keys, or classified information to this repository.**
---
## License
Proprietary — All rights reserved. See [LICENSE](LICENSE) for details.
---
## Related Documents
- [`master_development_plan.md`](master_development_plan.md) — Full architectural specification
- [`phased_development_plan.md`](phased_development_plan.md) — Phase-by-phase implementation plan
- [`development_status.md`](development_status.md) — Sprint tracking and task status
- [Documentation Site](http://127.0.0.1:8000) — MkDocs user and developer documentation

View File

@ -1,12 +1,12 @@
# UTO Manager — Development Status
Last updated: 2026-08-09 — branch `fix/phase-4-analyzer-infos` merged to `main`
Last updated: 2026-08-09 — Phase 5 UI/CRUD layer merged to `main`
---
## Current Branch
`main` (fix/phase-4-analyzer-infos merged)
`main` (Phase 5 UI/CRUD layer merged)
## Overall Progress
@ -16,7 +16,7 @@ Last updated: 2026-08-09 — branch `fix/phase-4-analyzer-infos` merged to `main
| 2 | Canvas Engine & Persistence | ✅ Complete |
| 3 | Template Library & Import/Export | ✅ Complete |
| 4 | Reporting & Analytics | ✅ Complete |
| 5 | Multi-user / Sync | 🔲 Not started |
| 5 | UI/CRUD Layer & Web Platform | ✅ Complete |
---
@ -204,6 +204,132 @@ Last updated: 2026-08-09 — branch `fix/phase-4-analyzer-infos` merged to `main
---
## Phase 5 — UI/CRUD Layer & Web Platform ✅
**Completed items:**
### Domain Entities (Phase 5)
- `SoldierPosition` entity — named duty position for assignment to personnel records:
- `positionId` (UUID v4 for custom; fixed string for built-ins)
- `title` — human-readable position title (e.g. "Team Leader", "Rifleman")
- `isBuiltIn` — true for app-seeded positions; false for user-created ones
- `copyWith`, `Equatable` props, `toString`
- `SoldierPosition.builtIns` — 31 pre-defined Army duty positions across all branches:
- Command/Staff: Commander, XO, CSM, First Sergeant, S2/S3/S4/S6
- Platoon: Platoon Leader, Platoon Sergeant, RTO
- Squad/Section: Squad Leader, Team Leader
- Individual: Rifleman, Grenadier, Automatic Rifleman, Machine Gunner, Sniper,
JTAC/Fire Support, Medic, Combat Engineer, Driver, Gunner, Loader,
Tank Commander, Crew Member, Supply Sergeant, Armorer, Commo Sergeant,
Intelligence Analyst, Forward Observer, CBRN Specialist
### Domain Entities — Extended (Phase 5)
- `PersonnelRecord` — Extended with:
- `positionTitle` field (optional) — duty position title from `SoldierPosition`
- `displayLabel` getter — prefers `name`; falls back to `positionTitle`; then `(Unnamed Soldier)`
- `isAssigned` getter — true when `assignedSlotId != null`
- `isFmc` getter — true when `readinessState == fullyMissionCapable`
- `copyWith` — extended with `clearName`, `clearPositionTitle`, `clearAssignment` flags
### Infrastructure — Database (Phase 5)
- `SoldierPositions` Drift table — persists named duty positions:
- `position_id` (TEXT, PK), `title` (TEXT), `is_built_in` (BOOLEAN, default false)
- `SoldierPositionsDao` — full DAO for `SoldierPositions`:
- `getAll()`, `getById(positionId)`, `upsert()`, `upsertAll()`, `updatePosition()`,
`deletePosition()`, `watchAll()`
- `AppDatabase` — schema version bumped to **v2**:
- Migration v1→v2: adds `position_title` column to `Personnel` table; creates `soldier_positions` table
- Web platform support:
- `drift_worker.dart` / `drift_worker.js` — Drift web worker for IndexedDB persistence
- `sqlite3.wasm` — SQLite WASM binary for web target
- `_openConnection()` updated to use `driftDatabase()` (platform-adaptive: SQLite on native, WASM/IndexedDB on web)
### Infrastructure — Repositories (Phase 5)
- `DriftSoldierPositionsRepository` — Drift-backed repository for `SoldierPosition`:
- `getAll()` — returns all positions (built-in + custom)
- `watchAll()` — reactive stream of all positions
- `upsert(position)` — insert or update a position
- `seedBuiltIns()` — idempotent seed of all `SoldierPosition.builtIns` on first launch
- `delete(positionId)` — deletes custom positions only; no-op for built-ins
### Application — BLoC (Phase 5)
- `UnitTreeCubit` — Extended with full CRUD operations:
- `addUnit(unit)` — inserts a new `UnitNode`
- `updateUnit(unit)` — updates an existing `UnitNode`
- `addSlot(slot)` — inserts a new `PositionSlot`
- `updateSlot(slot)` — updates an existing `PositionSlot`
- `deleteSlot(slotId)` — deletes a position slot
- `clearAll()` — deletes all slots then all units (template reset)
- `PersonnelCubit` — Extended with full CRUD operations:
- `addPersonnel(record)` — inserts a new `PersonnelRecord`
- `updatePersonnel(record)` — updates an existing `PersonnelRecord`
- `deletePersonnel(soldierId)` — deletes soldier; clears slot assignment first if assigned
### Presentation — Dialogs (Phase 5)
- `AddEditPersonnelDialog` — full-featured dialog for creating/editing soldiers:
- Position dropdown (loaded from `DriftSoldierPositionsRepository`) with add/rename/delete custom positions
- Optional full name field (Last, First MI format)
- Pay grade dropdown (all `AuthorizedRank` values)
- Primary MOS field with live autocomplete suggestions from `DutyMos.allCodes`
- Readiness state dropdown
- MOS validation against known codes
- `AddEditSlotDialog` — dialog for creating/editing position slots:
- Position title text field
- Authorized rank dropdown
- Duty MOS field with live autocomplete suggestions
- Key leader toggle switch
- `AddEditUnitDialog` — dialog for creating/editing unit nodes:
- Optional unit name/designation field (auto-generates from echelon + branch if blank)
- Echelon dropdown
- Branch/functional area dropdown
- Optional parent unit dropdown (excludes self and descendants)
- Key leader unit toggle switch
### Presentation — HomeScreen (Phase 5)
- App bar actions:
- **Add Unit** (`add_box_outlined`) — opens `AddEditUnitDialog`
- **Add Soldier** (`person_add_outlined`) — opens `AddEditPersonnelDialog`
- **Run MTOE Validation** (`fact_check_outlined`) — triggers `ValidationCubit.validate()`
- **Export** (`upload_file_outlined`) — opens export format selection dialog
- **Clear All** (`delete_sweep_outlined`) — confirmation dialog to reset all units/slots
- Personnel panel:
- Search/filter bar (name, MOS, rank)
- Tap soldier tile to edit via `AddEditPersonnelDialog`
- Assigned/unassigned count footer
- Empty-state prompt with "Add Soldier" button
- Inspector panel:
- Edit unit button → `AddEditUnitDialog`
- Add child unit button → `AddEditUnitDialog` (pre-selected parent)
- Delete unit button → confirmation dialog (cascades to subtree)
- Validation findings section (per-unit, grouped by severity with color-coded icons)
- Position slots header with fill count and "Add Slot" button
- Per-slot edit and delete actions via `AddEditSlotDialog`
- Empty-state prompts for no units selected and no slots defined
- Export dialog (`_ExportDialog`):
- Format selection: JSON, Binary (`.utob`), PDF
- PDF page size selector (Letter, A4, Tabloid)
- Export result/error feedback inline
- Status bar:
- Unit count, slot fill rate (filled/total + percentage)
- Total soldier count
- Validation state: running spinner, clean indicator, or critical/warning/advisory counts
### App Wiring (Phase 5)
- `main.dart` — Added `DriftSoldierPositionsRepository` wired to `HomeScreen`
- `HomeScreen` — now requires `positionsRepository` parameter
**Test count (Phase 5):** existing 263 tests; widget test updated for new `HomeScreen` signature
---
## Test Summary
| File | Tests | Status |
@ -227,15 +353,18 @@ Last updated: 2026-08-09 — branch `fix/phase-4-analyzer-infos` merged to `main
---
## Known Limitations / Next Steps (Phase 5)
## Known Limitations / Next Steps (Phase 5 Hardening)
- No multi-user or real-time sync capability (Phase 5)
- No multi-user or real-time sync capability (Phase 5 hardening)
- Canvas layout is vertical tree only; horizontal layout not yet implemented
- No undo/redo for assignment operations
- No search/filter on the canvas itself
- `DeltaSynchronizer` uses simulated online/offline; real network transport deferred to Phase 5
- `ProtobufCodec` uses zlib-compressed JSON as payload; true protobuf schema deferred to Phase 5
- `DeltaSynchronizer` uses simulated online/offline; real network transport deferred
- `ProtobufCodec` uses zlib-compressed JSON as payload; true protobuf schema deferred
- Template instantiation does not yet persist to database in a single Drift transaction (uses sequential inserts)
- MIL-STD-2525D symbology engine not yet implemented (deferred)
- SQLCipher data-at-rest encryption not yet integrated (deferred)
- No undo/redo stack for unit/slot/personnel CRUD operations
---
@ -245,6 +374,7 @@ Last updated: 2026-08-09 — branch `fix/phase-4-analyzer-infos` merged to `main
lib/
├── domain/
│ ├── entities/ # Pure Dart domain models (no Flutter deps)
│ │ # UnitNode, PositionSlot, PersonnelRecord, SoldierPosition
│ ├── value_objects/ # DutyMos (supports 3-char and 4-char AOC formats)
│ ├── repositories/ # Abstract interfaces
│ └── services/ # UnitTreeService, AssignmentService,
@ -254,12 +384,18 @@ lib/
├── infrastructure/
│ ├── codecs/ # UnitTreeCodec (JSON), ProtobufCodec (binary),
│ │ # PdfChartExporter (PDF org chart)
│ ├── database/ # Drift schema, DAOs, generated code
│ ├── database/ # Drift schema (v2), DAOs, generated code
│ │ # Tables: Units, PositionSlots, Personnel,
│ │ # SoldierPositions, Templates
│ └── repositories/ # Drift-backed implementations
│ # DriftSoldierPositionsRepository (new in Phase 5)
├── application/
│ └── blocs/ # UnitTreeCubit, PersonnelCubit,
│ # TemplateCubit, ValidationCubit
└── presentation/
├── screens/ # HomeScreen (with PDF/binary export + validation UI)
├── screens/
│ ├── home_screen.dart # Full CRUD UI + export dialog
│ └── dialogs/ # AddEditPersonnelDialog, AddEditSlotDialog,
│ # AddEditUnitDialog (all new in Phase 5)
└── widgets/ # Canvas, cards, tiles
```

3
devtools_options.yaml Normal file
View File

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@ -1,89 +1,90 @@
# Developer Guide
## UTO Manager — Engineering & Architecture Reference
---
This guide covers the technical architecture, implementation details, and contribution guidelines for engineers building and extending the UTO Manager platform.
---
## In This Guide
| Section | Description |
|---|---|
| [Architecture Overview](architecture/overview.md) | Layered clean architecture and system design |
| [Domain Model](domain/entities.md) | Core Dart entities, enums, and repository contracts |
| [Canvas Engine](canvas/custom-painter.md) | CustomPainter rendering, viewport culling, layout algorithm |
| [Symbology Engine](symbology/mil-std-2525d.md) | MIL-STD-2525D hybrid rendering pipeline |
| [Database & Persistence](database/drift-schema.md) | Drift schema, DAOs, migrations, offline-first |
| [Validation Engine](validation/mtoe-engine.md) | Real-time MTOE validation in background Isolates |
| [Export Engine](export/json-codec.md) | JSON, Protobuf, and PDF export modules |
| [Performance](performance/repaint-boundaries.md) | RepaintBoundary isolation, Dart Isolates, benchmarks |
| [CI/CD](cicd/pipeline.md) | Multi-platform build pipeline and test gates |
| [Contributing](contributing/workflow.md) | Branch strategy, commit conventions, code standards |
---
## Architecture at a Glance
```text
[Presentation Layer]
CustomPainter / InteractiveViewer / Responsive Shell
│
▼
[Application State Layer]
BLoC / Signals / Drag-and-Drop Pipeline
│
▼
[Domain Layer]
MTOE Rules Engine / Structural Validation / Entities
│
▼
[Infrastructure Layer]
Drift SQLite / IndexedDB / Protobuf Codecs / Delta Sync
```
---
## Technology Stack
| Layer | Technology |
|---|---|
| UI Framework | Flutter 3.x (Dart 3.x) |
| Rendering | CustomPainter + InteractiveViewer |
| State Management | BLoC + Signals |
| Drag-and-Drop | super_drag_and_drop |
| Local Database | Drift (SQLite / IndexedDB) |
| Encryption | SQLCipher |
| Symbology (Web) | milsymbol.js via dart:js_interop |
| Symbology (Native) | path_drawing + SVG assets + LRU cache |
| Background Compute | Dart Isolates |
| Serialization | JSON + Protobuf |
| PDF Export | pdf package |
| Documentation | MkDocs + Material theme |
---
## Development Setup
```bash
# Clone the repository
git clone https://git.kellsupport.com/uto_manager.git
cd uto_manager
# Install Flutter dependencies
flutter pub get
# Run static analysis
flutter analyze
# Run all tests with coverage
flutter test --coverage
# Install documentation dependencies
pip install -r docs/requirements.txt
# Serve documentation locally
mkdocs serve
```
# Developer Guide
## UTO Manager — Engineering & Architecture Reference
---
This guide covers the technical architecture, implementation details, and contribution guidelines for engineers building and extending the UTO Manager platform.
---
## In This Guide
| Section | Description |
|---|---|
| [Architecture Overview](architecture/overview.md) | Layered clean architecture and system design |
| [Domain Model](domain/entities.md) | Core Dart entities, enums, and repository contracts |
| [Canvas Engine](canvas/custom-painter.md) | CustomPainter rendering, viewport culling, layout algorithm |
| [Symbology Engine](symbology/mil-std-2525d.md) | MIL-STD-2525D hybrid rendering pipeline |
| [Database & Persistence](database/drift-schema.md) | Drift schema, DAOs, migrations, offline-first |
| [Validation Engine](validation/mtoe-engine.md) | Real-time MTOE validation in background Isolates |
| [Export Engine](export/json-codec.md) | JSON, Protobuf, and PDF export modules |
| [Performance](performance/repaint-boundaries.md) | RepaintBoundary isolation, Dart Isolates, benchmarks |
| [CI/CD](cicd/pipeline.md) | Multi-platform build pipeline and test gates |
| [Contributing](contributing/workflow.md) | Branch strategy, commit conventions, code standards |
---
## Architecture at a Glance
```text
[Presentation Layer]
CustomPainter / InteractiveViewer / Responsive Shell
│
▼
[Application State Layer]
BLoC / Signals / Drag-and-Drop Pipeline
│
▼
[Domain Layer]
MTOE Rules Engine / Structural Validation / Entities
│
▼
[Infrastructure Layer]
Drift SQLite / IndexedDB / Protobuf Codecs / Delta Sync
```
---
## Technology Stack
| Layer | Technology |
|---|---|
| UI Framework | Flutter 3.x (Dart 3.x) |
| Rendering | CustomPainter + InteractiveViewer |
| State Management | BLoC (flutter_bloc) |
| Drag-and-Drop | Flutter DragTarget / Draggable |
| Local Database | Drift v2 (SQLite / IndexedDB) — schema v2 |
| Web DB Worker | drift_worker.js + sqlite3.wasm |
| Encryption | SQLCipher *(planned)* |
| Symbology (Web) | milsymbol.js via dart:js_interop *(planned)* |
| Symbology (Native) | path_drawing + SVG assets + LRU cache *(planned)* |
| Background Compute | Dart Isolates (IsolateValidationRunner) |
| Serialization | JSON (UnitTreeCodec) + Binary (ProtobufCodec) |
| PDF Export | Pure-Dart PDF 1.4 (PdfChartExporter) |
| Documentation | MkDocs + Material theme |
---
## Development Setup
```bash
# Clone the repository
git clone https://git.kellsupport.com/uto_manager.git
cd uto_manager
# Install Flutter dependencies
flutter pub get
# Run static analysis
flutter analyze
# Run all tests with coverage
flutter test --coverage
# Install documentation dependencies
pip install -r docs/requirements.txt
# Serve documentation locally
mkdocs serve
```

View File

@ -1,72 +1,72 @@
# UTO Manager Documentation
## Flutter-Based Military Unit Task Organization Enterprise System
### U.S. Army 2026 Doctrine — Cross-Platform Command & Control Application
---
Welcome to the **UTO Manager** documentation. Use the navigation tabs above to find what you need.
---
## Documentation Sections
<div class="grid cards" markdown>
- :material-book-open-variant:{ .lg .middle } **User Guide**
---
Step-by-step instructions for planners and operators using UTO Manager to build, visualize, and validate unit task organizations.
[:octicons-arrow-right-24: User Guide](user-guide/index.md)
- :material-code-braces:{ .lg .middle } **Developer Guide**
---
Architecture deep-dives, API references, and contribution guidelines for engineers building and extending the platform.
[:octicons-arrow-right-24: Developer Guide](developer-guide/index.md)
- :material-shield-star:{ .lg .middle } **Doctrinal Reference**
---
U.S. Army 2026 force structure specifications, MOS reference tables, and MIL-STD-2525D symbology reference.
[:octicons-arrow-right-24: Doctrinal Reference](doctrinal-reference/index.md)
- :material-note-text:{ .lg .middle } **Release Notes**
---
Version history, feature summaries, known limitations, and upgrade instructions.
[:octicons-arrow-right-24: Release Notes](release-notes/index.md)
</div>
---
## Quick Links
| Resource | Description |
|---|---|
| [Installation](user-guide/getting-started/installation.md) | Set up UTO Manager on Windows, Web, or Android |
| [Quick Start](user-guide/getting-started/quick-start.md) | Create your first unit task organization in 5 minutes |
| [MBCT Template](user-guide/templates/mbct.md) | Mobile Brigade Combat Team template guide |
| [Architecture Overview](developer-guide/architecture/overview.md) | Clean architecture and layered design |
| [MOS Reference](doctrinal-reference/mos/infantry.md) | Military Occupational Specialty reference tables |
| [MIL-STD-2525D](doctrinal-reference/symbology/overview.md) | Tactical symbology reference |
---
## About This Project
UTO Manager is a cross-platform C2 application built with **Flutter** targeting Windows x64, Web (WASM/CanvasKit), and Android from a single Dart codebase. The system enforces U.S. Army MTOE doctrinal rules in real time, supporting formations from individual soldier positions up to multi-domain Brigade Combat Teams.
**Current Version:** Pre-release (Phase 1 — Architecture & Data Foundations)
**Project Repository:** [git.kellsupport.com/uto_manager](https://git.kellsupport.com/uto_manager)
# UTO Manager Documentation
## Flutter-Based Military Unit Task Organization Enterprise System
### U.S. Army 2026 Doctrine — Cross-Platform Command & Control Application
---
Welcome to the **UTO Manager** documentation. Use the navigation tabs above to find what you need.
---
## Documentation Sections
<div class="grid cards" markdown>
- :material-book-open-variant:{ .lg .middle } **User Guide**
---
Step-by-step instructions for planners and operators using UTO Manager to build, visualize, and validate unit task organizations.
[:octicons-arrow-right-24: User Guide](user-guide/index.md)
- :material-code-braces:{ .lg .middle } **Developer Guide**
---
Architecture deep-dives, API references, and contribution guidelines for engineers building and extending the platform.
[:octicons-arrow-right-24: Developer Guide](developer-guide/index.md)
- :material-shield-star:{ .lg .middle } **Doctrinal Reference**
---
U.S. Army 2026 force structure specifications, MOS reference tables, and MIL-STD-2525D symbology reference.
[:octicons-arrow-right-24: Doctrinal Reference](doctrinal-reference/index.md)
- :material-note-text:{ .lg .middle } **Release Notes**
---
Version history, feature summaries, known limitations, and upgrade instructions.
[:octicons-arrow-right-24: Release Notes](release-notes/index.md)
</div>
---
## Quick Links
| Resource | Description |
|---|---|
| [Installation](user-guide/getting-started/installation.md) | Set up UTO Manager on Windows, Web, or Android |
| [Quick Start](user-guide/getting-started/quick-start.md) | Create your first unit task organization in 5 minutes |
| [MBCT Template](user-guide/templates/mbct.md) | Mobile Brigade Combat Team template guide |
| [Architecture Overview](developer-guide/architecture/overview.md) | Clean architecture and layered design |
| [MOS Reference](doctrinal-reference/mos/infantry.md) | Military Occupational Specialty reference tables |
| [MIL-STD-2525D](doctrinal-reference/symbology/overview.md) | Tactical symbology reference |
---
## About This Project
UTO Manager is a cross-platform C2 application built with **Flutter** targeting Windows x64, Web (WASM/CanvasKit), and Android from a single Dart codebase. The system enforces U.S. Army MTOE doctrinal rules in real time, supporting formations from individual soldier positions up to multi-domain Brigade Combat Teams.
**Current Version:** Pre-release (Phases 1–5 complete — UI/CRUD Layer & Web Platform)
**Project Repository:** [git.kellsupport.com/uto_manager](https://git.kellsupport.com/uto_manager)

View File

@ -1,21 +1,22 @@
# Release Notes
## UTO Manager Version History
---
| Version | Date | Status |
|---|---|---|
| [v1.0.0](v1.0.0.md) | TBD | 🔵 In Development |
---
## Roadmap
| Milestone | Target | Deliverable |
|---|---|---|
| M1 — End of Week 4 | TBD | Compilable multi-platform skeleton + Drift DB + CI pipeline |
| M2 — End of Week 9 | TBD | Functional canvas engine with drag-and-drop on Windows and Android |
| M3 — End of Week 14 | TBD | All BCT templates instantiable + MIL-STD-2525D symbols rendering |
| M4 — End of Week 19 | TBD | Full persistence + real-time MTOE validation + JSON/PDF export |
| M5 — End of Week 24 | TBD | Signed release packages for Windows, Web, and Android |
# Release Notes
## UTO Manager Version History
---
| Version | Date | Status |
|---|---|---|
| [v1.0.0](v1.0.0.md) | TBD | 🔵 In Development |
---
## Roadmap
| Milestone | Target | Deliverable |
|---|---|---|
| M1 — End of Week 4 | 2026-08-07 ✅ | Compilable multi-platform skeleton + Drift DB + domain entities |
| M2 — End of Week 9 | 2026-08-08 ✅ | Functional canvas engine with drag-and-drop on Windows and Android |
| M3 — End of Week 14 | 2026-08-09 ✅ | All BCT templates instantiable + MTOE validation engine |
| M4 — End of Week 19 | 2026-08-09 ✅ | Full persistence + real-time MTOE validation + JSON/PDF/binary export |
| M5 — End of Week 24 | 2026-08-09 ✅ | Full CRUD UI + web platform support + DB schema v2 |
| M6 — Hardening | TBD | MIL-STD-2525D symbology, SQLCipher encryption, signed release packages |

View File

@ -1,59 +1,112 @@
# v1.0.0 — Initial Release
## Status: 🔵 In Development
**Target:** End of Week 24
---
## Planned Features
### Core Platform
- Cross-platform Flutter application targeting Windows x64, Web (WASM), and Android
- GPU-accelerated `CustomPainter` canvas engine with viewport culling
- 60 FPS rendering at 5,000+ nodes (Windows), 2,000+ (Web), 1,500+ (Android)
### Organizational Modeling
- Complete domain model: `UnitNode`, `PositionSlot`, `PersonnelRecord`
- MOS validation across all functional branches (Infantry, Medical, Fires, Signal, Cyber/EW, Aviation, Logistics, Engineer)
- 2026 doctrinal templates: MBCT, SBCT, ABCT, CAB-Heavy, CAB-Light
### Visual Canvas
- Reingold-Tilford hierarchical tree layout algorithm
- Quadtree spatial index for hit-testing and drag proximity queries
- Cross-platform drag-and-drop re-parenting with doctrinal validation highlights
- MIL-STD-2525D tactical symbology rendering
### Persistence & Validation
- Drift/SQLite offline-first persistence with IndexedDB on Web
- Real-time MTOE validation engine (MOS mismatch, rank mismatch, vacant leadership, structural violations)
- Delta synchronization for contested network environments
- SQLCipher data-at-rest encryption
### Export
- JSON export/import with doctrinal validation
- Protobuf binary export
- Vector PDF organizational chart export
### Platform-Specific
- Windows: multi-window, high-DPI scaling, `.msi` installer, `.uto` file association
- Web: PWA offline capability, IndexedDB quota management
- Android: battery profiling, touch-optimized targets (48×48 dp minimum)
---
## Known Limitations
*To be documented upon release.*
---
## Upgrade Instructions
*Initial release — no upgrade path required.*
# v1.0.0 — Initial Release
## Status: 🔵 In Development
**Target:** End of hardening phase
---
## Implemented Features (Phases 1–5)
### Core Platform
- Cross-platform Flutter application targeting Windows x64, Web (WASM), and Android
- GPU-accelerated `CustomPainter` canvas engine with viewport culling
- Three-pane adaptive layout: personnel roster | org chart canvas | unit inspector
- Offline-first Drift/SQLite persistence with IndexedDB on Web (schema v2)
- Web platform: `drift_worker.js` + `sqlite3.wasm` for full WASM/IndexedDB support
### Organizational Modeling
- Complete domain model: `UnitNode`, `PositionSlot`, `PersonnelRecord`, `SoldierPosition`
- MOS validation across all functional branches (Infantry, Medical, Fires, Signal, Cyber/EW, Aviation, Logistics, Engineer)
- 2026 doctrinal templates: MBCT, SBCT, ABCT, CAB-Heavy, CAB-Light
- 31 built-in Army duty positions (Command/Staff, Platoon, Squad, Individual) + user-defined custom positions
### Full CRUD User Interface
- **Add/Edit/Delete Units** — `AddEditUnitDialog` with echelon, branch, parent, and key-leader fields
- **Add/Edit/Delete Position Slots** — `AddEditSlotDialog` with MOS autocomplete and key-leader toggle
- **Add/Edit/Delete Soldiers** — `AddEditPersonnelDialog` with position picker, MOS autocomplete, pay grade, and readiness state
- **Custom Position Management** — create, rename, and delete custom duty positions from within the personnel dialog
- **Clear All** — confirmation-gated reset of all units, slots, and assignments
### Visual Canvas
- `InteractiveViewer` pan/zoom org chart
- Drag-and-drop soldier assignment (drag from personnel panel → drop on position slot card)
- Drag-and-drop unit re-parenting with doctrinal validation highlights
- Validation node indicators: red border for critical findings, amber for warnings
### MTOE Validation Engine
- 7 validation rules (V001–V007): MOS mismatch, rank mismatch, vacant key leader, missing 68W medic, missing 13F FSO, structural hierarchy violation, manning below 70%
- Background Dart Isolate execution via `IsolateValidationRunner`
- Findings displayed in inspector panel (per-unit, color-coded by severity) and status bar
### Persistence & Sync
- Drift schema v2: `units`, `position_slots`, `personnel`, `soldier_positions`, `templates`
- Schema migration v1→v2: adds `position_title` to `Personnel`, creates `soldier_positions` table
- `CommandEventStore` — append-only event log for all structural mutations
- `DeltaSynchronizer` — offline-first sync helper (simulated transport; real network deferred)
### Export
- **JSON export/import** — full unit tree with schema versioning and round-trip fidelity
- **Binary export (`.utob`)** — `ProtobufCodec` with 12-byte UTOB header + zlib-compressed JSON
- **PDF export** — pure-Dart PDF 1.4 org-chart generator (Letter/A4/Tabloid, UNCLASSIFIED banner)
- Export dialog with format selection and PDF page-size picker
---
## Planned Features (Hardening Phase)
### Visual Symbology
- MIL-STD-2525D tactical symbology rendering (hybrid: `milsymbol.js` on Web, SVG path engine on native)
### Security
- SQLCipher data-at-rest encryption for local Drift database
- Platform keychain key storage (Windows Credential Manager / Android Keystore)
### Platform Hardening
- Windows: multi-window support, `.msi` installer, `.uto` file association
- Web: PWA offline capability, IndexedDB quota management, service worker
- Android: battery profiling, touch-optimized targets (48×48 dp minimum)
### Performance
- Reingold-Tilford layout algorithm in background Dart Isolate
- Quadtree spatial index for hit-testing
- `RepaintBoundary` layer isolation
- 60 FPS at 5,000+ nodes (Windows), 2,000+ (Web), 1,500+ (Android)
### UX
- Undo/redo stack for all CRUD operations
- Canvas search/filter
- Keyboard shortcuts (Ctrl+Z, Ctrl+F, Ctrl+D, Ctrl+S)
---
## Known Limitations
- MIL-STD-2525D symbology not yet rendered (nodes show text labels only)
- No data-at-rest encryption (SQLCipher deferred)
- `DeltaSynchronizer` uses simulated online/offline; no real network transport
- `ProtobufCodec` uses zlib-compressed JSON payload; true `.proto` schema deferred
- Template instantiation uses sequential Drift inserts (not a single transaction)
- No undo/redo for CRUD operations
- Canvas layout is vertical tree only; horizontal layout not implemented
- No canvas search/filter
---
## Upgrade Instructions
*Initial release — no upgrade path required.*
*Existing databases at schema v1 will be automatically migrated to v2 on first launch.*

View File

@ -52,7 +52,7 @@ class PersonnelLoaded extends PersonnelState {
if (filterQuery.isEmpty) return unassigned;
final q = filterQuery.toLowerCase();
return unassigned.where((p) {
return p.name.toLowerCase().contains(q) ||
return p.displayLabel.toLowerCase().contains(q) ||
p.primaryMos.code.toLowerCase().contains(q) ||
p.payGrade.payGrade.toLowerCase().contains(q);
}).toList();
@ -187,6 +187,35 @@ class PersonnelCubit extends Cubit<PersonnelState> {
}
}
/// 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();

View File

@ -142,8 +142,14 @@ class UnitTreeCubit extends Cubit<UnitTreeState> {
final roots = treeService.buildTree(_units, _slots);
final currentSelection =
state is UnitTreeLoaded ? (state as UnitTreeLoaded).selectedUnitId : null;
emit(UnitTreeLoaded(
units: _units, slots: _slots, roots: roots, selectedUnitId: currentSelection,),);
emit(
UnitTreeLoaded(
units: _units,
slots: _slots,
roots: roots,
selectedUnitId: currentSelection,
),
);
}
// ---------------------------------------------------------------------------
@ -206,6 +212,42 @@ class UnitTreeCubit extends Cubit<UnitTreeState> {
}
}
/// Updates an existing [unit] in the tree.
Future<void> updateUnit(UnitNode unit) async {
try {
await unitRepository.update(unit);
} catch (e) {
emit(UnitTreeError(e.toString()));
}
}
/// Adds a new position [slot] to a unit.
Future<void> addSlot(PositionSlot slot) async {
try {
await slotRepository.insert(slot);
} catch (e) {
emit(UnitTreeError(e.toString()));
}
}
/// Updates an existing position [slot].
Future<void> updateSlot(PositionSlot slot) async {
try {
await slotRepository.update(slot);
} catch (e) {
emit(UnitTreeError(e.toString()));
}
}
/// Deletes the position slot with [slotId].
Future<void> deleteSlot(String slotId) async {
try {
await slotRepository.delete(slotId);
} catch (e) {
emit(UnitTreeError(e.toString()));
}
}
/// Clears all units and slots (template reset).
Future<void> clearAll() async {
try {

View File

@ -36,13 +36,17 @@ enum ReadinessState {
///
/// Contains identifying information, MOS qualifications, pay grade,
/// and current readiness state.
///
/// [name] is optional. When omitted the soldier is identified by
/// [positionTitle] (e.g. "Team Leader", "Rifleman").
class PersonnelRecord extends Equatable {
const PersonnelRecord({
required this.soldierId,
required this.name,
required this.primaryMos,
required this.payGrade,
required this.readinessState,
this.name,
this.positionTitle,
this.qualifications = const [],
this.assignedSlotId,
});
@ -50,8 +54,13 @@ class PersonnelRecord extends Equatable {
/// Unique identifier for this soldier (UUID v4).
final String soldierId;
/// The soldier's full name (Last, First MI format).
final String name;
/// The soldier's full name (Last, First MI format), or null if unnamed.
final String? name;
/// The soldier's duty position title (e.g. "Team Leader", "Rifleman").
///
/// Used as the display label when [name] is null.
final String? positionTitle;
/// The soldier's primary Military Occupational Specialty.
final DutyMos primaryMos;
@ -68,6 +77,15 @@ class PersonnelRecord extends Equatable {
/// The ID of the position slot this soldier is assigned to, or null.
final String? assignedSlotId;
/// Returns the best available display label for this soldier.
///
/// Prefers [name]; falls back to [positionTitle]; then a generic label.
String get displayLabel {
if (name != null && name!.isNotEmpty) return name!;
if (positionTitle != null && positionTitle!.isNotEmpty) return positionTitle!;
return '(Unnamed Soldier)';
}
/// Returns true if this soldier is currently assigned to a position.
bool get isAssigned => assignedSlotId != null;
@ -81,6 +99,9 @@ class PersonnelRecord extends Equatable {
PersonnelRecord copyWith({
String? soldierId,
String? name,
bool clearName = false,
String? positionTitle,
bool clearPositionTitle = false,
DutyMos? primaryMos,
AuthorizedRank? payGrade,
List<String>? qualifications,
@ -90,7 +111,8 @@ class PersonnelRecord extends Equatable {
}) {
return PersonnelRecord(
soldierId: soldierId ?? this.soldierId,
name: name ?? this.name,
name: clearName ? null : (name ?? this.name),
positionTitle: clearPositionTitle ? null : (positionTitle ?? this.positionTitle),
primaryMos: primaryMos ?? this.primaryMos,
payGrade: payGrade ?? this.payGrade,
qualifications: qualifications ?? this.qualifications,
@ -103,6 +125,7 @@ class PersonnelRecord extends Equatable {
List<Object?> get props => [
soldierId,
name,
positionTitle,
primaryMos,
payGrade,
qualifications,
@ -111,7 +134,7 @@ class PersonnelRecord extends Equatable {
];
@override
String toString() => 'PersonnelRecord(id: $soldierId, name: $name, '
String toString() => 'PersonnelRecord(id: $soldierId, label: $displayLabel, '
'mos: ${primaryMos.code}, grade: ${payGrade.payGrade}, '
'readiness: ${readinessState.displayName})';
}

View File

@ -0,0 +1,108 @@
import 'package:equatable/equatable.dart';
/// A named duty position that can be assigned to a soldier record.
///
/// Built-in positions are seeded at first launch and cannot be deleted.
/// Users may add, rename, or delete custom positions.
class SoldierPosition extends Equatable {
const SoldierPosition({
required this.positionId,
required this.title,
this.isBuiltIn = false,
});
/// Unique identifier (UUID v4 for custom; fixed string for built-ins).
final String positionId;
/// Human-readable position title (e.g. "Team Leader", "Rifleman").
final String title;
/// True for positions seeded by the app; false for user-created ones.
final bool isBuiltIn;
SoldierPosition copyWith({
String? positionId,
String? title,
bool? isBuiltIn,
}) {
return SoldierPosition(
positionId: positionId ?? this.positionId,
title: title ?? this.title,
isBuiltIn: isBuiltIn ?? this.isBuiltIn,
);
}
@override
List<Object?> get props => [positionId, title, isBuiltIn];
@override
String toString() => 'SoldierPosition($positionId, "$title", builtIn: $isBuiltIn)';
// ---------------------------------------------------------------------------
// Built-in positions
// ---------------------------------------------------------------------------
/// The full list of built-in Army duty positions seeded on first launch.
static const List<SoldierPosition> builtIns = [
// ── Command / Staff ──────────────────────────────────────────────────────
SoldierPosition(positionId: 'bi_commander', title: 'Commander', isBuiltIn: true),
SoldierPosition(
positionId: 'bi_executive_officer',
title: 'Executive Officer',
isBuiltIn: true,
),
SoldierPosition(
positionId: 'bi_command_sergeant_major',
title: 'Command Sergeant Major',
isBuiltIn: true,
),
SoldierPosition(positionId: 'bi_first_sergeant', title: 'First Sergeant', isBuiltIn: true),
SoldierPosition(
positionId: 'bi_operations_officer',
title: 'S3 / Operations Officer',
isBuiltIn: true,
),
SoldierPosition(
positionId: 'bi_intelligence_officer',
title: 'S2 / Intelligence Officer',
isBuiltIn: true,
),
SoldierPosition(
positionId: 'bi_logistics_officer',
title: 'S4 / Logistics Officer',
isBuiltIn: true,
),
SoldierPosition(positionId: 'bi_signal_officer', title: 'S6 / Signal Officer', isBuiltIn: true),
// ── Platoon ───────────────────────────────────────────────────────────────
SoldierPosition(positionId: 'bi_platoon_leader', title: 'Platoon Leader', isBuiltIn: true),
SoldierPosition(positionId: 'bi_platoon_sergeant', title: 'Platoon Sergeant', isBuiltIn: true),
SoldierPosition(positionId: 'bi_rto', title: 'RTO / Radio Operator', isBuiltIn: true),
// ── Squad / Section ───────────────────────────────────────────────────────
SoldierPosition(positionId: 'bi_squad_leader', title: 'Squad Leader', isBuiltIn: true),
SoldierPosition(positionId: 'bi_team_leader', title: 'Team Leader', isBuiltIn: true),
// ── Individual ────────────────────────────────────────────────────────────
SoldierPosition(positionId: 'bi_rifleman', title: 'Rifleman', isBuiltIn: true),
SoldierPosition(positionId: 'bi_grenadier', title: 'Grenadier', isBuiltIn: true),
SoldierPosition(
positionId: 'bi_automatic_rifleman',
title: 'Automatic Rifleman',
isBuiltIn: true,
),
SoldierPosition(positionId: 'bi_machine_gunner', title: 'Machine Gunner', isBuiltIn: true),
SoldierPosition(positionId: 'bi_sniper', title: 'Sniper', isBuiltIn: true),
SoldierPosition(positionId: 'bi_jtac', title: 'JTAC / Fire Support', isBuiltIn: true),
SoldierPosition(positionId: 'bi_medic', title: 'Medic', isBuiltIn: true),
SoldierPosition(positionId: 'bi_combat_engineer', title: 'Combat Engineer', isBuiltIn: true),
SoldierPosition(positionId: 'bi_driver', title: 'Driver', isBuiltIn: true),
SoldierPosition(positionId: 'bi_gunner', title: 'Gunner', isBuiltIn: true),
SoldierPosition(positionId: 'bi_loader', title: 'Loader', isBuiltIn: true),
SoldierPosition(positionId: 'bi_tank_commander', title: 'Tank Commander', isBuiltIn: true),
SoldierPosition(positionId: 'bi_crew_member', title: 'Crew Member', isBuiltIn: true),
SoldierPosition(positionId: 'bi_supply_sergeant', title: 'Supply Sergeant', isBuiltIn: true),
SoldierPosition(positionId: 'bi_armorer', title: 'Armorer', isBuiltIn: true),
SoldierPosition(positionId: 'bi_commo_sergeant', title: 'Commo Sergeant', isBuiltIn: true),
SoldierPosition(positionId: 'bi_intel_analyst', title: 'Intelligence Analyst', isBuiltIn: true),
SoldierPosition(positionId: 'bi_forward_observer', title: 'Forward Observer', isBuiltIn: true),
SoldierPosition(positionId: 'bi_chemical_officer', title: 'CBRN Specialist', isBuiltIn: true),
];
}

View File

@ -74,6 +74,9 @@ class DutyMos {
return _knownMosCodes.contains(code.trim().toUpperCase());
}
/// Returns all known MOS codes as a sorted list.
static List<String> get allCodes => _knownMosCodes.toList()..sort();
/// Returns all MOS codes for a given [DoctrinalBranch].
static Set<String> codesForBranch(DoctrinalBranch branch) {
return _branchMosMap[branch] ?? {};

View File

@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:uto_manager/infrastructure/database/daos/personnel_dao.dart';
import 'package:uto_manager/infrastructure/database/daos/position_slot_dao.dart';
import 'package:uto_manager/infrastructure/database/daos/soldier_positions_dao.dart';
import 'package:uto_manager/infrastructure/database/daos/template_dao.dart';
import 'package:uto_manager/infrastructure/database/daos/unit_dao.dart';
@ -41,7 +42,8 @@ class PositionSlots extends Table {
/// Drift table for soldier personnel records.
class Personnel extends Table {
TextColumn get soldierId => text().named('soldier_id')();
TextColumn get name => text()();
TextColumn get name => text().nullable()();
TextColumn get positionTitle => text().named('position_title').nullable()();
TextColumn get primaryMos => text().named('primary_mos')();
TextColumn get payGrade => text().named('pay_grade')();
TextColumn get qualifications => text()(); // JSON-encoded list
@ -52,6 +54,16 @@ class Personnel extends Table {
Set<Column> get primaryKey => {soldierId};
}
/// Drift table for user-managed soldier duty positions.
class SoldierPositions extends Table {
TextColumn get positionId => text().named('position_id')();
TextColumn get title => text()();
BoolColumn get isBuiltIn => boolean().named('is_built_in').withDefault(const Constant(false))();
@override
Set<Column> get primaryKey => {positionId};
}
/// Drift table for serialized unit tree templates.
class Templates extends Table {
TextColumn get templateId => text().named('template_id')();
@ -71,11 +83,11 @@ class Templates extends Table {
/// The main Drift database for the UTO Manager application.
///
/// Manages all four core tables: units, position_slots, personnel, templates.
/// Manages all core tables: units, position_slots, personnel, soldier_positions, templates.
/// Supports SQLite on Windows/Android and IndexedDB on Web via drift_web.
@DriftDatabase(
tables: [Units, PositionSlots, Personnel, Templates],
daos: [UnitDao, PersonnelDao, PositionSlotDao, TemplateDao],
tables: [Units, PositionSlots, Personnel, SoldierPositions, Templates],
daos: [UnitDao, PersonnelDao, PositionSlotDao, SoldierPositionsDao, TemplateDao],
)
class AppDatabase extends _$AppDatabase {
/// Creates the production database backed by SQLite on disk.
@ -85,7 +97,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase.forTesting(super.executor);
@override
int get schemaVersion => 1;
int get schemaVersion => 2;
@override
MigrationStrategy get migration => MigrationStrategy(
@ -93,7 +105,12 @@ class AppDatabase extends _$AppDatabase {
await m.createAll();
},
onUpgrade: (m, from, to) async {
// Future migration steps added here as schemaVersion increases.
if (from < 2) {
// v1 → v2: make Personnel.name nullable, add position_title column,
// and create the new soldier_positions table.
await m.addColumn(personnel, personnel.positionTitle);
await m.createTable(soldierPositions);
}
},
);
}
@ -102,5 +119,11 @@ class AppDatabase extends _$AppDatabase {
///
/// Uses SQLite via FFI on Windows/Android and WASM/IndexedDB on Web.
QueryExecutor _openConnection() {
return driftDatabase(name: 'uto_manager');
return driftDatabase(
name: 'uto_manager',
web: DriftWebOptions(
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
driftWorker: Uri.parse('drift_worker.js'),
),
);
}

View File

@ -0,0 +1,41 @@
import 'package:drift/drift.dart';
import 'package:uto_manager/infrastructure/database/app_database.dart';
part 'soldier_positions_dao.g.dart';
// The Drift-generated row type for the SoldierPositions table is also named
// `SoldierPosition` (same as our domain entity). We use a typedef here so the
// DAO can refer to the DB row type unambiguously.
typedef SoldierPositionRow = SoldierPosition;
/// Data Access Object for [SoldierPositions] table operations.
@DriftAccessor(tables: [SoldierPositions])
class SoldierPositionsDao extends DatabaseAccessor<AppDatabase> with _$SoldierPositionsDaoMixin {
SoldierPositionsDao(super.db);
/// Returns all soldier position rows.
Future<List<SoldierPositionRow>> getAll() => select(soldierPositions).get();
/// Returns the position with [positionId], or null if not found.
Future<SoldierPositionRow?> getById(String positionId) =>
(select(soldierPositions)..where((t) => t.positionId.equals(positionId))).getSingleOrNull();
/// Inserts a new position row (insert-or-replace).
Future<void> upsert(SoldierPositionsCompanion entry) =>
into(soldierPositions).insertOnConflictUpdate(entry);
/// Inserts multiple rows in a single transaction.
Future<void> upsertAll(List<SoldierPositionsCompanion> entries) =>
batch((b) => b.insertAllOnConflictUpdate(soldierPositions, entries));
/// Updates an existing position row.
Future<bool> updatePosition(SoldierPositionsCompanion entry) =>
update(soldierPositions).replace(entry);
/// Deletes the position with [positionId].
Future<int> deletePosition(String positionId) =>
(delete(soldierPositions)..where((t) => t.positionId.equals(positionId))).go();
/// Returns a stream that emits all positions whenever the table changes.
Stream<List<SoldierPositionRow>> watchAll() => select(soldierPositions).watch();
}

View File

@ -26,6 +26,7 @@ class DriftPersonnelRepository implements PersonnelRepository {
return domain.PersonnelRecord(
soldierId: row.soldierId,
name: row.name,
positionTitle: row.positionTitle,
primaryMos: DutyMos.unvalidated(row.primaryMos),
payGrade: AuthorizedRank.values.firstWhere((r) => r.name == row.payGrade),
qualifications: (jsonDecode(row.qualifications) as List<dynamic>).cast<String>(),
@ -39,7 +40,8 @@ class DriftPersonnelRepository implements PersonnelRepository {
PersonnelCompanion _toCompanion(domain.PersonnelRecord record) {
return PersonnelCompanion.insert(
soldierId: record.soldierId,
name: record.name,
name: Value(record.name),
positionTitle: Value(record.positionTitle),
primaryMos: record.primaryMos.code,
payGrade: record.payGrade.name,
qualifications: jsonEncode(record.qualifications),

View File

@ -0,0 +1,77 @@
import 'package:drift/drift.dart' show Value;
import 'package:uto_manager/domain/entities/soldier_position.dart' as domain;
import 'package:uto_manager/infrastructure/database/app_database.dart' as db_lib;
import 'package:uto_manager/infrastructure/database/daos/soldier_positions_dao.dart';
/// Drift-backed repository for [domain.SoldierPosition] persistence.
///
/// Converts between Drift [SoldierPositionRow] rows and domain
/// [domain.SoldierPosition] entities. All persistence is handled via
/// [SoldierPositionsDao].
class DriftSoldierPositionsRepository {
const DriftSoldierPositionsRepository({required this.dao});
final SoldierPositionsDao dao;
// ---------------------------------------------------------------------------
// Mapping helpers
// ---------------------------------------------------------------------------
domain.SoldierPosition _toDomain(SoldierPositionRow row) {
return domain.SoldierPosition(
positionId: row.positionId,
title: row.title,
isBuiltIn: row.isBuiltIn,
);
}
db_lib.SoldierPositionsCompanion _toCompanion(domain.SoldierPosition pos) {
return db_lib.SoldierPositionsCompanion(
positionId: Value(pos.positionId),
title: Value(pos.title),
isBuiltIn: Value(pos.isBuiltIn),
);
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/// Returns all positions (built-in + custom).
Future<List<domain.SoldierPosition>> getAll() async {
final rows = await dao.getAll();
return rows.map(_toDomain).toList();
}
/// Returns a stream that emits all positions whenever the table changes.
Stream<List<domain.SoldierPosition>> watchAll() {
return dao.watchAll().map((rows) => rows.map(_toDomain).toList());
}
/// Inserts or updates a position.
Future<void> upsert(domain.SoldierPosition position) async {
await dao.upsert(_toCompanion(position));
}
/// Seeds all [domain.SoldierPosition.builtIns] that are not yet in the database.
Future<void> seedBuiltIns() async {
final existing = await dao.getAll();
final existingIds = existing.map((SoldierPositionRow r) => r.positionId).toSet();
final toInsert = domain.SoldierPosition.builtIns
.where((p) => !existingIds.contains(p.positionId))
.map(_toCompanion)
.toList();
if (toInsert.isNotEmpty) {
await dao.upsertAll(toInsert);
}
}
/// Deletes a custom position by [positionId].
///
/// Built-in positions cannot be deleted; this is a no-op for them.
Future<void> delete(String positionId) async {
final row = await dao.getById(positionId);
if (row == null || row.isBuiltIn) return;
await dao.deletePosition(positionId);
}
}

View File

@ -9,6 +9,7 @@ import 'package:uto_manager/domain/services/unit_tree_service.dart';
import 'package:uto_manager/infrastructure/database/app_database.dart';
import 'package:uto_manager/infrastructure/repositories/drift_personnel_repository.dart';
import 'package:uto_manager/infrastructure/repositories/drift_position_slot_repository.dart';
import 'package:uto_manager/infrastructure/repositories/drift_soldier_positions_repository.dart';
import 'package:uto_manager/infrastructure/repositories/drift_unit_repository.dart';
import 'package:uto_manager/presentation/screens/home_screen.dart';
@ -32,6 +33,7 @@ class _UtoManagerAppState extends State<UtoManagerApp> {
late final DriftUnitRepository _unitRepo;
late final DriftPersonnelRepository _personnelRepo;
late final DriftPositionSlotRepository _slotRepo;
late final DriftSoldierPositionsRepository _positionsRepo;
late final UnitTreeService _treeService;
late final AssignmentService _assignmentService;
@ -42,6 +44,7 @@ class _UtoManagerAppState extends State<UtoManagerApp> {
_unitRepo = DriftUnitRepository(dao: _db.unitDao, db: _db);
_personnelRepo = DriftPersonnelRepository(dao: _db.personnelDao);
_slotRepo = DriftPositionSlotRepository(dao: _db.positionSlotDao);
_positionsRepo = DriftSoldierPositionsRepository(dao: _db.soldierPositionsDao);
_treeService = const UnitTreeService();
_assignmentService = AssignmentService(treeService: _treeService);
}
@ -91,7 +94,7 @@ class _UtoManagerAppState extends State<UtoManagerApp> {
),
useMaterial3: true,
),
home: const HomeScreen(),
home: HomeScreen(positionsRepository: _positionsRepo),
),
);
}

View File

@ -0,0 +1,404 @@
import 'package:flutter/material.dart';
import 'package:uto_manager/domain/entities/authorized_rank.dart';
import 'package:uto_manager/domain/entities/personnel_record.dart';
import 'package:uto_manager/domain/entities/soldier_position.dart';
import 'package:uto_manager/domain/value_objects/duty_mos.dart';
import 'package:uto_manager/infrastructure/repositories/drift_soldier_positions_repository.dart';
import 'package:uuid/uuid.dart';
/// Dialog for creating or editing a [PersonnelRecord].
///
/// Pass [existing] to edit; leave null to create a new record.
/// [positionsRepository] is used to load and manage the position list.
class AddEditPersonnelDialog extends StatefulWidget {
const AddEditPersonnelDialog({
required this.positionsRepository,
super.key,
this.existing,
});
final DriftSoldierPositionsRepository positionsRepository;
final PersonnelRecord? existing;
@override
State<AddEditPersonnelDialog> createState() => _AddEditPersonnelDialogState();
}
class _AddEditPersonnelDialogState extends State<AddEditPersonnelDialog> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nameCtrl;
late final TextEditingController _mosCtrl;
late AuthorizedRank _payGrade;
late ReadinessState _readiness;
/// The currently selected position, or null if none chosen.
SoldierPosition? _selectedPosition;
/// All positions loaded from the repository.
List<SoldierPosition> _positions = [];
List<String> _mosSuggestions = [];
bool get _isEditing => widget.existing != null;
@override
void initState() {
super.initState();
final e = widget.existing;
_nameCtrl = TextEditingController(text: e?.name ?? '');
_mosCtrl = TextEditingController(text: e?.primaryMos.code ?? '');
_payGrade = e?.payGrade ?? AuthorizedRank.e4;
_readiness = e?.readinessState ?? ReadinessState.fullyMissionCapable;
_loadPositions(existingTitle: e?.positionTitle);
}
@override
void dispose() {
_nameCtrl.dispose();
_mosCtrl.dispose();
super.dispose();
}
Future<void> _loadPositions({String? existingTitle}) async {
await widget.positionsRepository.seedBuiltIns();
final positions = await widget.positionsRepository.getAll();
positions.sort((a, b) => a.title.compareTo(b.title));
final selected =
existingTitle != null ? positions.where((p) => p.title == existingTitle).firstOrNull : null;
if (mounted) {
setState(() {
_positions = positions;
_selectedPosition = selected;
});
}
}
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 nameText = _nameCtrl.text.trim();
final record = PersonnelRecord(
soldierId: widget.existing?.soldierId ?? const Uuid().v4(),
name: nameText.isEmpty ? null : nameText,
positionTitle: _selectedPosition?.title,
primaryMos: mos,
payGrade: _payGrade,
readinessState: _readiness,
qualifications: widget.existing?.qualifications ?? const [],
assignedSlotId: widget.existing?.assignedSlotId,
);
Navigator.of(context).pop(record);
}
// ---------------------------------------------------------------------------
// Position CRUD helpers
// ---------------------------------------------------------------------------
Future<void> _showAddPositionDialog() async {
final title = await _showPositionTitleDialog(context, initial: '');
if (title == null || title.isEmpty) return;
final pos = SoldierPosition(positionId: const Uuid().v4(), title: title);
await widget.positionsRepository.upsert(pos);
await _loadPositions();
if (mounted) {
setState(() => _selectedPosition = _positions.where((p) => p.title == title).firstOrNull);
}
}
Future<void> _showEditPositionDialog(SoldierPosition pos) async {
if (pos.isBuiltIn) return; // built-ins cannot be renamed
final title = await _showPositionTitleDialog(context, initial: pos.title);
if (title == null || title.isEmpty || title == pos.title) return;
await widget.positionsRepository.upsert(pos.copyWith(title: title));
await _loadPositions();
if (mounted) {
setState(
() =>
_selectedPosition = _positions.where((p) => p.positionId == pos.positionId).firstOrNull,
);
}
}
Future<void> _confirmDeletePosition(SoldierPosition pos) async {
if (pos.isBuiltIn) return;
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Delete Position?'),
content: Text('Remove the "${pos.title}" position? This cannot be undone.'),
actions: [
TextButton(onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Cancel')),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: Colors.red.shade700),
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Delete'),
),
],
),
);
if (confirmed ?? false) {
await widget.positionsRepository.delete(pos.positionId);
await _loadPositions();
if (mounted && _selectedPosition?.positionId == pos.positionId) {
setState(() => _selectedPosition = null);
}
}
}
/// Shows a simple text-input dialog for a position title.
Future<String?> _showPositionTitleDialog(BuildContext context, {required String initial}) {
final ctrl = TextEditingController(text: initial);
return showDialog<String>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(initial.isEmpty ? 'Add Position' : 'Rename Position'),
content: TextField(
controller: ctrl,
autofocus: true,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(
labelText: 'Position Title',
hintText: 'e.g. Anti-Tank Gunner',
),
onSubmitted: (_) => Navigator.of(ctx).pop(ctrl.text.trim()),
),
actions: [
TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Cancel')),
FilledButton(
onPressed: () => Navigator.of(ctx).pop(ctrl.text.trim()),
child: const Text('Save'),
),
],
),
);
}
// ---------------------------------------------------------------------------
// Build
// ---------------------------------------------------------------------------
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AlertDialog(
title: Row(
children: [
Icon(_isEditing ? Icons.edit : Icons.person_add, size: 20),
const SizedBox(width: 8),
Text(_isEditing ? 'Edit Soldier' : 'Add Soldier'),
],
),
content: SizedBox(
width: 420,
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Position ─────────────────────────────────────────────────
_buildPositionRow(theme),
const SizedBox(height: 12),
// ── Name (optional) ──────────────────────────────────────────
TextFormField(
controller: _nameCtrl,
decoration: const InputDecoration(
labelText: 'Full Name (optional)',
hintText: 'e.g. SMITH, John A.',
prefixIcon: Icon(Icons.badge_outlined),
helperText: 'Leave blank to identify by position only.',
),
textCapitalization: TextCapitalization.characters,
),
const SizedBox(height: 12),
// ── Pay Grade ────────────────────────────────────────────────
DropdownButtonFormField<AuthorizedRank>(
initialValue: _payGrade,
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Pay Grade',
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(() => _payGrade = v!),
),
const SizedBox(height: 12),
// ── MOS with autocomplete ────────────────────────────────────
TextFormField(
controller: _mosCtrl,
decoration: const InputDecoration(
labelText: 'Primary MOS',
hintText: 'e.g. 11B, 68W, 25U',
prefixIcon: Icon(Icons.work_outline),
),
textCapitalization: TextCapitalization.characters,
onChanged: _updateMosSuggestions,
validator: (v) {
if (v == null || v.trim().isEmpty) return 'MOS is required';
final code = v.trim().toUpperCase();
if (!DutyMos.isKnown(code)) {
return 'Unknown MOS code. Use a valid Army MOS (e.g. 11B, 68W)';
}
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: 12),
// ── Readiness State ──────────────────────────────────────────
DropdownButtonFormField<ReadinessState>(
initialValue: _readiness,
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Readiness State',
prefixIcon: Icon(Icons.health_and_safety_outlined),
),
items: ReadinessState.values.map((s) {
return DropdownMenuItem(
value: s,
child: Text(s.displayName),
);
}).toList(),
onChanged: (v) => setState(() => _readiness = v!),
),
],
),
),
),
),
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 Soldier'),
onPressed: _submit,
),
],
);
}
/// Builds the position dropdown row with add / edit / delete action buttons.
Widget _buildPositionRow(ThemeData theme) {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: _positions.isEmpty
? const LinearProgressIndicator()
: DropdownButtonFormField<SoldierPosition>(
initialValue: _selectedPosition,
isExpanded: true,
decoration: const InputDecoration(
labelText: 'Position',
prefixIcon: Icon(Icons.work_history_outlined),
),
hint: const Text('Select position…'),
items: _positions.map((p) {
return DropdownMenuItem(
value: p,
child: Row(
children: [
Expanded(child: Text(p.title, overflow: TextOverflow.ellipsis)),
if (p.isBuiltIn)
Icon(
Icons.lock_outline,
size: 12,
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
),
],
),
);
}).toList(),
onChanged: (v) => setState(() => _selectedPosition = v),
),
),
// Add custom position
IconButton(
icon: const Icon(Icons.add_circle_outline, size: 18),
tooltip: 'Add custom position',
onPressed: _showAddPositionDialog,
),
// Edit selected position (only if custom)
if (_selectedPosition != null && !_selectedPosition!.isBuiltIn)
IconButton(
icon: const Icon(Icons.edit_outlined, size: 18),
tooltip: 'Rename position',
onPressed: () => _showEditPositionDialog(_selectedPosition!),
),
// Delete selected position (only if custom)
if (_selectedPosition != null && !_selectedPosition!.isBuiltIn)
IconButton(
icon: Icon(Icons.delete_outline, size: 18, color: theme.colorScheme.error),
tooltip: 'Delete position',
onPressed: () => _confirmDeletePosition(_selectedPosition!),
),
],
);
}
}

View File

@ -0,0 +1,221 @@
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,
),
],
);
}
}

View File

@ -0,0 +1,202 @@
import 'package:flutter/material.dart';
import 'package:uto_manager/domain/entities/doctrinal_branch.dart';
import 'package:uto_manager/domain/entities/echelon.dart';
import 'package:uto_manager/domain/entities/unit_node.dart';
import 'package:uuid/uuid.dart';
/// Dialog for creating or editing a [UnitNode].
///
/// Pass [existing] to edit; leave null to create a new unit.
/// Pass [parentUnit] to pre-select the parent and constrain the echelon.
class AddEditUnitDialog extends StatefulWidget {
const AddEditUnitDialog({
super.key,
this.existing,
this.parentUnit,
this.availableParents = const [],
});
final UnitNode? existing;
final UnitNode? parentUnit;
final List<UnitNode> availableParents;
@override
State<AddEditUnitDialog> createState() => _AddEditUnitDialogState();
}
class _AddEditUnitDialogState extends State<AddEditUnitDialog> {
final _formKey = GlobalKey<FormState>();
late final TextEditingController _nameCtrl;
late Echelon _echelon;
late DoctrinalBranch _branch;
String? _parentUnitId;
late bool _isKeyLeader;
bool get _isEditing => widget.existing != null;
@override
void initState() {
super.initState();
final e = widget.existing;
_nameCtrl = TextEditingController(text: e?.name ?? '');
_echelon = e?.echelon ?? _defaultChildEchelon();
_branch = e?.branch ?? DoctrinalBranch.infantry;
_parentUnitId = e?.parentUnitId ?? widget.parentUnit?.unitId;
_isKeyLeader = e?.isKeyLeader ?? false;
}
Echelon _defaultChildEchelon() {
final parent = widget.parentUnit;
if (parent == null) return Echelon.brigade;
// Default to one level below parent
final childLevel = parent.echelon.level + 1;
return Echelon.values.firstWhere(
(e) => e.level == childLevel,
orElse: () => Echelon.team,
);
}
@override
void dispose() {
_nameCtrl.dispose();
super.dispose();
}
void _submit() {
if (!_formKey.currentState!.validate()) return;
// If no name provided, generate a default from echelon + branch abbreviation
final rawName = _nameCtrl.text.trim();
final name = rawName.isEmpty ? '${_echelon.displayName} (${_branch.abbreviation})' : rawName;
final unit = UnitNode(
unitId: widget.existing?.unitId ?? const Uuid().v4(),
echelon: _echelon,
branch: _branch,
name: name,
isKeyLeader: _isKeyLeader,
parentUnitId: _parentUnitId,
);
Navigator.of(context).pop(unit);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Row(
children: [
Icon(_isEditing ? Icons.edit : Icons.add_box_outlined, size: 20),
const SizedBox(width: 8),
Text(_isEditing ? 'Edit Unit' : 'Add Unit'),
],
),
content: SizedBox(
width: 420,
child: Form(
key: _formKey,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Unit Name
TextFormField(
controller: _nameCtrl,
decoration: const InputDecoration(
labelText: 'Unit Name / Designation (optional)',
hintText: 'e.g. 1-68 AR, Alpha Company, 3rd Platoon',
helperText: 'Leave blank to auto-generate from echelon & branch',
prefixIcon: Icon(Icons.account_tree_outlined),
),
textCapitalization: TextCapitalization.words,
),
const SizedBox(height: 12),
// Echelon
DropdownButtonFormField<Echelon>(
initialValue: _echelon,
decoration: const InputDecoration(
labelText: 'Echelon',
prefixIcon: Icon(Icons.layers_outlined),
),
items: Echelon.values.map((e) {
return DropdownMenuItem(
value: e,
child: Text(e.displayName),
);
}).toList(),
onChanged: (v) => setState(() => _echelon = v!),
),
const SizedBox(height: 12),
// Branch
DropdownButtonFormField<DoctrinalBranch>(
initialValue: _branch,
decoration: const InputDecoration(
labelText: 'Branch / Functional Area',
prefixIcon: Icon(Icons.category_outlined),
),
items: DoctrinalBranch.values.map((b) {
return DropdownMenuItem(
value: b,
child: Text('${b.abbreviation} — ${b.displayName}'),
);
}).toList(),
onChanged: (v) => setState(() => _branch = v!),
),
const SizedBox(height: 12),
// Parent Unit (optional)
if (widget.availableParents.isNotEmpty) ...[
DropdownButtonFormField<String?>(
initialValue: _parentUnitId,
decoration: const InputDecoration(
labelText: 'Parent Unit (optional)',
prefixIcon: Icon(Icons.device_hub_outlined),
),
items: [
const DropdownMenuItem<String?>(
child: Text('— None (root unit) —'),
),
...widget.availableParents.map((u) {
return DropdownMenuItem<String?>(
value: u.unitId,
child: Text('${u.echelon.displayName}: ${u.name}'),
);
}),
],
onChanged: (v) => setState(() => _parentUnitId = v),
),
const SizedBox(height: 12),
],
// Key Leader toggle
SwitchListTile(
title: const Text('Key Leader Unit'),
subtitle: const Text('Marks this as a command/key leader node'),
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 Unit'),
onPressed: _submit,
),
],
);
}
}

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:uto_manager/application/blocs/personnel_cubit.dart';
@ -5,10 +7,15 @@ import 'package:uto_manager/application/blocs/unit_tree_cubit.dart';
import 'package:uto_manager/application/blocs/validation_cubit.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/services/mtoe_validation_engine.dart';
import 'package:uto_manager/infrastructure/codecs/pdf_chart_exporter.dart';
import 'package:uto_manager/infrastructure/codecs/protobuf_codec.dart';
import 'package:uto_manager/infrastructure/codecs/unit_tree_codec.dart';
import 'package:uto_manager/infrastructure/repositories/drift_soldier_positions_repository.dart';
import 'package:uto_manager/presentation/screens/dialogs/add_edit_personnel_dialog.dart';
import 'package:uto_manager/presentation/screens/dialogs/add_edit_slot_dialog.dart';
import 'package:uto_manager/presentation/screens/dialogs/add_edit_unit_dialog.dart';
import 'package:uto_manager/presentation/widgets/personnel_list_tile.dart';
import 'package:uto_manager/presentation/widgets/position_slot_card.dart';
import 'package:uto_manager/presentation/widgets/uto_canvas_widget.dart';
@ -20,7 +27,9 @@ import 'package:uto_manager/presentation/widgets/uto_canvas_widget.dart';
/// - Center: Interactive org chart canvas
/// - Right panel: Unit inspector (selected unit's position slots + validation)
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
const HomeScreen({required this.positionsRepository, super.key});
final DriftSoldierPositionsRepository positionsRepository;
@override
Widget build(BuildContext context) {
@ -48,6 +57,22 @@ class HomeScreen extends StatelessWidget {
backgroundColor: const Color(0xFF1B3A6B),
foregroundColor: Colors.white,
actions: [
// Add Unit button
BlocBuilder<UnitTreeCubit, UnitTreeState>(
builder: (context, treeState) {
return IconButton(
icon: const Icon(Icons.add_box_outlined),
tooltip: 'Add Unit',
onPressed: () => _showAddUnitDialog(context, treeState),
);
},
),
// Add Soldier button
IconButton(
icon: const Icon(Icons.person_add_outlined),
tooltip: 'Add Soldier',
onPressed: () => _showAddPersonnelDialog(context),
),
// Validate button
BlocBuilder<UnitTreeCubit, UnitTreeState>(
builder: (context, treeState) {
@ -101,7 +126,7 @@ class HomeScreen extends StatelessWidget {
const VerticalDivider(width: 1),
// Right panel: Unit inspector
SizedBox(
width: 280,
width: 300,
child: _buildInspectorPanel(context),
),
],
@ -129,13 +154,28 @@ class HomeScreen extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'PERSONNEL ROSTER',
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
color: theme.colorScheme.onSurfaceVariant,
),
Row(
children: [
Text(
'PERSONNEL ROSTER',
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
color: theme.colorScheme.onSurfaceVariant,
),
),
const Spacer(),
// Add soldier button in panel header
SizedBox(
height: 24,
child: IconButton(
icon: const Icon(Icons.person_add_outlined, size: 16),
tooltip: 'Add Soldier',
padding: EdgeInsets.zero,
onPressed: () => _showAddPersonnelDialog(context),
),
),
],
),
const SizedBox(height: 6),
BlocBuilder<PersonnelCubit, PersonnelState>(
@ -165,10 +205,40 @@ class HomeScreen extends StatelessWidget {
}
if (state is PersonnelLoaded) {
final soldiers = state.filteredUnassigned;
if (soldiers.isEmpty && state.filterQuery.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.person_outline,
size: 40,
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
),
const SizedBox(height: 8),
Text(
'No soldiers yet',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
FilledButton.tonalIcon(
icon: const Icon(Icons.person_add_outlined, size: 14),
label: const Text('Add Soldier'),
onPressed: () => _showAddPersonnelDialog(context),
),
],
),
),
);
}
if (soldiers.isEmpty) {
return Center(
child: Text(
'No unassigned soldiers',
'No matches',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
@ -177,13 +247,34 @@ class HomeScreen extends StatelessWidget {
}
return ListView.builder(
itemCount: soldiers.length,
itemBuilder: (context, i) => PersonnelListTile(record: soldiers[i]),
itemBuilder: (context, i) => PersonnelListTile(
record: soldiers[i],
onTap: () => _showEditPersonnelDialog(context, soldiers[i]),
),
);
}
return const SizedBox.shrink();
},
),
),
// Show all personnel (assigned + unassigned) section
BlocBuilder<PersonnelCubit, PersonnelState>(
builder: (context, state) {
if (state is! PersonnelLoaded) return const SizedBox.shrink();
final assigned = state.allPersonnel.where((p) => p.isAssigned).toList();
if (assigned.isEmpty) return const SizedBox.shrink();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
color: theme.colorScheme.surfaceContainerLow,
child: Text(
'${assigned.length} assigned • ${state.unassigned.length} unassigned',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
);
},
),
],
);
}
@ -232,12 +323,29 @@ class HomeScreen extends StatelessWidget {
return Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
'Select a unit on the canvas to inspect its position slots.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.touch_app_outlined,
size: 40,
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
),
const SizedBox(height: 8),
Text(
'Select a unit on the canvas to inspect its position slots.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
FilledButton.tonalIcon(
icon: const Icon(Icons.add_box_outlined, size: 14),
label: const Text('Add Unit'),
onPressed: () => _showAddUnitDialog(context, treeState),
),
],
),
),
);
@ -254,20 +362,61 @@ class HomeScreen extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Inspector header
// Inspector header with edit/delete actions
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
color: theme.colorScheme.surfaceContainerHigh,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'UNIT INSPECTOR',
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
color: theme.colorScheme.onSurfaceVariant,
),
Row(
children: [
Text(
'UNIT INSPECTOR',
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.2,
color: theme.colorScheme.onSurfaceVariant,
),
),
const Spacer(),
// Edit unit button
SizedBox(
height: 24,
child: IconButton(
icon: const Icon(Icons.edit_outlined, size: 15),
tooltip: 'Edit Unit',
padding: EdgeInsets.zero,
onPressed: () =>
_showEditUnitDialog(context, selectedUnit, treeState.units),
),
),
// Add child unit button
SizedBox(
height: 24,
child: IconButton(
icon: const Icon(Icons.add_box_outlined, size: 15),
tooltip: 'Add Child Unit',
padding: EdgeInsets.zero,
onPressed: () =>
_showAddChildUnitDialog(context, selectedUnit, treeState),
),
),
// Delete unit button
SizedBox(
height: 24,
child: IconButton(
icon: Icon(
Icons.delete_outline,
size: 15,
color: theme.colorScheme.error,
),
tooltip: 'Delete Unit & Subtree',
padding: EdgeInsets.zero,
onPressed: () => _confirmDeleteUnit(context, selectedUnit),
),
),
],
),
const SizedBox(height: 4),
Text(
@ -310,14 +459,68 @@ class HomeScreen extends StatelessWidget {
);
},
),
// Slots header with add-slot button
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
color: theme.colorScheme.surfaceContainerLow,
child: Row(
children: [
Text(
'POSITION SLOTS',
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 1.1,
color: theme.colorScheme.onSurfaceVariant,
),
),
const Spacer(),
Text(
'${slots.where((s) => s.isFilled).length}/${slots.length} filled',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(width: 8),
SizedBox(
height: 24,
child: IconButton(
icon: const Icon(Icons.add_circle_outline, size: 15),
tooltip: 'Add Position Slot',
padding: EdgeInsets.zero,
onPressed: () => _showAddSlotDialog(context, selectedUnit.unitId),
),
),
],
),
),
// Slots list
Expanded(
child: slots.isEmpty
? Center(
child: Text(
'No position slots defined',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.work_outline,
size: 32,
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
),
const SizedBox(height: 8),
Text(
'No position slots defined',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 8),
FilledButton.tonalIcon(
icon: const Icon(Icons.add_circle_outline, size: 14),
label: const Text('Add Slot'),
onPressed: () => _showAddSlotDialog(context, selectedUnit.unitId),
),
],
),
),
)
@ -333,6 +536,8 @@ class HomeScreen extends StatelessWidget {
assignedSoldier: soldier,
onUnassign: slot.isFilled ? () => _unassign(context, slot) : null,
onAcceptDrop: (soldier) => _assign(context, soldier, slot),
onEdit: () => _showEditSlotDialog(context, slot),
onDelete: () => _confirmDeleteSlot(context, slot),
);
},
),
@ -402,6 +607,18 @@ class HomeScreen extends StatelessWidget {
},
),
const SizedBox(width: 16),
// Personnel count
BlocBuilder<PersonnelCubit, PersonnelState>(
builder: (context, state) {
if (state is! PersonnelLoaded) return const SizedBox.shrink();
return _statusChip(
theme,
Icons.people_outline,
'${state.allPersonnel.length} soldiers',
);
},
),
const SizedBox(width: 16),
// Validation finding counts
BlocBuilder<ValidationCubit, ValidationState>(
builder: (context, valState) {
@ -483,7 +700,7 @@ class HomeScreen extends StatelessWidget {
}
// ---------------------------------------------------------------------------
// Actions
// Assignment Actions
// ---------------------------------------------------------------------------
void _assign(BuildContext context, PersonnelRecord soldier, PositionSlot slot) {
@ -494,6 +711,172 @@ class HomeScreen extends StatelessWidget {
context.read<PersonnelCubit>().unassignFromSlot(slot);
}
// ---------------------------------------------------------------------------
// Personnel CRUD
// ---------------------------------------------------------------------------
Future<void> _showAddPersonnelDialog(BuildContext context) async {
final record = await showDialog<PersonnelRecord>(
context: context,
builder: (_) => AddEditPersonnelDialog(positionsRepository: positionsRepository),
);
if (record != null && context.mounted) {
unawaited(context.read<PersonnelCubit>().addPersonnel(record));
}
}
Future<void> _showEditPersonnelDialog(BuildContext context, PersonnelRecord existing) async {
final record = await showDialog<PersonnelRecord>(
context: context,
builder: (_) => AddEditPersonnelDialog(
positionsRepository: positionsRepository,
existing: existing,
),
);
if (record != null && context.mounted) {
unawaited(context.read<PersonnelCubit>().updatePersonnel(record));
}
}
// ---------------------------------------------------------------------------
// Unit CRUD
// ---------------------------------------------------------------------------
Future<void> _showAddUnitDialog(BuildContext context, UnitTreeState treeState) async {
final units = treeState is UnitTreeLoaded ? treeState.units : <UnitNode>[];
final unit = await showDialog<UnitNode>(
context: context,
builder: (_) => AddEditUnitDialog(availableParents: units),
);
if (unit != null && context.mounted) {
unawaited(context.read<UnitTreeCubit>().addUnit(unit));
}
}
Future<void> _showAddChildUnitDialog(
BuildContext context,
UnitNode parent,
UnitTreeLoaded treeState,
) async {
final unit = await showDialog<UnitNode>(
context: context,
builder: (_) => AddEditUnitDialog(
parentUnit: parent,
availableParents: treeState.units,
),
);
if (unit != null && context.mounted) {
unawaited(context.read<UnitTreeCubit>().addUnit(unit));
}
}
Future<void> _showEditUnitDialog(
BuildContext context,
UnitNode existing,
List<UnitNode> allUnits,
) async {
// Exclude the unit itself and its descendants from available parents
final unit = await showDialog<UnitNode>(
context: context,
builder: (_) => AddEditUnitDialog(
existing: existing,
availableParents: allUnits.where((u) => u.unitId != existing.unitId).toList(),
),
);
if (unit != null && context.mounted) {
unawaited(context.read<UnitTreeCubit>().updateUnit(unit));
}
}
Future<void> _confirmDeleteUnit(BuildContext context, UnitNode unit) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Delete Unit?'),
content: Text(
'Delete "${unit.name}" and all its subordinate units and position slots? '
'This action cannot be undone.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Cancel'),
),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: Colors.red.shade700),
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Delete'),
),
],
),
);
if ((confirmed ?? false) && context.mounted) {
unawaited(context.read<UnitTreeCubit>().deleteSubtree(unit.unitId));
}
}
// ---------------------------------------------------------------------------
// Position Slot CRUD
// ---------------------------------------------------------------------------
Future<void> _showAddSlotDialog(BuildContext context, String unitId) async {
final slot = await showDialog<PositionSlot>(
context: context,
builder: (_) => AddEditSlotDialog(unitId: unitId),
);
if (slot != null && context.mounted) {
unawaited(context.read<UnitTreeCubit>().addSlot(slot));
}
}
Future<void> _showEditSlotDialog(BuildContext context, PositionSlot existing) async {
final slot = await showDialog<PositionSlot>(
context: context,
builder: (_) => AddEditSlotDialog(unitId: existing.unitId, existing: existing),
);
if (slot != null && context.mounted) {
unawaited(context.read<UnitTreeCubit>().updateSlot(slot));
}
}
Future<void> _confirmDeleteSlot(BuildContext context, PositionSlot slot) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Delete Position Slot?'),
content: Text(
'Remove the "${slot.positionTitle}" (${slot.dutyMos.code}) slot? '
'${slot.isFilled ? "The assigned soldier will be unassigned. " : ""}'
'This cannot be undone.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Cancel'),
),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: Colors.red.shade700),
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Delete'),
),
],
),
);
if ((confirmed ?? false) && context.mounted) {
// If slot is filled, unassign the soldier first
if (slot.isFilled) {
await context.read<PersonnelCubit>().unassignFromSlot(slot);
}
if (context.mounted) {
unawaited(context.read<UnitTreeCubit>().deleteSlot(slot.slotId));
}
}
}
// ---------------------------------------------------------------------------
// Validation & Export
// ---------------------------------------------------------------------------
void _runValidation(BuildContext context, UnitTreeLoaded treeState) {
final personnel = context.read<PersonnelCubit>().state is PersonnelLoaded
? (context.read<PersonnelCubit>().state as PersonnelLoaded).allPersonnel
@ -628,7 +1011,10 @@ class _ExportDialogState extends State<_ExportDialog> {
color: Colors.red.shade900.withAlpha(80),
borderRadius: BorderRadius.circular(4),
),
child: Text(_error!, style: const TextStyle(fontSize: 11, color: Colors.redAccent)),
child: Text(
_error!,
style: const TextStyle(fontSize: 11, color: Colors.redAccent),
),
),
],
],

View File

@ -5,15 +5,23 @@ import 'package:uto_manager/presentation/widgets/position_slot_card.dart' show P
/// A draggable list tile representing a soldier in the personnel panel.
///
/// Can be dragged onto a [PositionSlotCard] to assign the soldier.
/// Tap to edit; long-press for a context menu with edit/delete options.
class PersonnelListTile extends StatelessWidget {
const PersonnelListTile({
required this.record, super.key,
required this.record,
super.key,
this.onTap,
this.onDelete,
});
final PersonnelRecord record;
/// Called when the tile is tapped (opens edit dialog).
final VoidCallback? onTap;
/// Called when the user selects Delete from the context menu.
final VoidCallback? onDelete;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -40,7 +48,7 @@ class PersonnelListTile extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Text(
record.name,
record.displayLabel,
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.onPrimaryContainer,
@ -84,7 +92,7 @@ class PersonnelListTile extends StatelessWidget {
),
),
title: Text(
record.name,
record.displayLabel,
style: theme.textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
),
subtitle: Text(

View File

@ -13,6 +13,8 @@ class PositionSlotCard extends StatelessWidget {
this.assignedSoldier,
this.onUnassign,
this.onAcceptDrop,
this.onEdit,
this.onDelete,
});
final PositionSlot slot;
@ -20,6 +22,12 @@ class PositionSlotCard extends StatelessWidget {
final VoidCallback? onUnassign;
final void Function(PersonnelRecord soldier)? onAcceptDrop;
/// Called when the user taps the edit button on this slot.
final VoidCallback? onEdit;
/// Called when the user taps the delete button on this slot.
final VoidCallback? onDelete;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -67,21 +75,48 @@ class PositionSlotCard extends StatelessWidget {
fontWeight: slot.isVacant ? FontWeight.bold : FontWeight.normal,
),
),
trailing: slot.isFilled && onUnassign != null
? IconButton(
icon: const Icon(Icons.person_remove, size: 16),
onPressed: onUnassign,
tooltip: 'Unassign soldier',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
)
: null,
trailing: _buildTrailing(theme),
),
);
},
);
}
Widget? _buildTrailing(ThemeData theme) {
final hasActions = onUnassign != null || onEdit != null || onDelete != null;
if (!hasActions) return null;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (slot.isFilled && onUnassign != null)
IconButton(
icon: const Icon(Icons.person_remove, size: 15),
onPressed: onUnassign,
tooltip: 'Unassign soldier',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 26, minHeight: 26),
),
if (onEdit != null)
IconButton(
icon: const Icon(Icons.edit_outlined, size: 15),
onPressed: onEdit,
tooltip: 'Edit slot',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 26, minHeight: 26),
),
if (onDelete != null)
IconButton(
icon: Icon(Icons.delete_outline, size: 15, color: theme.colorScheme.error),
onPressed: onDelete,
tooltip: 'Delete slot',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 26, minHeight: 26),
),
],
);
}
Widget _buildMosBadge(ThemeData theme) {
return Container(
width: 40,

View File

@ -642,7 +642,27 @@ This document breaks the master development plan into five structured phases spa
---
## Phase 5: Platform Optimization, Profiling, & Hardening
## Phase 5: UI/CRUD Layer & Web Platform ✅ COMPLETE (2026-08-09)
> **Status:** Phase 5 UI/CRUD layer and web platform support completed and merged to `main`.
> 263/263 tests passing. `flutter analyze` — No issues found.
>
> **Completed in this phase:**
>
> - `SoldierPosition` entity — 31 built-in Army duty positions + user-defined custom positions
> - `SoldierPositions` Drift table + `SoldierPositionsDao` + `DriftSoldierPositionsRepository`
> - `AppDatabase` schema v2 migration — adds `position_title` to `Personnel`, creates `soldier_positions` table
> - Web platform: `drift_worker.dart/js`, `sqlite3.wasm`, platform-adaptive `driftDatabase()` connection
> - `PersonnelRecord` — extended with `positionTitle`, `displayLabel`, `isAssigned`, `isFmc`
> - `UnitTreeCubit` — full CRUD: `addUnit`, `updateUnit`, `addSlot`, `updateSlot`, `deleteSlot`, `clearAll`
> - `PersonnelCubit` — full CRUD: `addPersonnel`, `updatePersonnel`, `deletePersonnel`
> - `AddEditPersonnelDialog` — position picker with custom position management, MOS autocomplete
> - `AddEditSlotDialog` — slot creation/editing with MOS autocomplete and key-leader toggle
> - `AddEditUnitDialog` — unit creation/editing with echelon/branch/parent selectors
> - `HomeScreen` — full CRUD app bar actions, inspector CRUD buttons, export dialog with format/page-size selection
>
> **Deferred to hardening:** MIL-STD-2525D symbology engine, SQLCipher encryption,
> undo/redo stack, multi-window support, PWA service worker, Android battery profiling.
### Weeks 20 – 24

View File

@ -11,11 +11,13 @@ import 'package:uto_manager/domain/services/unit_tree_service.dart';
import 'package:uto_manager/infrastructure/database/app_database.dart';
import 'package:uto_manager/infrastructure/repositories/drift_personnel_repository.dart';
import 'package:uto_manager/infrastructure/repositories/drift_position_slot_repository.dart';
import 'package:uto_manager/infrastructure/repositories/drift_soldier_positions_repository.dart';
import 'package:uto_manager/infrastructure/repositories/drift_unit_repository.dart';
import 'package:uto_manager/presentation/screens/home_screen.dart';
void main() {
late AppDatabase db;
late DriftSoldierPositionsRepository positionsRepo;
late UnitTreeCubit unitTreeCubit;
late PersonnelCubit personnelCubit;
late TemplateCubit templateCubit;
@ -28,6 +30,7 @@ void main() {
final unitRepo = DriftUnitRepository(dao: db.unitDao, db: db);
final personnelRepo = DriftPersonnelRepository(dao: db.personnelDao);
final slotRepo = DriftPositionSlotRepository(dao: db.positionSlotDao);
positionsRepo = DriftSoldierPositionsRepository(dao: db.soldierPositionsDao);
const assignmentService = AssignmentService(treeService: treeService);
// Create cubits but do NOT call startWatching() — avoids live streams.
@ -66,7 +69,7 @@ void main() {
BlocProvider<TemplateCubit>.value(value: templateCubit),
BlocProvider<ValidationCubit>.value(value: validationCubit),
],
child: const MaterialApp(home: HomeScreen()),
child: MaterialApp(home: HomeScreen(positionsRepository: positionsRepo)),
),
);

5
web/drift_worker.dart Normal file
View File

@ -0,0 +1,5 @@
import 'package:drift/wasm.dart';
void main() {
WasmDatabase.workerMainForOpen();
}

14475
web/drift_worker.js Normal file

File diff suppressed because one or more lines are too long

BIN
web/sqlite3.wasm Normal file

Binary file not shown.