Compare commits

..

No commits in common. "e52d3406a6d36e5baa6f47f8a1365cd5f552ce93" and "6ee71775fa1239378723b0bc626399dab67ae00b" have entirely different histories.

31 changed files with 926 additions and 17231 deletions

590
.gitignore vendored
View File

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

574
README.md
View File

@ -1,288 +1,286 @@
# UTO Manager # UTO Manager
## Flutter-Based Military Unit Task Organization Enterprise System ## Flutter-Based Military Unit Task Organization Enterprise System
### U.S. Army 2026 Doctrine — Cross-Platform Command & Control Application ### 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) [![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) [![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) [![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) [![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) [![Docs](https://img.shields.io/badge/Docs-MkDocs-526CFE?logo=materialformkdocs)](https://www.mkdocs.org)
--- ---
## Overview ## 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. **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. 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 ## Key Features
- **Interactive Visual Canvas** — GPU-accelerated `CustomPainter` engine with pan/zoom, viewport culling, and 60 FPS rendering at 5,000+ nodes - **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
- **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
- **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
- **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
- **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
- **Offline-First Architecture** — Drift/SQLite persistence with delta synchronization for contested network environments - **Multi-Format Export** — JSON, Protobuf, and vector PDF organizational chart export
- **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 Targets | Platform | Deployment | Performance Target |
|---|---|---|
| Platform | Deployment | Performance Target | | Windows x64 | Native `.msi` / `.exe` | 60 FPS @ 5,000+ nodes |
|---|---|---| | Web (WASM) | WebAssembly + CanvasKit | 60 FPS @ 2,000+ nodes |
| Windows x64 | Native `.msi` / `.exe` | 60 FPS @ 5,000+ nodes | | Android | Native `.apk` / `.aab` | 60 FPS @ 1,500+ nodes |
| Web (WASM) | WebAssembly + CanvasKit | 60 FPS @ 2,000+ nodes |
| Android | Native `.apk` / `.aab` | 60 FPS @ 1,500+ nodes | ---
--- ## Architecture
## Architecture ```text
[Presentation Layer: CustomPainter / InteractiveViewer / Responsive Shell]
```text │
[Presentation Layer: CustomPainter / InteractiveViewer / Responsive Shell] ▼
│ [Application State Layer: BLoC / Signals / Super Drag-and-Drop Pipeline]
▼ │
[Application State Layer: BLoC / Signals / Super Drag-and-Drop Pipeline] ▼
│ [Domain Layer: MTOE Doctrinal Rules Engine / Structural Validation]
▼ │
[Domain Layer: MTOE Doctrinal Rules Engine / Structural Validation] ▼
│ [Infrastructure Layer: Drift SQLite / IndexedDB / Protobuf Codecs]
▼ ```
[Infrastructure Layer: Drift SQLite / IndexedDB / Protobuf Codecs]
``` ### Project Structure
### Project Structure ```text
lib/
```text ├── domain/ # Entities, value objects, repository interfaces
lib/ │ ├── entities/ # UnitNode, PositionSlot, PersonnelRecord
├── domain/ # Entities, value objects, repository interfaces │ ├── enums/ # Echelon, DoctrinalBranch, AuthorizedRank
│ ├── entities/ # UnitNode, PositionSlot, PersonnelRecord │ └── repositories/ # Abstract repository contracts
│ ├── enums/ # Echelon, DoctrinalBranch, AuthorizedRank ├── application/ # BLoC / use cases / state management
│ └── repositories/ # Abstract repository contracts │ ├── blocs/ # State management BLoCs
├── application/ # BLoC / use cases / state management │ └── use_cases/ # Domain use case implementations
│ ├── blocs/ # State management BLoCs ├── infrastructure/ # Drift DB, IndexedDB adapters, Protobuf codecs
│ └── use_cases/ # Domain use case implementations │ ├── database/ # Drift table definitions and DAOs
├── infrastructure/ # Drift DB, IndexedDB adapters, Protobuf codecs │ ├── codecs/ # JSON and Protobuf serialization
│ ├── database/ # Drift table definitions and DAOs │ └── sync/ # Delta synchronization engine
│ ├── codecs/ # JSON and Protobuf serialization └── presentation/ # Widgets, painters, responsive shell
│ └── sync/ # Delta synchronization engine ├── canvas/ # CustomPainter, InteractiveViewer, viewport
└── presentation/ # Widgets, painters, responsive shell ├── layout/ # Reingold-Tilford algorithm, Quadtree index
├── canvas/ # CustomPainter, InteractiveViewer, viewport ├── symbology/ # MIL-STD-2525D rendering pipeline
├── layout/ # Reingold-Tilford algorithm, Quadtree index └── shell/ # Three-pane UI, responsive layouts
├── symbology/ # MIL-STD-2525D rendering pipeline ```
└── shell/ # Three-pane UI, responsive layouts
``` ---
--- ## Getting Started
## Getting Started ### Prerequisites
### 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)
- [Flutter SDK](https://docs.flutter.dev/get-started/install) 3.x or later - [Visual Studio 2022](https://visualstudio.microsoft.com/) with **Desktop development with C++** workload (Windows target)
- [Dart SDK](https://dart.dev/get-dart) 3.x (included with Flutter) - [Android Studio](https://developer.android.com/studio) with Android SDK (Android target)
- [Visual Studio 2022](https://visualstudio.microsoft.com/) with **Desktop development with C++** workload (Windows target) - [Python 3.9+](https://www.python.org/) with `pip` (MkDocs documentation)
- [Android Studio](https://developer.android.com/studio) with Android SDK (Android target) - [Git](https://git-scm.com/)
- [Python 3.9+](https://www.python.org/) with `pip` (MkDocs documentation)
- [Git](https://git-scm.com/) ### Clone the Repository
### Clone the Repository ```bash
git clone https://git.kellsupport.com/uto_manager.git
```bash cd uto_manager
git clone https://git.kellsupport.com/uto_manager.git ```
cd uto_manager
``` ### Install Flutter Dependencies
### Install Flutter Dependencies ```bash
flutter pub get
```bash ```
flutter pub get
``` ### Run the Application
### Run the Application ```bash
# Windows desktop
```bash flutter run -d windows
# Windows desktop
flutter run -d windows # Web (Chrome)
flutter run -d chrome
# Web (Chrome)
flutter run -d chrome # Web (WASM mode)
flutter run -d chrome --wasm
# Web (WASM mode)
flutter run -d chrome --wasm # Android (connected device or emulator)
flutter run -d android
# Android (connected device or emulator) ```
flutter run -d android
``` ### Run Tests
### Run Tests ```bash
# All tests
```bash flutter test
# All tests
flutter test # With coverage
flutter test --coverage
# With coverage
flutter test --coverage # Static analysis
flutter analyze
# Static analysis ```
flutter analyze
``` ### Build Release Packages
### Build Release Packages ```bash
# Windows
```bash flutter build windows --release
# Windows
flutter build windows --release # Web (WASM)
flutter build web --wasm --release
# Web (WASM)
flutter build web --wasm --release # Android APK
flutter build apk --release
# Android APK
flutter build apk --release # Android App Bundle (Google Play)
flutter build appbundle --release
# Android App Bundle (Google Play) ```
flutter build appbundle --release
``` ---
--- ## Documentation
## Documentation Project documentation is built with [MkDocs](https://www.mkdocs.org/) using the [Material theme](https://squidfunk.github.io/mkdocs-material/).
Project documentation is built with [MkDocs](https://www.mkdocs.org/) using the [Material theme](https://squidfunk.github.io/mkdocs-material/). ### Install Documentation Dependencies
### Install Documentation Dependencies ```bash
pip install -r docs/requirements.txt
```bash ```
pip install -r docs/requirements.txt
``` ### Serve Documentation Locally
### Serve Documentation Locally ```bash
mkdocs serve
```bash ```
mkdocs serve
``` Then open [http://127.0.0.1:8000](http://127.0.0.1:8000) in your browser.
Then open [http://127.0.0.1:8000](http://127.0.0.1:8000) in your browser. ### Build Documentation Site
### Build Documentation Site ```bash
mkdocs build
```bash ```
mkdocs build
``` Output is written to the `site/` directory (excluded from version control).
Output is written to the `site/` directory (excluded from version control). ---
--- ## Development Workflow
## Development Workflow ### Branch Strategy
### Branch Strategy | Branch | Purpose |
|---|---|
| Branch | Purpose | | `main` | Production-ready releases only |
|---|---| | `develop` | Integration branch for completed features |
| `main` | Production-ready releases only | | `feature/*` | Individual feature development |
| `develop` | Integration branch for completed features | | `fix/*` | Bug fixes |
| `feature/*` | Individual feature development | | `release/*` | Release preparation and stabilization |
| `fix/*` | Bug fixes |
| `release/*` | Release preparation and stabilization | ### Commit Convention
### Commit Convention This project follows [Conventional Commits](https://www.conventionalcommits.org/):
This project follows [Conventional Commits](https://www.conventionalcommits.org/): ```text
feat(canvas): implement viewport culling via inverse matrix transform
```text fix(validation): correct MOS mismatch detection for 68W slots
feat(canvas): implement viewport culling via inverse matrix transform docs(readme): update platform prerequisites
fix(validation): correct MOS mismatch detection for 68W slots chore(deps): upgrade drift to 2.x
docs(readme): update platform prerequisites ```
chore(deps): upgrade drift to 2.x
``` ### Code Quality
### Code Quality ```bash
# Format code
```bash dart format .
# Format code
dart format . # Analyze code
flutter analyze
# Analyze code
flutter analyze # Run linter
dart run dart_code_metrics:metrics analyze lib
# Run linter ```
dart run dart_code_metrics:metrics analyze lib
``` ---
--- ## Technology Stack
## Technology Stack | Layer | Technology | Purpose |
|---|---|---|
| Layer | Technology | Purpose | | UI Framework | Flutter (Dart) | Single codebase for Windows, Web, Android |
|---|---|---| | Rendering | CustomPainter + InteractiveViewer | GPU-direct drawing; bypasses widget tree overhead |
| UI Framework | Flutter (Dart) | Single codebase for Windows, Web, Android | | State Management | BLoC + Signals | Reactive atomic state |
| Rendering | CustomPainter + InteractiveViewer | GPU-direct drawing; bypasses widget tree overhead | | Drag-and-Drop | super_drag_and_drop | Cross-platform payload integrity |
| State Management | BLoC + Signals | Reactive atomic state | | Local Database | Drift (SQLite / IndexedDB) | Type-safe reactive ORM; offline-first |
| Drag-and-Drop | super_drag_and_drop | Cross-platform payload integrity | | Encryption | SQLCipher | Data-at-rest protection |
| Local Database | Drift (SQLite / IndexedDB) | Type-safe reactive ORM; offline-first | | Symbology (Web) | milsymbol.js via dart:js_interop | Full MIL-STD-2525D compliance |
| Encryption | SQLCipher | Data-at-rest protection | | Symbology (Native) | path_drawing + SVG assets + LRU cache | Zero external runtime dependencies |
| Symbology (Web) | milsymbol.js via dart:js_interop | Full MIL-STD-2525D compliance | | Background Compute | Dart Isolates | Offload layout, validation, export |
| Symbology (Native) | path_drawing + SVG assets + LRU cache | Zero external runtime dependencies | | Serialization | JSON + Protobuf | Human-readable + compact binary export |
| Background Compute | Dart Isolates | Offload layout, validation, export | | PDF Export | pdf package | Vector-quality organizational chart output |
| Serialization | JSON + Protobuf | Human-readable + compact binary export | | Documentation | MkDocs + Material theme | User and developer documentation |
| PDF Export | pdf package | Vector-quality organizational chart output |
| Documentation | MkDocs + Material theme | User and developer documentation | ---
--- ## Development Phases
## Development Phases | Phase | Title | Weeks | Status |
|---|---|---|---|
| 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 1 | Architecture & Data Foundations | 1 – 4 | ✅ Complete | | Phase 3 | 2026 Doctrinal Templates & Symbology Engine | 10 – 14 | ⬜ Not Started |
| Phase 2 | Canvas Graphics Engine & Persistence | 5 – 9 | ✅ Complete | | Phase 4 | Persistence, Validation Rules, & Export Engine | 15 – 19 | ⬜ Not Started |
| Phase 3 | Template Library & Import/Export | 10 – 14 | ✅ Complete | | Phase 5 | Platform Optimization, Profiling, & Hardening | 20 – 24 | ⬜ Not Started |
| 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.
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
## Contributing 1. Fork the repository and create a `feature/*` branch from `develop`
2. Implement your changes with appropriate unit tests
1. Fork the repository and create a `feature/*` branch from `develop` 3. Ensure `flutter analyze` passes with zero violations
2. Implement your changes with appropriate unit tests 4. Ensure `flutter test --coverage` meets the 80% minimum threshold
3. Ensure `flutter analyze` passes with zero violations 5. Submit a pull request targeting the `develop` branch
4. Ensure `flutter test --coverage` meets the 80% minimum threshold 6. Pull requests require passing CI on all three platform targets before merge
5. Submit a pull request targeting the `develop` branch
6. Pull requests require passing CI on all three platform targets before merge ---
--- ## Security
## 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).
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.**
**Do not commit sensitive data, encryption keys, or classified information to this repository.** ---
--- ## License
## License Proprietary — All rights reserved. See [LICENSE](LICENSE) for details.
Proprietary — All rights reserved. See [LICENSE](LICENSE) for details. ---
--- ## Related Documents
## 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
- [`master_development_plan.md`](master_development_plan.md) — Full architectural specification - [`development_status.md`](development_status.md) — Sprint tracking and task status
- [`phased_development_plan.md`](phased_development_plan.md) — Phase-by-phase implementation plan - [Documentation Site](http://127.0.0.1:8000) — MkDocs user and developer documentation
- [`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 # UTO Manager — Development Status
Last updated: 2026-08-09 — Phase 5 UI/CRUD layer merged to `main` Last updated: 2026-08-09 — branch `fix/phase-4-analyzer-infos` merged to `main`
--- ---
## Current Branch ## Current Branch
`main` (Phase 5 UI/CRUD layer merged) `main` (fix/phase-4-analyzer-infos merged)
## Overall Progress ## Overall Progress
@ -16,7 +16,7 @@ Last updated: 2026-08-09 — Phase 5 UI/CRUD layer merged to `main`
| 2 | Canvas Engine & Persistence | ✅ Complete | | 2 | Canvas Engine & Persistence | ✅ Complete |
| 3 | Template Library & Import/Export | ✅ Complete | | 3 | Template Library & Import/Export | ✅ Complete |
| 4 | Reporting & Analytics | ✅ Complete | | 4 | Reporting & Analytics | ✅ Complete |
| 5 | UI/CRUD Layer & Web Platform | ✅ Complete | | 5 | Multi-user / Sync | 🔲 Not started |
--- ---
@ -204,132 +204,6 @@ Last updated: 2026-08-09 — Phase 5 UI/CRUD layer 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 ## Test Summary
| File | Tests | Status | | File | Tests | Status |
@ -353,18 +227,15 @@ Last updated: 2026-08-09 — Phase 5 UI/CRUD layer merged to `main`
--- ---
## Known Limitations / Next Steps (Phase 5 Hardening) ## Known Limitations / Next Steps (Phase 5)
- No multi-user or real-time sync capability (Phase 5 hardening) - No multi-user or real-time sync capability (Phase 5)
- Canvas layout is vertical tree only; horizontal layout not yet implemented - Canvas layout is vertical tree only; horizontal layout not yet implemented
- No undo/redo for assignment operations - No undo/redo for assignment operations
- No search/filter on the canvas itself - No search/filter on the canvas itself
- `DeltaSynchronizer` uses simulated online/offline; real network transport deferred - `DeltaSynchronizer` uses simulated online/offline; real network transport deferred to Phase 5
- `ProtobufCodec` uses zlib-compressed JSON as payload; true protobuf schema deferred - `ProtobufCodec` uses zlib-compressed JSON as payload; true protobuf schema deferred to Phase 5
- Template instantiation does not yet persist to database in a single Drift transaction (uses sequential inserts) - 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
--- ---
@ -374,7 +245,6 @@ Last updated: 2026-08-09 — Phase 5 UI/CRUD layer merged to `main`
lib/ lib/
├── domain/ ├── domain/
│ ├── entities/ # Pure Dart domain models (no Flutter deps) │ ├── entities/ # Pure Dart domain models (no Flutter deps)
│ │ # UnitNode, PositionSlot, PersonnelRecord, SoldierPosition
│ ├── value_objects/ # DutyMos (supports 3-char and 4-char AOC formats) │ ├── value_objects/ # DutyMos (supports 3-char and 4-char AOC formats)
│ ├── repositories/ # Abstract interfaces │ ├── repositories/ # Abstract interfaces
│ └── services/ # UnitTreeService, AssignmentService, │ └── services/ # UnitTreeService, AssignmentService,
@ -384,18 +254,12 @@ lib/
├── infrastructure/ ├── infrastructure/
│ ├── codecs/ # UnitTreeCodec (JSON), ProtobufCodec (binary), │ ├── codecs/ # UnitTreeCodec (JSON), ProtobufCodec (binary),
│ │ # PdfChartExporter (PDF org chart) │ │ # PdfChartExporter (PDF org chart)
│ ├── database/ # Drift schema (v2), DAOs, generated code │ ├── database/ # Drift schema, DAOs, generated code
│ │ # Tables: Units, PositionSlots, Personnel,
│ │ # SoldierPositions, Templates
│ └── repositories/ # Drift-backed implementations │ └── repositories/ # Drift-backed implementations
│ # DriftSoldierPositionsRepository (new in Phase 5)
├── application/ ├── application/
│ └── blocs/ # UnitTreeCubit, PersonnelCubit, │ └── blocs/ # UnitTreeCubit, PersonnelCubit,
│ # TemplateCubit, ValidationCubit │ # TemplateCubit, ValidationCubit
└── presentation/ └── presentation/
├── screens/ ├── screens/ # HomeScreen (with PDF/binary export + validation UI)
│ ├── home_screen.dart # Full CRUD UI + export dialog
│ └── dialogs/ # AddEditPersonnelDialog, AddEditSlotDialog,
│ # AddEditUnitDialog (all new in Phase 5)
└── widgets/ # Canvas, cards, tiles └── widgets/ # Canvas, cards, tiles
``` ```

View File

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

View File

@ -1,72 +1,72 @@
# UTO Manager Documentation # UTO Manager Documentation
## Flutter-Based Military Unit Task Organization Enterprise System ## Flutter-Based Military Unit Task Organization Enterprise System
### U.S. Army 2026 Doctrine — Cross-Platform Command & Control Application ### 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. Welcome to the **UTO Manager** documentation. Use the navigation tabs above to find what you need.
--- ---
## Documentation Sections ## Documentation Sections
<div class="grid cards" markdown> <div class="grid cards" markdown>
- :material-book-open-variant:{ .lg .middle } **User Guide** - :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. 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) [:octicons-arrow-right-24: User Guide](user-guide/index.md)
- :material-code-braces:{ .lg .middle } **Developer Guide** - :material-code-braces:{ .lg .middle } **Developer Guide**
--- ---
Architecture deep-dives, API references, and contribution guidelines for engineers building and extending the platform. 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) [:octicons-arrow-right-24: Developer Guide](developer-guide/index.md)
- :material-shield-star:{ .lg .middle } **Doctrinal Reference** - :material-shield-star:{ .lg .middle } **Doctrinal Reference**
--- ---
U.S. Army 2026 force structure specifications, MOS reference tables, and MIL-STD-2525D symbology 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) [:octicons-arrow-right-24: Doctrinal Reference](doctrinal-reference/index.md)
- :material-note-text:{ .lg .middle } **Release Notes** - :material-note-text:{ .lg .middle } **Release Notes**
--- ---
Version history, feature summaries, known limitations, and upgrade instructions. Version history, feature summaries, known limitations, and upgrade instructions.
[:octicons-arrow-right-24: Release Notes](release-notes/index.md) [:octicons-arrow-right-24: Release Notes](release-notes/index.md)
</div> </div>
--- ---
## Quick Links ## Quick Links
| Resource | Description | | Resource | Description |
|---|---| |---|---|
| [Installation](user-guide/getting-started/installation.md) | Set up UTO Manager on Windows, Web, or Android | | [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 | | [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 | | [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 | | [Architecture Overview](developer-guide/architecture/overview.md) | Clean architecture and layered design |
| [MOS Reference](doctrinal-reference/mos/infantry.md) | Military Occupational Specialty reference tables | | [MOS Reference](doctrinal-reference/mos/infantry.md) | Military Occupational Specialty reference tables |
| [MIL-STD-2525D](doctrinal-reference/symbology/overview.md) | Tactical symbology reference | | [MIL-STD-2525D](doctrinal-reference/symbology/overview.md) | Tactical symbology reference |
--- ---
## About This Project ## 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. 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) **Current Version:** Pre-release (Phase 1 — Architecture & Data Foundations)
**Project Repository:** [git.kellsupport.com/uto_manager](https://git.kellsupport.com/uto_manager) **Project Repository:** [git.kellsupport.com/uto_manager](https://git.kellsupport.com/uto_manager)

View File

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

View File

@ -1,112 +1,59 @@
# v1.0.0 — Initial Release # v1.0.0 — Initial Release
## Status: 🔵 In Development ## Status: 🔵 In Development
**Target:** End of hardening phase **Target:** End of Week 24
--- ---
## Implemented Features (Phases 1–5) ## Planned Features
### Core Platform ### Core Platform
- Cross-platform Flutter application targeting Windows x64, Web (WASM), and Android - Cross-platform Flutter application targeting Windows x64, Web (WASM), and Android
- GPU-accelerated `CustomPainter` canvas engine with viewport culling - GPU-accelerated `CustomPainter` canvas engine with viewport culling
- Three-pane adaptive layout: personnel roster | org chart canvas | unit inspector - 60 FPS rendering at 5,000+ nodes (Windows), 2,000+ (Web), 1,500+ (Android)
- 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
### Organizational Modeling - Complete domain model: `UnitNode`, `PositionSlot`, `PersonnelRecord`
- MOS validation across all functional branches (Infantry, Medical, Fires, Signal, Cyber/EW, Aviation, Logistics, Engineer)
- Complete domain model: `UnitNode`, `PositionSlot`, `PersonnelRecord`, `SoldierPosition` - 2026 doctrinal templates: MBCT, SBCT, ABCT, CAB-Heavy, CAB-Light
- 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
- 31 built-in Army duty positions (Command/Staff, Platoon, Squad, Individual) + user-defined custom positions
- Reingold-Tilford hierarchical tree layout algorithm
### Full CRUD User Interface - Quadtree spatial index for hit-testing and drag proximity queries
- Cross-platform drag-and-drop re-parenting with doctrinal validation highlights
- **Add/Edit/Delete Units** — `AddEditUnitDialog` with echelon, branch, parent, and key-leader fields - MIL-STD-2525D tactical symbology rendering
- **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 ### Persistence & Validation
- **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 - Drift/SQLite offline-first persistence with IndexedDB on Web
- Real-time MTOE validation engine (MOS mismatch, rank mismatch, vacant leadership, structural violations)
### Visual Canvas - Delta synchronization for contested network environments
- SQLCipher data-at-rest encryption
- `InteractiveViewer` pan/zoom org chart
- Drag-and-drop soldier assignment (drag from personnel panel → drop on position slot card) ### Export
- Drag-and-drop unit re-parenting with doctrinal validation highlights
- Validation node indicators: red border for critical findings, amber for warnings - JSON export/import with doctrinal validation
- Protobuf binary export
### MTOE Validation Engine - Vector PDF organizational chart export
- 7 validation rules (V001–V007): MOS mismatch, rank mismatch, vacant key leader, missing 68W medic, missing 13F FSO, structural hierarchy violation, manning below 70% ### Platform-Specific
- Background Dart Isolate execution via `IsolateValidationRunner`
- Findings displayed in inspector panel (per-unit, color-coded by severity) and status bar - Windows: multi-window, high-DPI scaling, `.msi` installer, `.uto` file association
- Web: PWA offline capability, IndexedDB quota management
### Persistence & Sync - Android: battery profiling, touch-optimized targets (48×48 dp minimum)
- 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 ## Known Limitations
- `DeltaSynchronizer` — offline-first sync helper (simulated transport; real network deferred)
*To be documented upon release.*
### 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 ## Upgrade Instructions
- **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 *Initial release — no upgrade path required.*
---
## 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; if (filterQuery.isEmpty) return unassigned;
final q = filterQuery.toLowerCase(); final q = filterQuery.toLowerCase();
return unassigned.where((p) { return unassigned.where((p) {
return p.displayLabel.toLowerCase().contains(q) || return p.name.toLowerCase().contains(q) ||
p.primaryMos.code.toLowerCase().contains(q) || p.primaryMos.code.toLowerCase().contains(q) ||
p.payGrade.payGrade.toLowerCase().contains(q); p.payGrade.payGrade.toLowerCase().contains(q);
}).toList(); }).toList();
@ -187,35 +187,6 @@ 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 @override
Future<void> close() { Future<void> close() {
_allSubscription?.cancel(); _allSubscription?.cancel();

View File

@ -142,14 +142,8 @@ class UnitTreeCubit extends Cubit<UnitTreeState> {
final roots = treeService.buildTree(_units, _slots); final roots = treeService.buildTree(_units, _slots);
final currentSelection = final currentSelection =
state is UnitTreeLoaded ? (state as UnitTreeLoaded).selectedUnitId : null; state is UnitTreeLoaded ? (state as UnitTreeLoaded).selectedUnitId : null;
emit( emit(UnitTreeLoaded(
UnitTreeLoaded( units: _units, slots: _slots, roots: roots, selectedUnitId: currentSelection,),);
units: _units,
slots: _slots,
roots: roots,
selectedUnitId: currentSelection,
),
);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -212,42 +206,6 @@ 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). /// Clears all units and slots (template reset).
Future<void> clearAll() async { Future<void> clearAll() async {
try { try {

View File

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

View File

@ -1,108 +0,0 @@
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,9 +74,6 @@ class DutyMos {
return _knownMosCodes.contains(code.trim().toUpperCase()); 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]. /// Returns all MOS codes for a given [DoctrinalBranch].
static Set<String> codesForBranch(DoctrinalBranch branch) { static Set<String> codesForBranch(DoctrinalBranch branch) {
return _branchMosMap[branch] ?? {}; return _branchMosMap[branch] ?? {};

View File

@ -1,8 +1,11 @@
import 'dart:io';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart'; import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:uto_manager/infrastructure/database/daos/personnel_dao.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/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/template_dao.dart';
import 'package:uto_manager/infrastructure/database/daos/unit_dao.dart'; import 'package:uto_manager/infrastructure/database/daos/unit_dao.dart';
@ -42,8 +45,7 @@ class PositionSlots extends Table {
/// Drift table for soldier personnel records. /// Drift table for soldier personnel records.
class Personnel extends Table { class Personnel extends Table {
TextColumn get soldierId => text().named('soldier_id')(); TextColumn get soldierId => text().named('soldier_id')();
TextColumn get name => text().nullable()(); TextColumn get name => text()();
TextColumn get positionTitle => text().named('position_title').nullable()();
TextColumn get primaryMos => text().named('primary_mos')(); TextColumn get primaryMos => text().named('primary_mos')();
TextColumn get payGrade => text().named('pay_grade')(); TextColumn get payGrade => text().named('pay_grade')();
TextColumn get qualifications => text()(); // JSON-encoded list TextColumn get qualifications => text()(); // JSON-encoded list
@ -54,16 +56,6 @@ class Personnel extends Table {
Set<Column> get primaryKey => {soldierId}; 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. /// Drift table for serialized unit tree templates.
class Templates extends Table { class Templates extends Table {
TextColumn get templateId => text().named('template_id')(); TextColumn get templateId => text().named('template_id')();
@ -83,11 +75,11 @@ class Templates extends Table {
/// The main Drift database for the UTO Manager application. /// The main Drift database for the UTO Manager application.
/// ///
/// Manages all core tables: units, position_slots, personnel, soldier_positions, templates. /// Manages all four core tables: units, position_slots, personnel, templates.
/// Supports SQLite on Windows/Android and IndexedDB on Web via drift_web. /// Supports SQLite on Windows/Android and IndexedDB on Web via drift_web.
@DriftDatabase( @DriftDatabase(
tables: [Units, PositionSlots, Personnel, SoldierPositions, Templates], tables: [Units, PositionSlots, Personnel, Templates],
daos: [UnitDao, PersonnelDao, PositionSlotDao, SoldierPositionsDao, TemplateDao], daos: [UnitDao, PersonnelDao, PositionSlotDao, TemplateDao],
) )
class AppDatabase extends _$AppDatabase { class AppDatabase extends _$AppDatabase {
/// Creates the production database backed by SQLite on disk. /// Creates the production database backed by SQLite on disk.
@ -97,7 +89,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase.forTesting(super.executor); AppDatabase.forTesting(super.executor);
@override @override
int get schemaVersion => 2; int get schemaVersion => 1;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@ -105,25 +97,16 @@ class AppDatabase extends _$AppDatabase {
await m.createAll(); await m.createAll();
}, },
onUpgrade: (m, from, to) async { onUpgrade: (m, from, to) async {
if (from < 2) { // Future migration steps added here as schemaVersion increases.
// 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);
}
}, },
); );
} }
/// Opens a platform-adaptive database connection. /// Opens the SQLite database connection for native platforms.
/// LazyDatabase _openConnection() {
/// Uses SQLite via FFI on Windows/Android and WASM/IndexedDB on Web. return LazyDatabase(() async {
QueryExecutor _openConnection() { final dbFolder = await getApplicationDocumentsDirectory();
return driftDatabase( final file = File(p.join(dbFolder.path, 'uto_manager.sqlite'));
name: 'uto_manager', return NativeDatabase.createInBackground(file);
web: DriftWebOptions( });
sqlite3Wasm: Uri.parse('sqlite3.wasm'),
driftWorker: Uri.parse('drift_worker.js'),
),
);
} }

View File

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

View File

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

View File

@ -1,404 +0,0 @@
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

@ -1,221 +0,0 @@
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

@ -1,202 +0,0 @@
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,5 +1,3 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:uto_manager/application/blocs/personnel_cubit.dart'; import 'package:uto_manager/application/blocs/personnel_cubit.dart';
@ -7,15 +5,10 @@ import 'package:uto_manager/application/blocs/unit_tree_cubit.dart';
import 'package:uto_manager/application/blocs/validation_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/personnel_record.dart';
import 'package:uto_manager/domain/entities/position_slot.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/domain/services/mtoe_validation_engine.dart';
import 'package:uto_manager/infrastructure/codecs/pdf_chart_exporter.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/protobuf_codec.dart';
import 'package:uto_manager/infrastructure/codecs/unit_tree_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/personnel_list_tile.dart';
import 'package:uto_manager/presentation/widgets/position_slot_card.dart'; import 'package:uto_manager/presentation/widgets/position_slot_card.dart';
import 'package:uto_manager/presentation/widgets/uto_canvas_widget.dart'; import 'package:uto_manager/presentation/widgets/uto_canvas_widget.dart';
@ -27,9 +20,7 @@ import 'package:uto_manager/presentation/widgets/uto_canvas_widget.dart';
/// - Center: Interactive org chart canvas /// - Center: Interactive org chart canvas
/// - Right panel: Unit inspector (selected unit's position slots + validation) /// - Right panel: Unit inspector (selected unit's position slots + validation)
class HomeScreen extends StatelessWidget { class HomeScreen extends StatelessWidget {
const HomeScreen({required this.positionsRepository, super.key}); const HomeScreen({super.key});
final DriftSoldierPositionsRepository positionsRepository;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -57,22 +48,6 @@ class HomeScreen extends StatelessWidget {
backgroundColor: const Color(0xFF1B3A6B), backgroundColor: const Color(0xFF1B3A6B),
foregroundColor: Colors.white, foregroundColor: Colors.white,
actions: [ 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 // Validate button
BlocBuilder<UnitTreeCubit, UnitTreeState>( BlocBuilder<UnitTreeCubit, UnitTreeState>(
builder: (context, treeState) { builder: (context, treeState) {
@ -126,7 +101,7 @@ class HomeScreen extends StatelessWidget {
const VerticalDivider(width: 1), const VerticalDivider(width: 1),
// Right panel: Unit inspector // Right panel: Unit inspector
SizedBox( SizedBox(
width: 300, width: 280,
child: _buildInspectorPanel(context), child: _buildInspectorPanel(context),
), ),
], ],
@ -154,28 +129,13 @@ class HomeScreen extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Text(
children: [ 'PERSONNEL ROSTER',
Text( style: theme.textTheme.labelSmall?.copyWith(
'PERSONNEL ROSTER', fontWeight: FontWeight.bold,
style: theme.textTheme.labelSmall?.copyWith( letterSpacing: 1.2,
fontWeight: FontWeight.bold, color: theme.colorScheme.onSurfaceVariant,
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), const SizedBox(height: 6),
BlocBuilder<PersonnelCubit, PersonnelState>( BlocBuilder<PersonnelCubit, PersonnelState>(
@ -205,40 +165,10 @@ class HomeScreen extends StatelessWidget {
} }
if (state is PersonnelLoaded) { if (state is PersonnelLoaded) {
final soldiers = state.filteredUnassigned; 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) { if (soldiers.isEmpty) {
return Center( return Center(
child: Text( child: Text(
'No matches', 'No unassigned soldiers',
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
@ -247,34 +177,13 @@ class HomeScreen extends StatelessWidget {
} }
return ListView.builder( return ListView.builder(
itemCount: soldiers.length, itemCount: soldiers.length,
itemBuilder: (context, i) => PersonnelListTile( itemBuilder: (context, i) => PersonnelListTile(record: soldiers[i]),
record: soldiers[i],
onTap: () => _showEditPersonnelDialog(context, soldiers[i]),
),
); );
} }
return const SizedBox.shrink(); 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,
),
),
);
},
),
], ],
); );
} }
@ -323,29 +232,12 @@ class HomeScreen extends StatelessWidget {
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Text(
mainAxisSize: MainAxisSize.min, 'Select a unit on the canvas to inspect its position slots.',
children: [ style: theme.textTheme.bodySmall?.copyWith(
Icon( color: theme.colorScheme.onSurfaceVariant,
Icons.touch_app_outlined, ),
size: 40, textAlign: TextAlign.center,
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),
),
],
), ),
), ),
); );
@ -362,61 +254,20 @@ class HomeScreen extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
// Inspector header with edit/delete actions // Inspector header
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
color: theme.colorScheme.surfaceContainerHigh, color: theme.colorScheme.surfaceContainerHigh,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Text(
children: [ 'UNIT INSPECTOR',
Text( style: theme.textTheme.labelSmall?.copyWith(
'UNIT INSPECTOR', fontWeight: FontWeight.bold,
style: theme.textTheme.labelSmall?.copyWith( letterSpacing: 1.2,
fontWeight: FontWeight.bold, color: theme.colorScheme.onSurfaceVariant,
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), const SizedBox(height: 4),
Text( Text(
@ -459,68 +310,14 @@ 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 // Slots list
Expanded( Expanded(
child: slots.isEmpty child: slots.isEmpty
? Center( ? Center(
child: Padding( child: Text(
padding: const EdgeInsets.all(16), 'No position slots defined',
child: Column( style: theme.textTheme.bodySmall?.copyWith(
mainAxisSize: MainAxisSize.min, color: theme.colorScheme.onSurfaceVariant,
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),
),
],
), ),
), ),
) )
@ -536,8 +333,6 @@ class HomeScreen extends StatelessWidget {
assignedSoldier: soldier, assignedSoldier: soldier,
onUnassign: slot.isFilled ? () => _unassign(context, slot) : null, onUnassign: slot.isFilled ? () => _unassign(context, slot) : null,
onAcceptDrop: (soldier) => _assign(context, soldier, slot), onAcceptDrop: (soldier) => _assign(context, soldier, slot),
onEdit: () => _showEditSlotDialog(context, slot),
onDelete: () => _confirmDeleteSlot(context, slot),
); );
}, },
), ),
@ -607,18 +402,6 @@ class HomeScreen extends StatelessWidget {
}, },
), ),
const SizedBox(width: 16), 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 // Validation finding counts
BlocBuilder<ValidationCubit, ValidationState>( BlocBuilder<ValidationCubit, ValidationState>(
builder: (context, valState) { builder: (context, valState) {
@ -700,7 +483,7 @@ class HomeScreen extends StatelessWidget {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Assignment Actions // Actions
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void _assign(BuildContext context, PersonnelRecord soldier, PositionSlot slot) { void _assign(BuildContext context, PersonnelRecord soldier, PositionSlot slot) {
@ -711,172 +494,6 @@ class HomeScreen extends StatelessWidget {
context.read<PersonnelCubit>().unassignFromSlot(slot); 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) { void _runValidation(BuildContext context, UnitTreeLoaded treeState) {
final personnel = context.read<PersonnelCubit>().state is PersonnelLoaded final personnel = context.read<PersonnelCubit>().state is PersonnelLoaded
? (context.read<PersonnelCubit>().state as PersonnelLoaded).allPersonnel ? (context.read<PersonnelCubit>().state as PersonnelLoaded).allPersonnel
@ -1011,10 +628,7 @@ class _ExportDialogState extends State<_ExportDialog> {
color: Colors.red.shade900.withAlpha(80), color: Colors.red.shade900.withAlpha(80),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: Text( child: Text(_error!, style: const TextStyle(fontSize: 11, color: Colors.redAccent)),
_error!,
style: const TextStyle(fontSize: 11, color: Colors.redAccent),
),
), ),
], ],
], ],

View File

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

View File

@ -13,8 +13,6 @@ class PositionSlotCard extends StatelessWidget {
this.assignedSoldier, this.assignedSoldier,
this.onUnassign, this.onUnassign,
this.onAcceptDrop, this.onAcceptDrop,
this.onEdit,
this.onDelete,
}); });
final PositionSlot slot; final PositionSlot slot;
@ -22,12 +20,6 @@ class PositionSlotCard extends StatelessWidget {
final VoidCallback? onUnassign; final VoidCallback? onUnassign;
final void Function(PersonnelRecord soldier)? onAcceptDrop; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@ -75,48 +67,21 @@ class PositionSlotCard extends StatelessWidget {
fontWeight: slot.isVacant ? FontWeight.bold : FontWeight.normal, fontWeight: slot.isVacant ? FontWeight.bold : FontWeight.normal,
), ),
), ),
trailing: _buildTrailing(theme), 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,
), ),
); );
}, },
); );
} }
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) { Widget _buildMosBadge(ThemeData theme) {
return Container( return Container(
width: 40, width: 40,

View File

@ -642,27 +642,7 @@ This document breaks the master development plan into five structured phases spa
--- ---
## Phase 5: UI/CRUD Layer & Web Platform ✅ COMPLETE (2026-08-09) ## Phase 5: Platform Optimization, Profiling, & Hardening
> **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 ### Weeks 20 – 24

View File

@ -5,18 +5,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: "3b19a47f6ea7c2632760777c78174f47f6aec1e05f0cd611380d4593b8af1dbc" sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "96.0.0" version: "100.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "0c516bc4ad36a1a75759e54d5047cb9d15cded4459df01aa35a0b5ec7db2c2a0" sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.2.0" version: "13.0.0"
args: args:
dependency: transitive dependency: transitive
description: description:
@ -125,10 +125,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: cli_util name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c sha256: "71e43f1976c3eae2d9468a5b4d6600bf8cae61508b87f27fa1799228935d48ab"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.2" version: "0.5.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -181,34 +181,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.7" version: "3.1.9"
drift: drift:
dependency: "direct main" dependency: "direct main"
description: description:
name: drift name: drift
sha256: "970cd188fddb111b26ea6a9b07a62bf5c2432d74147b8122c67044ae3b97e99e" sha256: "3a3f1f6f905037d7426e4c445854139fd6a3d592135f7c96d7931682b73d16f4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.31.0" version: "2.34.3"
drift_dev: drift_dev:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: drift_dev name: drift_dev
sha256: "917184b2fb867b70a548a83bf0d36268423b38d39968c06cce4905683da49587" sha256: "735aad3c34215805c66bd518c8812fa4f83b5534b2c13c4092c970c04a7e9983"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.31.0" version: "2.34.5"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: c07120854742a0cae2f7501a0da02493addde550db6641d284983c08762e60a7
url: "https://pub.dev"
source: hosted
version: "0.2.8"
equatable: equatable:
dependency: "direct main" dependency: "direct main"
description: description:
@ -435,6 +427,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
native_toolchain_c:
dependency: transitive
description:
name: native_toolchain_c
sha256: f9c168717100ae6d9fee9ffb0be379bf1f8b26b0f6bcbd4fdddcd931993a6a72
url: "https://pub.dev"
source: hosted
version: "0.19.2"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@ -628,10 +628,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqlite3 name: sqlite3
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2" sha256: "64b2c63c8232dd20d14b34105a81ebfd74320442e8451f836179ec89986aa478"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.9.4" version: "3.5.1"
sqlite3_flutter_libs: sqlite3_flutter_libs:
dependency: "direct main" dependency: "direct main"
description: description:
@ -644,10 +644,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sqlparser name: sqlparser
sha256: "337e9997f7141ffdd054259128553c348635fa318f7ca492f07a4ab76f850d19" sha256: "772bb2f6f5bce0631a60f26b57d6e8882e1105c22345b05c163ee6ee5d7ba32d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.43.1" version: "0.45.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:

View File

@ -10,7 +10,6 @@ environment:
dependencies: dependencies:
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
drift: ^2.20.3 drift: ^2.20.3
drift_flutter: ^0.2.8
equatable: ^2.0.5 equatable: ^2.0.5
flutter: flutter:
sdk: flutter sdk: flutter

View File

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

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.