diff --git a/.gitignore b/.gitignore index 6635cf5..dd688e3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ node_modules !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# exported schedules +*.docx +rota.txt diff --git a/README.md b/README.md index 954f388..2b04fe4 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,54 @@

rota

-Desktop app for generating monthly schedules for medical residents. The scheduling engine uses a parallel constraint-satisfaction DFS with a least-flexibility-first heuristic. +

+ Desktop app for generating monthly shift schedules for medical residents. +

-### Features -- Configurable residents, shift types, and forbidden pairings -- Negative shift requests and manual pre-assignments -- Fairness-aware workload distribution -- Export to `.docx` and `.txt` +

+ Tauri + Rust + SvelteKit + License: MIT +

-### Stack -- **Frontend**: SvelteKit + TypeScript + Tailwind CSS -- **Backend**: Rust (Tauri) +## Features -### Usage -```bash -just --list -``` +- **Configurable roster**: residents, their allowed shift types, and per-resident shift caps +- **Constraints**: forbidden pairings, day-off (negative) requests, and manual pre-assignments that the solver works around +- **Fairness**: workload, weekend/holiday, and per-shift-type loads are balanced across the team +- **Interactive swaps**: after generating, ask the engine for all legal swaps for any shift and apply one without breaking constraints +- **Export**: one click writes the schedule and per-resident metrics to `.docx` and `.txt` + +## How it works + +A month is laid out as a sequence of **slots**. Days alternate between two kinds of duty: + +| Day | Shift | Slots per day | +|-----|-------|---------------| +| odd | *open* | 2 | +| even | *closed* | 1 | + +The solver fills every slot with exactly one resident subject to the hard constraints: + +- no resident works two consecutive days +- forbidden ("toxic") pairs never share a day +- a resident is never placed on a day they requested off, or into a shift type they aren't allowed to take +- per-resident caps on total shifts, weekend/holiday shifts, and shifts of each type are respected + +The search is a **backtracking DFS** over the slots, with a few tricks to make it fast and produce good schedules: + +- **Least-flexibility-first**: at each slot, candidate residents are ordered by how few options they have left in the rest of the month, so the most constrained people get placed first (a fail-first / most-constrained heuristic). +- **Parallel roots**:the first slot's candidates are fanned out across threads with [rayon](https://github.com/rayon-rs/rayon); the first thread to find a complete schedule wins and signals the others to stop. +- **Timeout**: the search aborts after 5s and reports that no solution was found rather than hanging. + +## Getting started + +### Prerequisites + +- [Rust](https://www.rust-lang.org/tools/install) (stable) and the [Tauri 2 system dependencies](https://tauri.app/start/prerequisites/) for your OS +- [Node.js](https://nodejs.org/) and [pnpm](https://pnpm.io/) +- [`just`](https://github.com/casey/just) (optional, for the shortcuts below) + +## Commands + +Run `just --list` to see every recipe. diff --git a/justfile b/justfile index f9b45f8..152777d 100644 --- a/justfile +++ b/justfile @@ -1,36 +1,58 @@ -build: - pnpm tauri build +tauri_path := "src-tauri" +# Launch the desktop app with hot reload +[group('dev')] dev: pnpm tauri dev -tauri_path := "src-tauri" +# Produce a release bundle +[group('dev')] +build: + pnpm tauri build +# Run the Rust unit tests +[group('test')] +test: + cd {{tauri_path}} && cargo test --lib -- --test-threads=1 + +# Run the end-to-end solver tests +[group('test')] +test-integration: + cd {{tauri_path}} && cargo test --test integration -- --test-threads=1 --nocapture + +# Run the full suite in release mode +[group('test')] +test-all: + cd {{tauri_path}} && cargo test --release + +# Benchmark the parallel search (criterion) +[group('test')] +bench: + cd {{tauri_path}} && cargo bench + +# Measure coverage via cargo-llvm-cov +[group('test')] +cov: + cd {{tauri_path}} && cargo llvm-cov run + +# Run mutation testing via cargo-mutants +[group('test')] +mutants: + cd {{tauri_path}} && cargo mutants + +# Format frontend (prettier) + backend (cargo fmt) +[group('quality')] fmt: pnpm prettier --write "**/*.svelte" "**/*.ts" cd {{tauri_path}} && cargo fmt +# Lint the backend (cargo clippy) +[group('quality')] lint: cd {{tauri_path}} && cargo clippy -test: - cd {{tauri_path}} && cargo test --lib -- --test-threads=1 - -test-integration: - cd {{tauri_path}} && cargo test --test integration -- --test-threads=1 --nocapture - -test-all: - cd {{tauri_path}} && cargo test --release - -bench: - cd {{tauri_path}} && cargo bench - -cov: - cd {{tauri_path}} && cargo llvm-cov run - -mutants: - cd {{tauri_path}} && cargo mutants - +# Remove node_modules and cargo build artifacts +[group('misc')] clean: rm -rf node_modules cd {{tauri_path}} && cargo clean