Implement DTOs, add managed state for schedule, export to txt
This commit is contained in:
@@ -1,15 +1,25 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::Month;
|
use chrono::Month;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
resident::Resident,
|
resident::{Resident, ResidentDTO},
|
||||||
schedule::{MonthlySchedule, ShiftType},
|
schedule::{MonthlySchedule, ShiftType},
|
||||||
slot::{Day, ShiftPosition, Slot},
|
slot::{Day, ShiftPosition, Slot},
|
||||||
};
|
};
|
||||||
|
|
||||||
const YEAR: i32 = 2026;
|
const YEAR: i32 = 2026;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct UserConfigDTO {
|
||||||
|
month: usize,
|
||||||
|
year: i32,
|
||||||
|
holidays: Vec<usize>,
|
||||||
|
residents: Vec<ResidentDTO>,
|
||||||
|
toxic_pairs: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct UserConfig {
|
pub struct UserConfig {
|
||||||
month: Month,
|
month: Month,
|
||||||
@@ -36,6 +46,18 @@ impl UserConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_dto(dto: UserConfigDTO) -> Self {
|
||||||
|
Self {
|
||||||
|
month: Month::try_from(dto.month as u8).unwrap(),
|
||||||
|
year: dto.year,
|
||||||
|
holidays: dto.holidays,
|
||||||
|
residents: dto.residents.into_iter().map(Resident::from_dto).collect(),
|
||||||
|
toxic_pairs: dto.toxic_pairs,
|
||||||
|
workload_limits: HashMap::new(),
|
||||||
|
holiday_limits: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_holidays(mut self, holidays: Vec<usize>) -> Self {
|
pub fn with_holidays(mut self, holidays: Vec<usize>) -> Self {
|
||||||
self.holidays = holidays;
|
self.holidays = holidays;
|
||||||
self
|
self
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
// here lies the logic for the export of the final schedule into docx/pdf formats
|
// here lies the logic for the export of the final schedule into docx/pdf formats
|
||||||
|
|
||||||
use crate::schedule::MonthlySchedule;
|
use std::{fs::File, io::Write};
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::UserConfig,
|
||||||
|
schedule::MonthlySchedule,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum FileType {
|
pub enum FileType {
|
||||||
|
Txt,
|
||||||
Json,
|
Json,
|
||||||
Csv,
|
Csv,
|
||||||
Doc,
|
Doc,
|
||||||
@@ -11,16 +19,17 @@ pub enum FileType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait Export {
|
pub trait Export {
|
||||||
fn export(&self, file_type: FileType);
|
fn export(&self, file_type: FileType, config: &UserConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Export for MonthlySchedule {
|
impl Export for MonthlySchedule {
|
||||||
fn export(&self, file_type: FileType) {
|
fn export(&self, file_type: FileType, config: &UserConfig) {
|
||||||
match file_type {
|
match file_type {
|
||||||
FileType::Csv => self.export_as_csv(),
|
FileType::Txt => self.export_as_txt(config),
|
||||||
FileType::Json => self.export_as_json(),
|
FileType::Csv => self.export_as_csv(config),
|
||||||
FileType::Doc => self.export_as_doc(),
|
FileType::Json => self.export_as_json(config),
|
||||||
FileType::Pdf => self.export_as_pdf(),
|
FileType::Doc => self.export_as_doc(config),
|
||||||
|
FileType::Pdf => self.export_as_pdf(config),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: make this env var from a config file? Option to change this in-app
|
// TODO: make this env var from a config file? Option to change this in-app
|
||||||
@@ -29,27 +38,35 @@ impl Export for MonthlySchedule {
|
|||||||
"exported type {:?}. Saved at folder path {}",
|
"exported type {:?}. Saved at folder path {}",
|
||||||
file_type, env_path
|
file_type, env_path
|
||||||
);
|
);
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MonthlySchedule {
|
impl MonthlySchedule {
|
||||||
// return error result as string or nothing, maybe use anyhow
|
pub fn export_as_txt(&self, config: &UserConfig) -> String {
|
||||||
// or just return a string.. for now
|
let file = File::create("schedule.txt").unwrap();
|
||||||
|
let mut writer = std::io::BufWriter::new(file);
|
||||||
|
writer
|
||||||
|
.write_all(self.pretty_print(config).as_bytes())
|
||||||
|
.expect("Failed to write to buffer");
|
||||||
|
|
||||||
pub fn export_as_csv(&self) -> String {
|
writer.flush().expect("Failed to flush buffer");
|
||||||
|
info!("im here");
|
||||||
|
"ok".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_as_csv(&self, config: &UserConfig) -> String {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn export_as_json(&self) -> String {
|
pub fn export_as_json(&self, config: &UserConfig) -> String {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn export_as_doc(&self) -> String {
|
pub fn export_as_doc(&self, config: &UserConfig) -> String {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn export_as_pdf(&self) -> String {
|
pub fn export_as_pdf(&self, config: &UserConfig) -> String {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::UserConfig,
|
config::{UserConfig, UserConfigDTO},
|
||||||
export::{Export, FileType},
|
export::{Export, FileType},
|
||||||
generator::backtracking,
|
generator::backtracking,
|
||||||
schedule::MonthlySchedule,
|
schedule::MonthlySchedule,
|
||||||
@@ -16,35 +18,46 @@ mod resident;
|
|||||||
mod schedule;
|
mod schedule;
|
||||||
mod slot;
|
mod slot;
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
schedule: Mutex<MonthlySchedule>,
|
||||||
|
}
|
||||||
|
|
||||||
/// argument to this must be the rota state including all
|
/// argument to this must be the rota state including all
|
||||||
/// information for the residents, the forbidden pairs the holidays
|
/// information for the residents, the forbidden pairs the holidays
|
||||||
/// and the period of the schedule
|
/// and the period of the schedule
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn generate() -> String {
|
fn generate(config: UserConfigDTO, state: tauri::State<'_, AppState>) -> MonthlySchedule {
|
||||||
let config = UserConfig::default();
|
info!("{:?}", config);
|
||||||
|
let mut config = UserConfig::from_dto(config);
|
||||||
|
config.calculate_workload_limits();
|
||||||
|
config.calculate_holiday_limits();
|
||||||
let mut schedule = MonthlySchedule::new();
|
let mut schedule = MonthlySchedule::new();
|
||||||
schedule.prefill(&config);
|
schedule.prefill(&config);
|
||||||
|
|
||||||
backtracking(&mut schedule, Slot::default(), &config);
|
|
||||||
info!("{}", schedule.pretty_print(&config));
|
info!("{}", schedule.pretty_print(&config));
|
||||||
|
|
||||||
schedule.export_as_json()
|
let solved = backtracking(&mut schedule, Slot::default(), &config);
|
||||||
|
let mut internal_schedule = state.schedule.lock().unwrap();
|
||||||
|
*internal_schedule = schedule.clone();
|
||||||
|
assert!(solved);
|
||||||
|
info!("{}", schedule.pretty_print(&config));
|
||||||
|
|
||||||
|
schedule
|
||||||
}
|
}
|
||||||
|
|
||||||
/// export into docx
|
/// export into docx
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn export() -> String {
|
fn export(config: UserConfigDTO, state: tauri::State<'_, AppState>) {
|
||||||
let schedule = MonthlySchedule::new();
|
let config = UserConfig::from_dto(config);
|
||||||
let filetype = FileType::Doc;
|
let schedule = state.schedule.lock().unwrap();
|
||||||
|
schedule.export(FileType::Txt, &config);
|
||||||
schedule.export(filetype);
|
|
||||||
|
|
||||||
todo!()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.manage(AppState {
|
||||||
|
schedule: Mutex::new(MonthlySchedule::new()),
|
||||||
|
})
|
||||||
.plugin(
|
.plugin(
|
||||||
tauri_plugin_log::Builder::new()
|
tauri_plugin_log::Builder::new()
|
||||||
.level(tauri_plugin_log::log::LevelFilter::Info)
|
.level(tauri_plugin_log::log::LevelFilter::Info)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
schedule::ShiftType,
|
schedule::ShiftType,
|
||||||
slot::{Day, Slot},
|
slot::{Day, ShiftPosition, Slot},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
@@ -17,6 +17,18 @@ pub struct Resident {
|
|||||||
pub reduced_load: bool,
|
pub reduced_load: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ResidentDTO {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
negative_shifts: Vec<usize>,
|
||||||
|
manual_shifts: Vec<Slot>,
|
||||||
|
max_shifts: Option<usize>,
|
||||||
|
allowed_types: Vec<ShiftType>,
|
||||||
|
reduced_load: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl Resident {
|
impl Resident {
|
||||||
pub fn new(id: &str, name: &str) -> Self {
|
pub fn new(id: &str, name: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -43,4 +55,30 @@ impl Resident {
|
|||||||
self.reduced_load = true;
|
self.reduced_load = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_dto(dto: ResidentDTO) -> Self {
|
||||||
|
Self {
|
||||||
|
id: dto.id,
|
||||||
|
name: dto.name,
|
||||||
|
negative_shifts: dto
|
||||||
|
.negative_shifts
|
||||||
|
.into_iter()
|
||||||
|
.map(|d| Day(d as u8))
|
||||||
|
.collect(),
|
||||||
|
manual_shifts: dto
|
||||||
|
.manual_shifts
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| {
|
||||||
|
Slot {
|
||||||
|
day: s.day,
|
||||||
|
// FIXME: frontend always brings resident manual shifts as first
|
||||||
|
position: ShiftPosition::First,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
max_shifts: dto.max_shifts,
|
||||||
|
allowed_types: dto.allowed_types,
|
||||||
|
reduced_load: dto.reduced_load,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,33 @@ use std::collections::HashMap;
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::UserConfig,
|
config::UserConfig,
|
||||||
resident::Resident,
|
resident::Resident,
|
||||||
slot::{Day, Slot},
|
slot::{Day, ShiftPosition, Slot},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use serde::Serializer;
|
||||||
|
|
||||||
|
impl Serialize for MonthlySchedule {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
use serde::ser::SerializeMap;
|
||||||
|
let mut map = serializer.serialize_map(Some(self.0.len()))?;
|
||||||
|
for (slot, name) in &self.0 {
|
||||||
|
let pos_str = match slot.position {
|
||||||
|
ShiftPosition::First => "First",
|
||||||
|
ShiftPosition::Second => "Second",
|
||||||
|
};
|
||||||
|
let key = format!("{}-{}", slot.day.0, pos_str);
|
||||||
|
map.serialize_entry(&key, name)?;
|
||||||
|
}
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// each slot has one resident
|
// each slot has one resident
|
||||||
// a day can span between 1 or 2 slots depending on if it is open(odd) or closed(even)
|
// a day can span between 1 or 2 slots depending on if it is open(odd) or closed(even)
|
||||||
#[derive(Debug)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct MonthlySchedule(HashMap<Slot, String>);
|
pub struct MonthlySchedule(HashMap<Slot, String>);
|
||||||
|
|
||||||
impl MonthlySchedule {
|
impl MonthlySchedule {
|
||||||
@@ -179,7 +200,7 @@ impl MonthlySchedule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||||
pub enum ShiftType {
|
pub enum ShiftType {
|
||||||
Closed,
|
Closed,
|
||||||
OpenFirst,
|
OpenFirst,
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { Calendar, Popover } from "bits-ui";
|
import { trace } from "@tauri-apps/plugin-log";
|
||||||
import { CalendarDate, getDayOfWeek } from "@internationalized/date";
|
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
|
||||||
import { warn, debug, trace, info, error } from "@tauri-apps/plugin-log";
|
|
||||||
|
|
||||||
import Basic from "./components/configurations/basic.svelte";
|
import Basic from "./components/configurations/basic.svelte";
|
||||||
import Residents from "./components/configurations/residents.svelte";
|
import Residents from "./components/configurations/residents.svelte";
|
||||||
@@ -99,7 +96,7 @@
|
|||||||
<div class="h-1.5 w-full overflow-hidden rounded-full bg-zinc-200">
|
<div class="h-1.5 w-full overflow-hidden rounded-full bg-zinc-200">
|
||||||
<div
|
<div
|
||||||
class="h-full bg-emerald-600 transition-all duration-500"
|
class="h-full bg-emerald-600 transition-all duration-500"
|
||||||
style="width: {(rota.currentStep / steps.length) * 100}%"
|
style="width: {(((rota.currentStep - 1) / (steps.length - 1)) * 100).toFixed(0)}%"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { CalendarDate } from "@internationalized/date";
|
|
||||||
import { trace } from "@tauri-apps/plugin-log";
|
|
||||||
import { Calendar, Popover } from "bits-ui";
|
import { Calendar, Popover } from "bits-ui";
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
import { rota, steps } from "../../state.svelte.js";
|
import { rota, steps } from "../../state.svelte.js";
|
||||||
|
|||||||
@@ -321,7 +321,7 @@
|
|||||||
<div class="flex flex-col items-center space-y-2 px-4">
|
<div class="flex flex-col items-center space-y-2 px-4">
|
||||||
<p class="text-[10px] font-black tracking-widest text-zinc-500 uppercase">Shift Types</p>
|
<p class="text-[10px] font-black tracking-widest text-zinc-500 uppercase">Shift Types</p>
|
||||||
<div class="flex gap-1">
|
<div class="flex gap-1">
|
||||||
{#each ["OpenAsFirst", "OpenAsSecond", "Closed"] as type}
|
{#each ["Closed", "OpenFirst", "OpenSecond"] as type}
|
||||||
{@const active = resident.allowedTypes.includes(type)}
|
{@const active = resident.allowedTypes.includes(type)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -348,14 +348,14 @@
|
|||||||
Reduced Workload
|
Reduced Workload
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onclick={() => (resident.hasReducedLoad = !resident.hasReducedLoad)}
|
onclick={() => (resident.reducedLoad = !resident.reducedLoad)}
|
||||||
class="flex items-center gap-2 rounded-lg border px-3 py-1 transition-all hover:border-blue-200 hover:bg-white
|
class="flex items-center gap-2 rounded-lg border px-3 py-1 transition-all hover:border-blue-200 hover:bg-white
|
||||||
{resident.hasReducedLoad
|
{resident.reducedLoad
|
||||||
? 'border-green-200 bg-green-50 text-green-700'
|
? 'border-green-200 bg-green-50 text-green-700'
|
||||||
: 'border-zinc-200 bg-white text-zinc-500'}"
|
: 'border-zinc-200 bg-white text-zinc-500'}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="size-2 rounded-full {resident.hasReducedLoad
|
class="size-2 rounded-full {resident.reducedLoad
|
||||||
? 'animate-pulse bg-green-500'
|
? 'animate-pulse bg-green-500'
|
||||||
: 'bg-zinc-300'}"
|
: 'bg-zinc-300'}"
|
||||||
></div>
|
></div>
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Button } from "$lib/components/ui/button/index.js";
|
import { Button } from "$lib/components/ui/button/index.js";
|
||||||
import { rota, steps } from "../../state.svelte.js";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { type MonthlyScheduleDTO, rota, steps, type ShiftPosition } from "../../state.svelte.js";
|
||||||
|
|
||||||
function getResidentName(day: number, slot: number) {
|
function getResidentName(day: number, pos: ShiftPosition) {
|
||||||
|
const residentId = rota.solution[`${day}-${pos}`];
|
||||||
|
const r = rota.findResident(residentId);
|
||||||
|
if (r) return r.name;
|
||||||
|
|
||||||
|
// check for manual
|
||||||
const assignedResidents = rota.residents.filter((resident) =>
|
const assignedResidents = rota.residents.filter((resident) =>
|
||||||
resident.manualShifts.some(
|
resident.manualShifts.some(
|
||||||
(shift) =>
|
(shift) =>
|
||||||
@@ -12,6 +18,13 @@
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let slot;
|
||||||
|
if (pos == "First") {
|
||||||
|
slot = 1;
|
||||||
|
} else {
|
||||||
|
slot = 2;
|
||||||
|
}
|
||||||
|
|
||||||
const resident = assignedResidents[slot - 1];
|
const resident = assignedResidents[slot - 1];
|
||||||
return resident ? resident.name : "-";
|
return resident ? resident.name : "-";
|
||||||
}
|
}
|
||||||
@@ -21,23 +34,31 @@
|
|||||||
return day % 2 === 0 ? 1 : 2;
|
return day % 2 === 0 ? 1 : 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: invoke rust?
|
async function generate() {
|
||||||
// const resident = {
|
let config = rota.toDTO();
|
||||||
// id: crypto.randomUUID(),
|
console.log(config);
|
||||||
// name: "",
|
|
||||||
// negativeShifts: [] as CalendarDate[],
|
|
||||||
// manualShifts: [] as CalendarDate[]
|
|
||||||
// };
|
|
||||||
|
|
||||||
// try {
|
try {
|
||||||
// let replyFrom = await invoke("add_resident", { resident });
|
let schedule = await invoke<MonthlyScheduleDTO>("generate", { config });
|
||||||
// console.log("Result:", replyFrom);
|
console.log("replyFromGenerate:", schedule);
|
||||||
// residents = [...residents, resident];
|
rota.solution = schedule;
|
||||||
// } catch (error) {
|
} catch (error) {
|
||||||
// console.error("Error:", error);
|
console.error("Error:", error);
|
||||||
// }
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function export_file() {
|
||||||
|
let config = rota.toDTO();
|
||||||
|
let schedule = rota.solution;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke("export", { config, schedule });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<div class="mb-6 flex items-center justify-between">
|
||||||
<h2 class="text-2xl font-bold text-zinc-800">{steps[rota.currentStep - 1].title}</h2>
|
<h2 class="text-2xl font-bold text-zinc-800">{steps[rota.currentStep - 1].title}</h2>
|
||||||
<div class="justify-end">
|
<div class="justify-end">
|
||||||
@@ -78,15 +99,15 @@
|
|||||||
onclick={() => "handleCellClick(day, 1)"}
|
onclick={() => "handleCellClick(day, 1)"}
|
||||||
class="w-full overflow-hidden rounded border border-pink-200 bg-pink-50 px-1.5 py-1 text-left text-[10px] font-bold text-pink-600 transition-colors hover:bg-pink-100"
|
class="w-full overflow-hidden rounded border border-pink-200 bg-pink-50 px-1.5 py-1 text-left text-[10px] font-bold text-pink-600 transition-colors hover:bg-pink-100"
|
||||||
>
|
>
|
||||||
{getResidentName(day, 1)}
|
{getResidentName(day, "First")}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if slotCount == 2}
|
{#if slotCount > 1}
|
||||||
<button
|
<button
|
||||||
onclick={() => "handleCellClick(day, 2)"}
|
onclick={() => "handleCellClick(day, 2)"}
|
||||||
class="w-full overflow-hidden rounded border border-emerald-200 bg-emerald-50 px-1.5 py-1 text-left text-[10px] font-bold text-emerald-600 transition-colors hover:bg-emerald-100"
|
class="w-full overflow-hidden rounded border border-emerald-200 bg-emerald-50 px-1.5 py-1 text-left text-[10px] font-bold text-emerald-600 transition-colors hover:bg-emerald-100"
|
||||||
>
|
>
|
||||||
{getResidentName(day, 2)}
|
{getResidentName(day, "Second")}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -99,13 +120,14 @@
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="border-zinc-200 bg-white text-zinc-600 shadow-sm transition-all hover:bg-zinc-50 hover:text-zinc-900 active:scale-95"
|
class="border-zinc-200 bg-white text-zinc-600 shadow-sm transition-all hover:bg-zinc-50 hover:text-zinc-900 active:scale-95"
|
||||||
|
onclick={() => generate()}
|
||||||
>
|
>
|
||||||
Ανακατανομή
|
Ανακατανομή
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onclick={() => "export adasds"}
|
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="border-zinc-200 bg-white text-zinc-600 shadow-sm transition-all hover:bg-zinc-50 hover:text-zinc-900 active:scale-95"
|
class="border-zinc-200 bg-white text-zinc-600 shadow-sm transition-all hover:bg-zinc-50 hover:text-zinc-900 active:scale-95"
|
||||||
|
onclick={() => export_file()}
|
||||||
>
|
>
|
||||||
Εξαγωγή</Button
|
Εξαγωγή</Button
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
return day % 2 === 0 ? 1 : 2;
|
return day % 2 === 0 ? 1 : 2;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-6 flex items-center justify-between">
|
<div class="mb-6 flex items-center justify-between">
|
||||||
<h2 class="text-2xl font-bold text-zinc-800">{steps[rota.currentStep - 1].title}</h2>
|
<h2 class="text-2xl font-bold text-zinc-800">{steps[rota.currentStep - 1].title}</h2>
|
||||||
<div class="justify-end">
|
<div class="justify-end">
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
// state.svelte.ts
|
// state.svelte.ts
|
||||||
import { CalendarDate, getDayOfWeek } from "@internationalized/date";
|
import { CalendarDate, getDayOfWeek } from "@internationalized/date";
|
||||||
import { trace } from "@tauri-apps/plugin-log";
|
|
||||||
|
|
||||||
export interface Resident {
|
export interface Resident {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -9,7 +8,7 @@ export interface Resident {
|
|||||||
manualShifts: CalendarDate[];
|
manualShifts: CalendarDate[];
|
||||||
maxShifts: number | undefined;
|
maxShifts: number | undefined;
|
||||||
allowedTypes: string[];
|
allowedTypes: string[];
|
||||||
hasReducedLoad: boolean;
|
reducedLoad: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ForbiddenPair {
|
export interface ForbiddenPair {
|
||||||
@@ -30,6 +29,8 @@ export class RotaState {
|
|||||||
daysArray = $derived(Array.from({ length: this.projectMonthDays }, (_, i) => i + 1));
|
daysArray = $derived(Array.from({ length: this.projectMonthDays }, (_, i) => i + 1));
|
||||||
emptySlots = $derived(Array.from({ length: getDayOfWeek(this.projectMonth, "en-GB") }));
|
emptySlots = $derived(Array.from({ length: getDayOfWeek(this.projectMonth, "en-GB") }));
|
||||||
|
|
||||||
|
solution = $state<MonthlySchedule>({} as MonthlySchedule);
|
||||||
|
|
||||||
addResident() {
|
addResident() {
|
||||||
this.residents.push({
|
this.residents.push({
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
@@ -37,13 +38,17 @@ export class RotaState {
|
|||||||
negativeShifts: [],
|
negativeShifts: [],
|
||||||
manualShifts: [],
|
manualShifts: [],
|
||||||
maxShifts: undefined,
|
maxShifts: undefined,
|
||||||
allowedTypes: ["OpenAsFirst", "OpenAsSecond", "Closed"],
|
allowedTypes: ["Closed", "OpenFirst", "OpenSecond"],
|
||||||
hasReducedLoad: false
|
reducedLoad: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
removeResident(id: string) {
|
removeResident(id: string) {
|
||||||
this.residents = this.residents.filter((p) => p.id !== id);
|
this.residents = this.residents.filter((r) => r.id !== id);
|
||||||
|
}
|
||||||
|
|
||||||
|
findResident(id: string) {
|
||||||
|
return this.residents.find((r) => r.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
addForbiddenPair() {
|
addForbiddenPair() {
|
||||||
@@ -57,10 +62,61 @@ export class RotaState {
|
|||||||
removeForbiddenPair(index: number) {
|
removeForbiddenPair(index: number) {
|
||||||
this.forbiddenPairs.splice(index, 1);
|
this.forbiddenPairs.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toDTO(): UserConfigDTO {
|
||||||
|
return {
|
||||||
|
month: this.selectedMonth,
|
||||||
|
year: this.selectedYear,
|
||||||
|
holidays: this.holidays.map((d) => d.day),
|
||||||
|
residents: this.residents.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
negativeShifts: r.negativeShifts.map((d) => d.day),
|
||||||
|
manualShifts: r.manualShifts.map((s) => ({
|
||||||
|
day: s.day,
|
||||||
|
position: "First"
|
||||||
|
})),
|
||||||
|
maxShifts: r.maxShifts ?? null,
|
||||||
|
allowedTypes: r.allowedTypes as ShiftType[],
|
||||||
|
reducedLoad: r.reducedLoad
|
||||||
|
})),
|
||||||
|
|
||||||
|
toxic_pairs: this.forbiddenPairs.map((pair) => [pair.id1, pair.id2])
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rota = new RotaState();
|
export const rota = new RotaState();
|
||||||
|
|
||||||
|
export type MonthlyScheduleDTO = {
|
||||||
|
schedule: Record<number, Record<ShiftPosition, string>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserConfigDTO = {
|
||||||
|
month: number;
|
||||||
|
year: number;
|
||||||
|
holidays: Array<number>;
|
||||||
|
residents: Array<ResidentDTO>;
|
||||||
|
toxic_pairs: Array<[string, string]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ResidentDTO = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
negativeShifts: Array<number>;
|
||||||
|
manualShifts: Array<{ day: number; position: ShiftPosition }>;
|
||||||
|
maxShifts: number | null;
|
||||||
|
allowedTypes: Array<ShiftType>;
|
||||||
|
reducedLoad: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShiftType = "Closed" | "OpenFirst" | "OpenSecond";
|
||||||
|
|
||||||
|
export type ShiftPosition = "First" | "Second";
|
||||||
|
|
||||||
|
export type SlotKey = `${number}-${"First" | "Second"}`;
|
||||||
|
export type MonthlySchedule = { [key: SlotKey]: string };
|
||||||
|
|
||||||
export const steps = [
|
export const steps = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user