Init project

This commit is contained in:
2025-12-26 11:25:45 +02:00
commit 8f8fc50310
79 changed files with 9970 additions and 0 deletions

7
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

5526
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

29
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,29 @@
[package]
name = "rota"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "rota_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
chrono = "0.4.42"
itertools = "0.14.0"
rstest = "0.26.1"
tauri-plugin-log = "2"
log = "0.4.29"

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": [
"main"
],
"permissions": [
"core:default",
"opener:default",
"log:default"
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

69
src-tauri/src/export.rs Normal file
View File

@@ -0,0 +1,69 @@
// here lies the logic for the export of the final schedule into docx/pdf formats
use crate::model::{MonthlySchedule, WeeklySchedule};
#[derive(Debug)]
pub enum FileType {
Json,
Csv,
Doc,
Pdf,
}
pub trait Export {
fn export(&self, file_type: FileType);
}
impl Export for MonthlySchedule {
fn export(&self, file_type: FileType) {
match file_type {
FileType::Csv => self.export_as_csv(),
FileType::Json => self.export_as_json(),
FileType::Doc => self.export_as_doc(),
FileType::Pdf => self.export_as_pdf(),
};
// TODO: make this env var from a config file? Option to change this in-app
let env_path = "rota/me/";
println!(
"exported type {:?}. Saved at folder path {}",
file_type, env_path
);
todo!()
}
}
impl MonthlySchedule {
// return error result as string or nothing, maybe use anyhow
// or just return a string.. for now
pub fn export_as_csv(&self) -> String {
todo!()
}
pub fn export_as_json(&self) -> String {
todo!()
}
pub fn export_as_doc(&self) -> String {
todo!()
}
pub fn export_as_pdf(&self) -> String {
todo!()
}
}
impl Export for WeeklySchedule {
fn export(&self, file_type: FileType) {
todo!()
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
#[rstest]
pub fn xxxy() {}
}

36
src-tauri/src/fairness.rs Normal file
View File

@@ -0,0 +1,36 @@
// list of algos/methods that make sure the state of the schedule is still fair
use crate::model::{Day, MonthlySchedule, Resident};
// return yes if any resident has a shift in back to bacak days
// TODO: performance: this could only check the current recursion resident otherwise we would have cut the branch earlier
pub fn back_to_back_shifts(schedule: &MonthlySchedule) -> bool {
schedule.contains_any_consecutive_shifts()
}
// return yes if the same resident has a shift on a sunday and also the next saturday
pub fn sunday_and_next_saturday_shifts(schedule: &MonthlySchedule, resident: &Resident) -> bool {
todo!()
}
// find if a pair exists doing a shift at the same day, if yes return true
pub fn pair_exists(
schedule: &MonthlySchedule,
resident_a: &Resident,
resident_b: &Resident,
) -> bool {
todo!()
}
// if day is odd then open otherwise closed
pub fn is_closed(day: &Day) -> bool {
todo!()
}
// here include:
// if total shifts are 50 and there are 5 residents BUT this resident has reduced workload, he should to 9 instead of 10
// if resident has specific shift types then see he is not put in a wrong shift type
// if resident has a max limit of shifts see that he is not doing more
pub fn are_personal_restrictions_met(resident: &Resident) -> bool {
todo!()
}

View File

@@ -0,0 +1,46 @@
//! Generator
//!
//! here lies the schedule generator which uses a simple backtracking algorithm
use crate::model::{MonthlySchedule, Resident};
// schedule param contains the schedule with maybe some manually input days
// returns the complete schedule after applying all rules/restrictions/fairness
fn generate(schedule: &MonthlySchedule) -> MonthlySchedule {
todo!()
}
// make part of Schedule
// examines Schedule for validity, if no then backtrack
fn is_state_valid() {
todo!()
}
// From https://en.wikipedia.org/wiki/Backtracking
// Recursively fill a partially filled MonthlySchedule until shifts are set for all days of the month
// Collection of Resident is immutable and only there for reference
// TODO: we can create a struct `Generator` that contains the collection of Resident upon initialization and then call self.people inside self.backtracking
// Returns error if there is no solution for the specific set of constaints
fn backtracking(schedule: &mut MonthlySchedule, people: &Vec<Resident>, mut day: usize) {
if schedule.restrictions_violated() {
log::info!("restrictions_violated due to...");
return;
}
if schedule.is_filled() {
log::info!("..?");
return;
}
for resident in people {}
todo!()
}
#[cfg(test)]
mod tests {
use rstest::rstest;
#[rstest]
pub fn xxxt() {}
}

82
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,82 @@
use crate::{
export::{Export, FileType},
model::{MonthlySchedule, Resident},
};
mod export;
mod fairness;
mod generator;
mod model;
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[tauri::command]
fn set_restrictions() -> String {
"A new rota begins".to_string()
}
#[tauri::command]
fn add_resident(resident: Resident) -> String {
log::info!("hi resident {:?}", resident);
format!("{:?}", resident)
}
#[tauri::command]
fn generate() -> String {
"Generating new rota".to_string()
}
// takes the list of active residents and their configurations
// providing svelte with all possible info for swapping between residents shifts
// returns the updated resident data
#[tauri::command]
fn possible_swap_locations(people: Vec<Resident>) -> Vec<Resident> {
log::info!("Fetch possible swap locations for people: {:?}", people);
people
}
#[tauri::command]
fn export() -> String {
// param must have filetype as string from svelte
// somehow get the current schedule
let rota = MonthlySchedule::new(10);
let _ = rota.export(FileType::Json);
// locally store the _?
todo!()
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
log::info!("hi");
tauri::Builder::default()
.plugin(
tauri_plugin_log::Builder::new()
.level(tauri_plugin_log::log::LevelFilter::Info)
.build(),
)
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_log::Builder::new().build())
.invoke_handler(tauri::generate_handler![
greet,
set_restrictions,
add_resident,
generate,
possible_swap_locations,
export
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#[cfg(test)]
mod tests {
use rstest::rstest;
#[rstest]
pub fn xxx() {}
}

6
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
rota_lib::run()
}

159
src-tauri/src/model.rs Normal file
View File

@@ -0,0 +1,159 @@
use std::collections::HashMap;
use chrono::Month;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
// We're always talking about entities that are created for the single month of the rota generation
const YEAR: i32 = 2026;
pub struct Configurations {
month: Month,
}
impl Configurations {
pub fn new(month: usize) -> Self {
Self {
month: Month::try_from(month as u8).unwrap(),
}
}
pub fn total_days(&self) -> u8 {
self.month.num_days(YEAR).unwrap()
}
// open shfits -> 2 people per night
pub fn total_open_shifts(&self) -> u8 {
let mut total_open_shifts = 0;
for i in 1..=self.total_days() {
if i % 2 != 0 {
total_open_shifts += 1;
}
}
total_open_shifts
}
// closed shifts -> 1 resident per night
pub fn total_closed_shifts(&self) -> u8 {
self.total_days() - self.total_open_shifts()
}
}
pub struct MonthlySchedule {
rotation: Vec<Shift>,
}
impl MonthlySchedule {
pub fn new(days_of_month: usize) -> Self {
Self { rotation: vec![] }
}
pub fn restrictions_violated(&self) -> bool {
todo!()
}
pub fn is_filled(&self) -> bool {
todo!()
}
pub fn contains_any_consecutive_shifts(&self) -> bool {
// self.rotation
// .into_iter()
// .sorted_unstable_by_key(|shift| shift.day)
// .tuple_windows()
// .any(|((_, (p1, p1_opt)), (_, (p2, p2_opt)))| {
// let p1_name = &p1.name;
// let p1_opt_name = p1_opt
// .as_ref()
// .unwrap_or(&Resident::new(".", "-p1"))
// .name
// .clone();
// let p2_name = &p2.name;
// let p2_opt_name = p2_opt
// .as_ref()
// .unwrap_or(&Resident::new(".", "-p2"))
// .name
// .clone();
// p1_name == p2_name
// || p1_name == &p2_opt_name
// || p2_name == &p1_opt_name
// || p1_opt_name == p2_opt_name
// });
todo!()
}
// pub fn
}
#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Day(usize);
impl Day {
pub fn is_open_shift(&self) -> bool {
self.0.is_multiple_of(2)
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Resident {
id: String,
name: String,
negative_shifts: Vec<Day>,
// manual days on
manual_shifts: Vec<Day>,
max_shifts: Option<usize>,
allowed_types: Vec<ShiftType>,
reduced_load: bool,
}
impl Resident {
pub fn new(id: &str, name: &str) -> Self {
Self {
id: id.to_string(),
name: name.to_string(),
negative_shifts: Vec::new(),
manual_shifts: Vec::new(),
max_shifts: None,
allowed_types: vec![
ShiftType::OpenFirst,
ShiftType::OpenSecond,
ShiftType::Closed,
],
reduced_load: false,
}
}
}
pub struct WeeklySchedule {
// todo
}
impl WeeklySchedule {
// todo
}
pub struct Shift {
day: Day,
resident_a: Resident,
resident_b: Option<Resident>,
}
#[derive(Serialize, Deserialize, Debug)]
pub enum ShiftType {
Closed,
OpenFirst,
OpenSecond,
}
#[cfg(test)]
mod tests {
use rstest::rstest;
#[rstest]
pub fn xxx() {}
}

39
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,39 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "rota",
"version": "0.1.0",
"identifier": "com.stef.rota",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../build"
},
"app": {
"windows": [
{
"title": "rota",
"width": 1280,
"height": 800,
"minWidth": 1024,
"minHeight": 768,
"center": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}