use crate::models::activity::Activity;
use crate::models::time_grid;
use activity_generator::{
add_budget_min_day_activities, add_budget_min_week_activities,
add_budget_top_up_week_activities, add_simple_activities, add_tasks_completed_today,
};
use activity_placer::{place, place_postponed_as_best_effort};
use chrono::{Datelike, Duration, NaiveDateTime};
use models::task::TaskCompletedToday;
use models::{calendar::Calendar, goal::Goal, task::FinalTasks};
use serde_wasm_bindgen::{from_value, to_value};
use services::activity_generator;
use services::activity_placer;
use std::collections::BTreeMap;
use technical::error::SchedulerError;
use technical::input_output::Input;
use wasm_bindgen::prelude::*;
pub mod models;
pub mod services;
pub mod technical;
#[wasm_bindgen(typescript_custom_section)]
const TS_APPEND_CONTENT: &'static str = r#"
// Kept in sync with `technical::input_output::Input`, `models::goal::Goal`, and `models::goal::Budget`.
type NaiveDateTime = string; // e.g. "2022-01-01T00:00:00"
type Weekday = "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | "Sun";
interface Slot {
start: NaiveDateTime;
end: NaiveDateTime;
}
interface PeriodWindow {
afterTime: number; // minutes since midnight [0, 1440) (legacy v1: hours [0, 24))
beforeTime: number;
}
interface BudgetPeriod {
id?: string;
onDays: Weekday[];
window: PeriodWindow;
minForPeriod: number; // minutes within this period across its days (legacy v1: hours)
maxForPeriod: number;
}
interface Budget {
id: string;
title?: string;
minPerWeek: number; // weekly total across all periods (minutes; legacy v1: hours)
maxPerWeek: number;
periods: BudgetPeriod[];
}
interface Goal {
id: string;
title: string;
start?: NaiveDateTime;
deadline?: NaiveDateTime;
minDuration?: number; // fixed uniform block size (minutes; legacy v1: hours)
budgetId?: string; // optional reference into Input.budgets[].id
notOn?: Slot[];
}
interface Input {
schemaVersion?: number; // 1 = legacy hours (default), 2 = minutes
startDate: NaiveDateTime;
endDate: NaiveDateTime;
goals: Goal[];
budgets?: Budget[];
tasksCompletedToday: { goalid: string; start: NaiveDateTime; deadline: NaiveDateTime }[];
globalNotOn?: Slot[];
}
"#;
#[wasm_bindgen]
pub fn schedule(input: &JsValue) -> Result<JsValue, JsError> {
console_error_panic_hook::set_once();
let input: Input = from_value(input.clone())?;
let final_tasks = run_scheduler(&input)?;
Ok(to_value(&final_tasks)?)
}
pub fn run_scheduler(input: &Input) -> Result<FinalTasks, SchedulerError> {
let mut goals = input.goals.clone();
let mut budgets = input.budgets.clone();
technical::normalize::normalize_goals(&mut goals, input.schema_version)?;
technical::normalize::normalize_budgets(&mut budgets, input.schema_version)?;
validate_input(
input.start_date,
input.end_date,
&goals,
&input.tasks_completed_today,
)?;
let start_date = input.start_date;
let end_date = input.end_date;
let tasks_completed_today = input.tasks_completed_today.clone();
let mut calendar = Calendar::new(start_date, end_date);
let mut activities: Vec<Activity> = vec![];
let mut goal_map: BTreeMap<String, Goal> = BTreeMap::new(); for goal in &goals {
goal_map.insert(goal.id.clone(), goal.clone());
}
calendar.add_budgets_from(&goal_map, &budgets)?;
crate::log_dbg!(&calendar); add_tasks_completed_today(
&calendar,
&goal_map,
&tasks_completed_today,
&mut activities,
);
place(&mut calendar, &mut activities);
crate::log_dbg!(&calendar); add_simple_activities(&mut calendar, &goal_map, &mut activities)?;
add_budget_min_day_activities(&mut calendar, &mut activities);
place(&mut calendar, &mut activities);
crate::log_dbg!(&calendar); add_budget_min_week_activities(&calendar, &mut activities);
place(&mut calendar, &mut activities);
crate::log_dbg!(&calendar); add_budget_top_up_week_activities(&calendar, &mut activities);
place(&mut calendar, &mut activities);
crate::log_dbg!(&calendar); place_postponed_as_best_effort(&mut calendar, &mut activities);
crate::log_dbg!(&calendar);
calendar.log_impossible_activities(&activities);
Ok(calendar.print_new(&activities))
}
fn validate_input(
start_date: NaiveDateTime,
end_date: NaiveDateTime,
goals: &[Goal],
tasks_completed_today: &[TaskCompletedToday],
) -> Result<(), SchedulerError> {
let span_minutes = (end_date - start_date).num_minutes();
if span_minutes <= 0 || span_minutes % time_grid::SLOT_MINUTES != 0 {
return Err(SchedulerError::CalendarSpanNotAligned {
span_minutes,
slot_minutes: time_grid::SLOT_MINUTES as usize,
});
}
let check_aligned = |what: &'static str, dt: NaiveDateTime| -> Result<(), SchedulerError> {
if !time_grid::is_datetime_aligned(dt) {
return Err(SchedulerError::DateTimeNotAligned {
what,
datetime: dt,
slot_minutes: time_grid::SLOT_MINUTES as usize,
});
}
Ok(())
};
check_aligned("startDate", start_date)?;
check_aligned("endDate", end_date)?;
let lower = start_date - Duration::days(1);
let upper = end_date + Duration::days(1);
let check = |what: &'static str, dt: NaiveDateTime| -> Result<(), SchedulerError> {
check_aligned(what, dt)?;
if dt < lower || dt > upper {
Err(SchedulerError::DateTimeOutOfBounds {
what,
datetime: dt,
start: start_date,
end: end_date,
})
} else {
Ok(())
}
};
for task in tasks_completed_today {
check("tasksCompletedToday.start", task.start)?;
check("tasksCompletedToday.deadline", task.deadline)?;
}
for goal in goals {
if goal.start.year() != 1970 && goal.start >= lower && goal.start <= upper {
check("goal.start", goal.start)?;
}
if let Some(deadline) = goal.deadline {
check("goal.deadline", deadline)?;
}
if let Some(not_on) = &goal.not_on {
for slot in not_on {
check("goal.notOn.start", slot.start)?;
check("goal.notOn.end", slot.end)?;
}
}
}
Ok(())
}