Init project
7
src-tauri/.gitignore
vendored
Normal 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
29
src-tauri/Cargo.toml
Normal 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
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
13
src-tauri/capabilities/default.json
Normal 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
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
69
src-tauri/src/export.rs
Normal 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
@@ -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!()
|
||||
}
|
||||
46
src-tauri/src/generator.rs
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||