Sync progress
This commit is contained in:
@@ -2,45 +2,65 @@
|
|||||||
//!
|
//!
|
||||||
//! here lies the schedule generator which uses a simple backtracking algorithm
|
//! here lies the schedule generator which uses a simple backtracking algorithm
|
||||||
|
|
||||||
use crate::model::{MonthlySchedule, Resident};
|
use crate::model::{Configurations, Day, MonthlySchedule, Slot};
|
||||||
|
|
||||||
// schedule param contains the schedule with maybe some manually input days
|
/// https://en.wikipedia.org/wiki/Backtracking
|
||||||
// returns the complete schedule after applying all rules/restrictions/fairness
|
/// https://en.wikipedia.org/wiki/Nurse_scheduling_problem
|
||||||
fn generate(schedule: &MonthlySchedule) -> MonthlySchedule {
|
///
|
||||||
todo!()
|
/// DFS where maximum depth is calculated by total_days_of_month + odd_days_of_month (open shifts)
|
||||||
}
|
/// Starts with schedule partially completed from the user interface
|
||||||
|
/// Ends with a full schedule following restrictions and fairness
|
||||||
// make part of Schedule
|
pub fn backtracking(schedule: &mut MonthlySchedule, slot: Slot, config: &Configurations) -> bool {
|
||||||
// examines Schedule for validity, if no then backtrack
|
// check if schedule was fully filled in the previous iteration
|
||||||
fn is_state_valid() {
|
if slot.out_of_range(config.total_days()) {
|
||||||
todo!()
|
log::info!("Solution found, exiting recursive algorithm");
|
||||||
}
|
return true;
|
||||||
|
|
||||||
// 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() {
|
// TODO:check if slot is already assigned with manual shifts
|
||||||
log::info!("..?");
|
|
||||||
return;
|
// check if any of the rules is violated before continuing
|
||||||
|
if schedule.restrictions_violated(&slot) {
|
||||||
|
// log::info!("Cutting branch due to restriction violation");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for resident in people {}
|
// get all candidate options for current step/slot/depth
|
||||||
|
let candidates = config.candidates(slot, schedule);
|
||||||
|
|
||||||
todo!()
|
// TODO: sort the candidates by workload left in the month helping the algorithm
|
||||||
|
|
||||||
|
for (resident, shift_type) in candidates {
|
||||||
|
schedule.insert(slot, resident, shift_type);
|
||||||
|
|
||||||
|
if backtracking(schedule, slot.increment(), config) {
|
||||||
|
log::info!("Solution found...");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule.remove(slot, resident, shift_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
generator::backtracking,
|
||||||
|
model::{Configurations, MonthlySchedule, Slot},
|
||||||
|
};
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
pub fn xxxt() {}
|
fn test_backtracking() {
|
||||||
|
const JAN: usize = 0;
|
||||||
|
let config = Configurations::new(JAN);
|
||||||
|
let mut schedule = MonthlySchedule::new();
|
||||||
|
|
||||||
|
backtracking(&mut schedule, Slot::default(), &config);
|
||||||
|
|
||||||
|
// assert!(schedule.is_filled(config.total_days()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
use log::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
export::{Export, FileType},
|
export::{Export, FileType},
|
||||||
model::{MonthlySchedule, Resident},
|
generator::backtracking,
|
||||||
|
model::{Configurations, Day, MonthlySchedule, Resident, Slot},
|
||||||
};
|
};
|
||||||
|
|
||||||
mod export;
|
mod export;
|
||||||
@@ -24,9 +27,31 @@ fn add_resident(resident: Resident) -> String {
|
|||||||
format!("{:?}", resident)
|
format!("{:?}", resident)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// argument to this must be the rota state including all
|
||||||
|
/// information for the residents, the forbidden pairs the holidays
|
||||||
|
/// and the period of the schedule
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn generate() -> String {
|
fn generate() -> String {
|
||||||
"Generating new rota".to_string()
|
const JAN: usize = 0;
|
||||||
|
const DAYS_IN_JAN: usize = 31;
|
||||||
|
let config = Configurations::new(JAN);
|
||||||
|
let mut schedule = MonthlySchedule::new();
|
||||||
|
|
||||||
|
// find total number of slots MEANING
|
||||||
|
|
||||||
|
//TODO: return a result instead of ()
|
||||||
|
backtracking(&mut schedule, Slot::default(), &config);
|
||||||
|
|
||||||
|
info!(
|
||||||
|
"Generated rota for month {}, final schedule {:?}",
|
||||||
|
JAN, schedule
|
||||||
|
);
|
||||||
|
|
||||||
|
//TODO: this should in fact return a JSON of the schedule instance solution
|
||||||
|
format!(
|
||||||
|
"Generated rota for month {}, final schedule {:?}",
|
||||||
|
JAN, schedule
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// takes the list of active residents and their configurations
|
// takes the list of active residents and their configurations
|
||||||
@@ -42,7 +67,7 @@ fn possible_swap_locations(people: Vec<Resident>) -> Vec<Resident> {
|
|||||||
fn export() -> String {
|
fn export() -> String {
|
||||||
// param must have filetype as string from svelte
|
// param must have filetype as string from svelte
|
||||||
// somehow get the current schedule
|
// somehow get the current schedule
|
||||||
let rota = MonthlySchedule::new(10);
|
let rota = MonthlySchedule::new();
|
||||||
let _ = rota.export(FileType::Json);
|
let _ = rota.export(FileType::Json);
|
||||||
|
|
||||||
// locally store the _?
|
// locally store the _?
|
||||||
|
|||||||
@@ -2,28 +2,51 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use chrono::Month;
|
use chrono::Month;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use log::warn;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
// We're always talking about entities that are created for the single month of the rota generation
|
// here add all info that we will need when we unmake the move after recursing...
|
||||||
|
pub struct CalendarState {
|
||||||
|
// here make unmake stuff
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Restriction {}
|
||||||
|
|
||||||
|
pub struct Rule {}
|
||||||
|
|
||||||
const YEAR: i32 = 2026;
|
const YEAR: i32 = 2026;
|
||||||
|
|
||||||
pub struct Configurations {
|
pub struct Configurations {
|
||||||
month: Month,
|
month: Month,
|
||||||
|
holidays: Vec<usize>,
|
||||||
|
residents: Vec<Resident>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Configurations {
|
impl Configurations {
|
||||||
pub fn new(month: usize) -> Self {
|
pub fn new(month: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
month: Month::try_from(month as u8).unwrap(),
|
month: Month::try_from(month as u8).unwrap(),
|
||||||
|
holidays: vec![],
|
||||||
|
residents: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
month: Month::try_from(0).unwrap(),
|
||||||
|
holidays: vec![],
|
||||||
|
residents: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, resident: Resident) {
|
||||||
|
self.residents.push(resident);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn total_days(&self) -> u8 {
|
pub fn total_days(&self) -> u8 {
|
||||||
self.month.num_days(YEAR).unwrap()
|
self.month.num_days(YEAR).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
// open shfits -> 2 people per night
|
|
||||||
pub fn total_open_shifts(&self) -> u8 {
|
pub fn total_open_shifts(&self) -> u8 {
|
||||||
let mut total_open_shifts = 0;
|
let mut total_open_shifts = 0;
|
||||||
for i in 1..=self.total_days() {
|
for i in 1..=self.total_days() {
|
||||||
@@ -34,31 +57,170 @@ impl Configurations {
|
|||||||
total_open_shifts
|
total_open_shifts
|
||||||
}
|
}
|
||||||
|
|
||||||
// closed shifts -> 1 resident per night
|
|
||||||
pub fn total_closed_shifts(&self) -> u8 {
|
pub fn total_closed_shifts(&self) -> u8 {
|
||||||
self.total_days() - self.total_open_shifts()
|
self.total_days() - self.total_open_shifts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return all possible candidates for the next slot
|
||||||
|
pub fn candidates(
|
||||||
|
&self,
|
||||||
|
slot: Slot,
|
||||||
|
schedule: &MonthlySchedule,
|
||||||
|
) -> Vec<(&Resident, &ShiftType)> {
|
||||||
|
let mut candidates = vec![];
|
||||||
|
let is_open = slot.is_open_shift();
|
||||||
|
|
||||||
|
for resident in &self.residents {
|
||||||
|
if resident.negative_shifts.contains(&slot.day) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if schedule.is_already_assigned_in_slot(&slot) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for shift_type in &resident.allowed_types {
|
||||||
|
match shift_type {
|
||||||
|
ShiftType::OpenFirst => {
|
||||||
|
if is_open {
|
||||||
|
candidates.push((resident, &ShiftType::OpenFirst))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ShiftType::OpenSecond => {
|
||||||
|
if is_open {
|
||||||
|
candidates.push((resident, &ShiftType::OpenSecond))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ShiftType::Closed => {
|
||||||
|
if !is_open {
|
||||||
|
candidates.push((resident, &ShiftType::Closed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MonthlySchedule {
|
#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Hash, Clone, Copy)]
|
||||||
rotation: Vec<Shift>,
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Slot {
|
||||||
|
day: Day,
|
||||||
|
index: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Slot {
|
||||||
|
pub fn new(day: Day, index: usize) -> Self {
|
||||||
|
Self { day, index }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_open_shift(&self) -> bool {
|
||||||
|
self.day.is_open_shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increment(&self) -> Self {
|
||||||
|
if self.index == 0 {
|
||||||
|
Self {
|
||||||
|
day: self.day,
|
||||||
|
index: 1,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self {
|
||||||
|
day: self.day.increment(),
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn out_of_range(&self, limit: u8) -> bool {
|
||||||
|
self.day.0 > limit
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
day: Day(0),
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct MonthlySchedule(HashMap<Slot, Shift>);
|
||||||
|
|
||||||
impl MonthlySchedule {
|
impl MonthlySchedule {
|
||||||
pub fn new(days_of_month: usize) -> Self {
|
pub fn new() -> Self {
|
||||||
Self { rotation: vec![] }
|
Self(HashMap::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restrictions_violated(&self) -> bool {
|
pub fn get_resident(&self, slot: &Slot) -> Option<String> {
|
||||||
todo!()
|
let shift = self.0.get(slot);
|
||||||
}
|
|
||||||
|
if let Some(shift) = shift {
|
||||||
pub fn is_filled(&self) -> bool {
|
if slot.index == 0 {
|
||||||
|
return shift.resident_1_id.clone();
|
||||||
|
} else {
|
||||||
|
return shift.resident_1_id.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, slot: Slot, resident: &Resident, shift_type: &ShiftType) {
|
||||||
|
match self.0.get_mut(&slot) {
|
||||||
|
Some(shift) => {
|
||||||
|
warn!("one shift already set for today");
|
||||||
|
|
||||||
|
if shift.resident_2_id.is_none() && slot.is_open_shift() {
|
||||||
|
shift.resident_2_id = Some(resident.id.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let new_shift = Shift::new(resident.id.clone());
|
||||||
|
self.0.insert(slot, new_shift);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, slot: Slot, resident: &Resident, shift_type: &ShiftType) {
|
||||||
|
if let Some(shift) = self.0.get_mut(&slot) {
|
||||||
|
if let Some(res_1_id) = &shift.resident_1_id {
|
||||||
|
if &resident.id == res_1_id {
|
||||||
|
shift.resident_1_id = None;
|
||||||
|
}
|
||||||
|
} else if let Some(res_1_id) = &shift.resident_2_id {
|
||||||
|
if &resident.id == res_1_id {
|
||||||
|
shift.resident_1_id = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_already_assigned_in_slot(&self, slot: &Slot) -> bool {
|
||||||
|
if let Some(shift) = self.0.get(slot) {
|
||||||
|
if shift.resident_1_id.is_some() || shift.resident_2_id.is_some() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// here we check for the validity of all environment restrictions
|
||||||
|
/// if any is violated we return true (leading to pruning in the backtracking algorithm)
|
||||||
|
pub fn restrictions_violated(&self, slot: &Slot) -> bool {
|
||||||
|
// self.contains_any_consecutive_shifts()
|
||||||
|
|
||||||
|
// see if the spot in the self calendar for the slot has the same person like the previous?
|
||||||
|
// this would take out both back to back days, but also same day 2 times the same resident
|
||||||
|
let resident = self.get_resident(slot);
|
||||||
|
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// return true if
|
||||||
pub fn contains_any_consecutive_shifts(&self) -> bool {
|
pub fn contains_any_consecutive_shifts(&self) -> bool {
|
||||||
// self.rotation
|
// self.0
|
||||||
// .into_iter()
|
// .into_iter()
|
||||||
// .sorted_unstable_by_key(|shift| shift.day)
|
// .sorted_unstable_by_key(|shift| shift.day)
|
||||||
// .tuple_windows()
|
// .tuple_windows()
|
||||||
@@ -83,18 +245,20 @@ impl MonthlySchedule {
|
|||||||
// });
|
// });
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug)]
|
#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq, Debug, Hash, Clone, Copy)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|
||||||
pub struct Day(usize);
|
pub struct Day(pub u8);
|
||||||
|
|
||||||
impl Day {
|
impl Day {
|
||||||
pub fn is_open_shift(&self) -> bool {
|
pub fn is_open_shift(&self) -> bool {
|
||||||
self.0.is_multiple_of(2)
|
!self.0.is_multiple_of(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increment(&self) -> Self {
|
||||||
|
Self(self.0 + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,13 +301,40 @@ impl WeeklySchedule {
|
|||||||
// todo
|
// todo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this is agnostic to what specific types the resident shave
|
||||||
|
// we cant add here shift_Type except if we just do OPEN/CLOSED.
|
||||||
|
// also there's a discussion to be had regardng the Option of resident 1 id since a shift should always have one..
|
||||||
|
// (But due to the backtracking algorithm maybe its ok to work without any residents in a shift? cauise we add/remove all the time)
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Shift {
|
pub struct Shift {
|
||||||
day: Day,
|
resident_1_id: Option<String>,
|
||||||
resident_a: Resident,
|
// should be filled in odd days, in other words in open shifts (OpenFirst, OpenSecond)
|
||||||
resident_b: Option<Resident>,
|
resident_2_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
impl Shift {
|
||||||
|
pub fn new_empty() -> Self {
|
||||||
|
Self {
|
||||||
|
resident_1_id: None,
|
||||||
|
resident_2_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn new(resident_1_id: String) -> Self {
|
||||||
|
Self {
|
||||||
|
resident_1_id: Some(resident_1_id),
|
||||||
|
resident_2_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_open(resident_1_id: String, resident_2_id: String) -> Self {
|
||||||
|
Self {
|
||||||
|
resident_1_id: Some(resident_1_id),
|
||||||
|
resident_2_id: Some(resident_2_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
pub enum ShiftType {
|
pub enum ShiftType {
|
||||||
Closed,
|
Closed,
|
||||||
OpenFirst,
|
OpenFirst,
|
||||||
|
|||||||
@@ -26,33 +26,84 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="grid h-screen w-full grid-cols-5 overflow-hidden bg-zinc-200/50 tracking-tight">
|
<main
|
||||||
<aside class="col-span-1 flex flex-col border-r bg-zinc-100">
|
class="grid h-screen w-full grid-cols-5 overflow-hidden bg-zinc-200/50 font-sans tracking-tight antialiased"
|
||||||
<div class="border-b border-zinc-200 bg-white p-4">
|
>
|
||||||
<p class="mt-2 mb-2 text-center text-xl font-bold tracking-widest text-zinc-800 uppercase">
|
<aside
|
||||||
Rota
|
class="col-span-1 flex flex-col border-r border-zinc-200 bg-zinc-50/50 font-sans antialiased"
|
||||||
</p>
|
>
|
||||||
|
<div class="flex justify-center border p-3 font-sans">
|
||||||
|
<h1 class="text-xl font-black tracking-tight uppercase">Rota Scheduler</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="flex flex-1 flex-col">
|
<nav class="relative flex-1 p-6">
|
||||||
|
<div class="absolute top-10 bottom-10 left-9.75 w-0.5 bg-zinc-200"></div>
|
||||||
|
|
||||||
|
<div class="relative flex h-full flex-col justify-between">
|
||||||
{#each steps as step}
|
{#each steps as step}
|
||||||
<button
|
<button
|
||||||
onclick={() => (rota.currentStep = step.id)}
|
onclick={() => (rota.currentStep = step.id)}
|
||||||
class="flex flex-1 flex-col justify-center border-b border-zinc-800/20 px-8 transition-all last:border-b-0
|
class="group relative z-10 flex items-center gap-4 py-4 transition-all"
|
||||||
{rota.currentStep === step.id
|
|
||||||
? 'bg-blue-900/50 text-white'
|
|
||||||
: 'text-zinc-500 hover:bg-zinc-800/50'}"
|
|
||||||
>
|
>
|
||||||
<span class="text-xs font-bold uppercase">ΒΗΜΑ {step.id}</span>
|
<div
|
||||||
<span class="font-medium">{step.title}</span>
|
class="flex size-8 items-center justify-center rounded-full border-2 transition-all duration-300
|
||||||
|
{rota.currentStep === step.id
|
||||||
|
? 'bg-slate-800 text-white'
|
||||||
|
: rota.currentStep > step.id
|
||||||
|
? 'bg-emerald-600 text-white'
|
||||||
|
: 'bg-white text-zinc-400'}"
|
||||||
|
>
|
||||||
|
{#if rota.currentStep > step.id}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="size-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<span class="text-xs font-bold">{step.id}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-start">
|
||||||
|
<span
|
||||||
|
class="text-[10px] font-bold tracking-widest uppercase
|
||||||
|
{rota.currentStep === step.id ? 'text-black-800' : 'text-zinc-400'}"
|
||||||
|
>
|
||||||
|
ΒΗΜΑ {step.id}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="text-sm font-bold tracking-tight
|
||||||
|
{rota.currentStep === step.id ? 'text-zinc-900' : 'text-zinc-500'}"
|
||||||
|
>
|
||||||
|
{step.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="border-t border-zinc-200 bg-white p-4">
|
|
||||||
<p class="text-center text-[10px] font-bold tracking-widest text-zinc-500 uppercase">
|
|
||||||
v1.0.0-Beta
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div class="border-t border-zinc-200 bg-white p-6">
|
||||||
|
<div class="rounded-xl border border-zinc-100 bg-zinc-50 p-4">
|
||||||
|
<div class="mb-2 flex items-center justify-between">
|
||||||
|
<span class="text-[10px] font-bold text-zinc-500 uppercase">ΟΛΟΚΛΗΡΩΣΗ</span>
|
||||||
|
<span class="text-[10px] font-bold text-zinc-500"
|
||||||
|
>{(((rota.currentStep - 1) / (steps.length - 1)) * 100).toFixed(0)}%</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="h-1.5 w-full overflow-hidden rounded-full bg-zinc-200">
|
||||||
|
<div
|
||||||
|
class="h-full bg-emerald-600 transition-all duration-500"
|
||||||
|
style="width: {(rota.currentStep / steps.length) * 100}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section class="col-span-4 flex flex-col overflow-y-auto p-12">
|
<section class="col-span-4 flex flex-col overflow-y-auto p-12">
|
||||||
|
|||||||
@@ -29,28 +29,7 @@
|
|||||||
<p class="text-sm text-zinc-500">{steps[rota.currentStep - 1].description}</p>
|
<p class="text-sm text-zinc-500">{steps[rota.currentStep - 1].description}</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if rota.forbiddenPairs.length === 0}
|
{#if rota.forbiddenPairs.length > 0}
|
||||||
<div class="rounded-3xl border-2 border-dashed border-zinc-200 bg-white/50 py-16 text-center">
|
|
||||||
<div class="mx-auto mb-3 flex size-12 items-center justify-center rounded-full bg-zinc-100">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="text-zinc-500"
|
|
||||||
><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path
|
|
||||||
d="M22 21v-2a4 4 0 0 0-3-3.87"
|
|
||||||
/><path d="M16 3.13a4 4 0 0 1 0 7.75" /></svg
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm font-medium text-zinc-500">Δεν έχουν οριστεί περιορισμοί.</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
{#each rota.forbiddenPairs as pair, i (pair)}
|
{#each rota.forbiddenPairs as pair, i (pair)}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -36,37 +36,68 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<header class="mb-2">
|
<header class="mb-6">
|
||||||
<p class="text-sm text-zinc-500">{steps[rota.currentStep - 1].description}</p>
|
<p class="text-sm text-zinc-500">{steps[rota.currentStep - 1].description}</p>
|
||||||
</header>
|
</header>
|
||||||
<div class="grid grid-cols-2 gap-4 rounded-2xl border border-zinc-200 bg-white p-6">
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">Month</p>
|
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">MONTH</p>
|
||||||
|
<div class="relative">
|
||||||
<select
|
<select
|
||||||
bind:value={rota.selectedMonth}
|
bind:value={rota.selectedMonth}
|
||||||
class="w-full rounded-xl border border-zinc-200 bg-zinc-50 px-2 py-2 font-semibold text-zinc-600 outline-none focus:border-blue-600 focus:ring-2 focus:ring-blue-600/10"
|
class="w-full appearance-none rounded-xl border border-zinc-200 bg-white px-4 py-3 font-semibold text-zinc-700 transition-all outline-none focus:border-zinc-400 focus:ring-4 focus:ring-zinc-100"
|
||||||
>
|
>
|
||||||
{#each monthOptions as month}
|
{#each monthOptions as month}
|
||||||
<option value={month.value}>{month.label}</option>
|
<option value={month.value}>{month.label}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
|
<div class="pointer-events-none absolute top-1/2 right-4 -translate-y-1/2 text-zinc-400">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">Year</p>
|
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">YEAR</p>
|
||||||
|
<div class="relative">
|
||||||
<select
|
<select
|
||||||
bind:value={rota.selectedYear}
|
bind:value={rota.selectedYear}
|
||||||
class="w-full rounded-xl border border-zinc-200 bg-zinc-50 px-2 py-2 font-semibold text-zinc-600 outline-none focus:border-blue-600 focus:ring-2 focus:ring-blue-600/10"
|
class="w-full appearance-none rounded-xl border border-zinc-200 bg-white px-4 py-3 font-semibold text-zinc-700 transition-all outline-none focus:border-zinc-400 focus:ring-4 focus:ring-zinc-100"
|
||||||
>
|
>
|
||||||
{#each yearOptions as year}
|
{#each yearOptions as year}
|
||||||
<option value={year}>{year}</option>
|
<option value={year}>{year}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
|
<div class="pointer-events-none absolute top-1/2 right-4 -translate-y-1/2 text-zinc-400">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-span-4 mt-8 space-y-2">
|
<div class="mt-8 space-y-2">
|
||||||
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">Holidays</p>
|
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">HOLIDAYS</p>
|
||||||
|
|
||||||
<Popover.Root>
|
<Popover.Root>
|
||||||
<Popover.Trigger>
|
<Popover.Trigger>
|
||||||
@@ -74,10 +105,11 @@
|
|||||||
<Button
|
<Button
|
||||||
{...props}
|
{...props}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="w-full justify-start rounded-xl border-zinc-200 bg-white px-4 py-5 font-normal hover:bg-zinc-50"
|
class="flex h-12 w-full items-center justify-between rounded-xl border-zinc-200 bg-white px-5 font-normal transition-all hover:bg-zinc-50"
|
||||||
>
|
>
|
||||||
<span class="mr-2 text-zinc-500"
|
<div class="flex items-center gap-3">
|
||||||
><svg
|
<span class="text-zinc-500">
|
||||||
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="18"
|
width="18"
|
||||||
height="18"
|
height="18"
|
||||||
@@ -87,7 +119,7 @@
|
|||||||
stroke-width="2.5"
|
stroke-width="2.5"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="mr-3 text-teal-800"
|
class="text-teal-800"
|
||||||
>
|
>
|
||||||
<path d="M8 2v4" /><path d="M16 2v4" /><rect
|
<path d="M8 2v4" /><path d="M16 2v4" /><rect
|
||||||
width="18"
|
width="18"
|
||||||
@@ -96,23 +128,24 @@
|
|||||||
y="4"
|
y="4"
|
||||||
rx="2"
|
rx="2"
|
||||||
/><path d="m3 10 18 18" /><path d="m21 10-18 18" />
|
/><path d="m3 10 18 18" /><path d="m21 10-18 18" />
|
||||||
</svg></span
|
</svg>
|
||||||
>
|
|
||||||
{#if rota.holidays.length > 0}
|
|
||||||
<span class="font-bold text-teal-800">
|
|
||||||
Επιλέχθηκαν {rota.holidays.length}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-start leading-tight">
|
||||||
|
{#if rota.holidays.length > 0}
|
||||||
|
<span class="text-sm font-bold text-teal-800"
|
||||||
|
>Επιλέχθηκαν {rota.holidays.length}</span
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-zinc-500">Αργίες</span>
|
<span class="text-sm font-medium text-zinc-500">Αργίες</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Popover.Trigger>
|
</Popover.Trigger>
|
||||||
|
|
||||||
<Popover.Content
|
<Popover.Content class="z-50 rounded-2xl border border-zinc-200 bg-white p-4" sideOffset={8}>
|
||||||
class="z-50 rounded-2xl border border-zinc-200 bg-white p-4"
|
|
||||||
sideOffset={8}
|
|
||||||
>
|
|
||||||
<Calendar.Root
|
<Calendar.Root
|
||||||
type="multiple"
|
type="multiple"
|
||||||
placeholder={rota.projectMonth}
|
placeholder={rota.projectMonth}
|
||||||
@@ -124,7 +157,6 @@
|
|||||||
<Calendar.Heading
|
<Calendar.Heading
|
||||||
class="items-center justify-between pb-4 text-center text-sm font-bold text-zinc-800"
|
class="items-center justify-between pb-4 text-center text-sm font-bold text-zinc-800"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 sm:flex-row">
|
<div class="flex flex-col gap-4 sm:flex-row">
|
||||||
{#each months as month}
|
{#each months as month}
|
||||||
<Calendar.Grid>
|
<Calendar.Grid>
|
||||||
@@ -148,14 +180,7 @@
|
|||||||
{...props}
|
{...props}
|
||||||
class="flex size-8 items-center justify-center rounded-lg text-sm transition-all
|
class="flex size-8 items-center justify-center rounded-lg text-sm transition-all
|
||||||
hover:bg-teal-100 hover:text-teal-800
|
hover:bg-teal-100 hover:text-teal-800
|
||||||
data-outside-month:opacity-20 data-selected:bg-teal-800 data-selected:font-bold
|
data-outside-month:opacity-20 data-selected:bg-teal-800 data-selected:font-bold data-selected:text-white"
|
||||||
data-selected:text-white
|
|
||||||
data-unavailable:pointer-events-none
|
|
||||||
data-unavailable:cursor-not-allowed
|
|
||||||
data-unavailable:bg-zinc-200
|
|
||||||
data-unavailable:text-zinc-500
|
|
||||||
data-unavailable:line-through
|
|
||||||
data-unavailable:opacity-50"
|
|
||||||
>
|
>
|
||||||
{date.day}
|
{date.day}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export class RotaState {
|
|||||||
manualShifts: [],
|
manualShifts: [],
|
||||||
maxShifts: undefined,
|
maxShifts: undefined,
|
||||||
allowedTypes: ["OpenAsFirst", "OpenAsSecond", "Closed"],
|
allowedTypes: ["OpenAsFirst", "OpenAsSecond", "Closed"],
|
||||||
hasReducedLoad: false,
|
hasReducedLoad: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,27 +64,27 @@ export const rota = new RotaState();
|
|||||||
export const steps = [
|
export const steps = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "Βασικές Ρυθμίσεις",
|
title: "Περίοδος",
|
||||||
description: "Καθόρισε την περίοδο και τις αργίες του μήνα."
|
description: "Καθόρισε την περίοδο και τις αργίες του μήνα."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
title: "Ρύθμιση Προσωπικού",
|
title: "Ειδικευόμενοι",
|
||||||
description: "Δημιούργησε νέα εγγραφή ειδικευόμενου."
|
description: "Δημιούργησε νέα εγγραφή ειδικευόμενου."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 3,
|
id: 3,
|
||||||
title: "Προχωρημένες Ρυθμίσεις",
|
title: "Προχωρημένα",
|
||||||
description: "Επίλεξε ζευγάρια ατόμων που δεν μπορούν να κάνουν μαζί εφημερία."
|
description: "Επίλεξε ζευγάρια ατόμων που δεν μπορούν να κάνουν μαζί εφημερία."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 4,
|
id: 4,
|
||||||
title: "Επισκόπηση Προγράμματος",
|
title: "Επισκόπηση",
|
||||||
description: "Έλεγξε το πρόγραμμα με τις υποχρεωτικές υπάρχουσες εφημερίες."
|
description: "Έλεγξε το πρόγραμμα με τις υποχρεωτικές υπάρχουσες εφημερίες."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 5,
|
id: 5,
|
||||||
title: "Δημιουργία Προγράμματος",
|
title: "Δημιουργία",
|
||||||
description: "Τρέξε τον αλγόριθμο ανάθεσης εφημεριών, εξήγαγε τα αποτελέσματα."
|
description: "Τρέξε τον αλγόριθμο ανάθεσης εφημεριών, εξήγαγε τα αποτελέσματα."
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user