use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShiftRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub contracts: Vec<Contract>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub shifts: Vec<ShiftAssignment>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub employees: Vec<ShiftEmployee>,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Options>,
#[serde(skip_serializing_if = "Option::is_none")]
pub weights: Option<Weights>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fairness: Option<Fairness>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub day_off_requests: Vec<DayOffRequest>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub shift_off_requests: Vec<ShiftOffRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hook: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub constraint_weight_overrides: Option<ConstraintWeightOverrides>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub score: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unassigned_shifts: Vec<ShiftAssignment>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub assigned_shifts: Vec<ShiftAssignment>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Shift {
pub id: String,
pub min_start_time: String,
pub max_end_time: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShiftAssignment {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skills: Vec<String>,
#[serde(default)]
pub cost: f64,
#[serde(default)]
pub value: i64,
#[serde(default)]
pub priority: i64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub desired_skills: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default)]
pub pinned_by_user: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShiftEmployee {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub contract: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skills: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_rest_date: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub availability: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub preference: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub period_rules: Vec<PeriodRule>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unavailable_dates: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maximum_minutes_per_week: Option<i64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub shifts: Vec<ShiftAssignment>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Contract {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min: Option<String>,
#[serde(default)]
pub max_consecutive_work_days: i64,
#[serde(default)]
pub max_shifts_day: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_rest_between_shifts_same_day: Option<String>,
#[serde(default)]
pub max_working_days: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub latest_shift_start: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub earliest_shift_start: Option<String>,
#[serde(default)]
pub minimum_consecutive_days_off: i64,
#[serde(default)]
pub minimum_hours_off_between_shifts: i64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Weights {
pub required_skills: i64,
pub shift_capacity: i64,
pub minimum_staffing: i64,
pub no_double_booking: i64,
pub rest_between_shifts: i64,
pub employee_availability: i64,
pub shift_preferences: i64,
pub cost_minimization: i64,
pub workload_balance: i64,
pub fairness: i64,
pub max_consecutive_work_days: i64,
pub max_shifts_per_day: i64,
pub max_working_days_per_week: i64,
pub contract_rest_between_shifts: i64,
pub earliest_shift_start: i64,
pub latest_shift_start: i64,
pub minimum_consecutive_days_off: i64,
pub period_rule_violation: i64,
pub desired_skills: i64,
pub desired_day_off: i64,
pub shift_off_request: i64,
pub balance_time_worked: i64,
pub employee_affinity: i64,
pub avoid_shift_close_to_day_off: i64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Options {
#[serde(skip_serializing_if = "Option::is_none")]
pub partial_planning: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_iterations: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_limit: Option<i64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Fairness {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fairness_buckets: Vec<FairnessBucket>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FairnessBucket {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub employees: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub shifts: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub period: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DayOffRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub employee_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date: Option<String>,
#[serde(default)]
pub weight: i64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShiftOffRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub employee_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shift_name: Option<String>,
#[serde(default)]
pub weight: i64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PeriodRule {
#[serde(skip_serializing_if = "Option::is_none")]
pub period: Option<PlanningPeriod>,
#[serde(default)]
pub max_working_days: i64,
#[serde(default)]
pub min_working_days: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_working_duration: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_working_duration: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_rest_duration_between_shifts_same_day: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_rest_duration: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanningPeriod {
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConstraintWeightOverrides {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub known_constraint_names: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShiftStartResponse {
pub job_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub solver_job_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ShiftResultResponse {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub job_id: Option<String>,
pub employees: Vec<ScheduledEmployee>,
pub tasks: Vec<ScheduledTask>,
#[serde(skip_serializing_if = "Option::is_none")]
pub score: Option<String>,
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
pub weights: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScheduledEmployee {
pub id: String,
pub shifts: Vec<Shift>,
pub skills: Vec<String>,
pub tasks: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScheduledTask {
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub employee: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_time: Option<String>,
#[serde(default)]
pub duration: f64,
pub priority: String,
pub required_skills: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deadline: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serializes_and_omits_empty_collections() {
let json = serde_json::to_string(&ShiftRequest::default()).unwrap();
assert!(!json.contains("\"shifts\""));
assert!(!json.contains("\"employees\""));
}
#[test]
fn serializes_populated_assignment() {
let request = ShiftRequest {
shifts: vec![ShiftAssignment {
name: Some("Night".into()),
cost: 12.5,
priority: 3,
pinned_by_user: true,
..Default::default()
}],
..Default::default()
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"shifts\""));
assert!(json.contains("\"pinnedByUser\":true"));
assert!(json.contains("\"cost\":12.5"));
}
#[test]
fn deserializes_result_response() {
let json = r#"{
"employees": [
{"id": "e1", "shifts": [], "skills": ["nurse"], "tasks": ["t1"]}
],
"tasks": [
{"id": "t1", "name": "Ward A", "employee": "e1",
"startTime": "2024-01-15T22:00:00", "endTime": "2024-01-16T06:00:00",
"duration": 8.0, "priority": "HIGH", "requiredSkills": ["nurse"]}
],
"score": "0hard/0medium/0soft"
}"#;
let result: ShiftResultResponse = serde_json::from_str(json).unwrap();
assert_eq!(result.employees.len(), 1);
assert_eq!(result.tasks[0].employee.as_deref(), Some("e1"));
assert_eq!(
result.tasks[0].start_time.as_deref(),
Some("2024-01-15T22:00:00")
);
assert_eq!(result.tasks[0].duration, 8.0);
}
}