1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5#[serde(rename_all = "camelCase")]
6pub struct ShiftRequest {
7 #[serde(skip_serializing_if = "Option::is_none")]
8 pub id: Option<String>,
9 #[serde(skip_serializing_if = "Option::is_none")]
10 pub name: Option<String>,
11 #[serde(skip_serializing_if = "Option::is_none")]
12 pub description: Option<String>,
13 #[serde(default, skip_serializing_if = "Vec::is_empty")]
14 pub contracts: Vec<Contract>,
15 #[serde(default, skip_serializing_if = "Vec::is_empty")]
16 pub shifts: Vec<ShiftAssignment>,
17 #[serde(default, skip_serializing_if = "Vec::is_empty")]
18 pub employees: Vec<ShiftEmployee>,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub options: Option<Options>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub weights: Option<Weights>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub fairness: Option<Fairness>,
25 #[serde(default, skip_serializing_if = "Vec::is_empty")]
26 pub day_off_requests: Vec<DayOffRequest>,
27 #[serde(default, skip_serializing_if = "Vec::is_empty")]
28 pub shift_off_requests: Vec<ShiftOffRequest>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub hook: Option<String>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub constraint_weight_overrides: Option<ConstraintWeightOverrides>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub score: Option<serde_json::Value>,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
36 pub unassigned_shifts: Vec<ShiftAssignment>,
37 #[serde(default, skip_serializing_if = "Vec::is_empty")]
38 pub assigned_shifts: Vec<ShiftAssignment>,
39}
40
41#[derive(Debug, Clone, Default, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub struct Shift {
45 pub id: String,
46 pub min_start_time: String,
47 pub max_end_time: String,
48}
49
50#[derive(Debug, Clone, Default, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct ShiftAssignment {
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub name: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub from: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub to: Option<String>,
60 #[serde(default, skip_serializing_if = "Vec::is_empty")]
61 pub skills: Vec<String>,
62 #[serde(default)]
63 pub cost: f64,
64 #[serde(default)]
65 pub value: i64,
66 #[serde(default)]
67 pub priority: i64,
68 #[serde(default, skip_serializing_if = "Vec::is_empty")]
69 pub desired_skills: Vec<String>,
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
71 pub tags: Vec<String>,
72 #[serde(default)]
73 pub pinned_by_user: bool,
74}
75
76#[derive(Debug, Clone, Default, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct ShiftEmployee {
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub name: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub contract: Option<String>,
84 #[serde(default, skip_serializing_if = "Vec::is_empty")]
85 pub skills: Vec<String>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub last_rest_date: Option<String>,
88 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub availability: Vec<String>,
90 #[serde(default, skip_serializing_if = "Vec::is_empty")]
91 pub preference: Vec<String>,
92 #[serde(default, skip_serializing_if = "Vec::is_empty")]
93 pub period_rules: Vec<PeriodRule>,
94 #[serde(default, skip_serializing_if = "Vec::is_empty")]
95 pub unavailable_dates: Vec<String>,
96 #[serde(default, skip_serializing_if = "Vec::is_empty")]
97 pub tags: Vec<String>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub maximum_minutes_per_week: Option<i64>,
100 #[serde(default, skip_serializing_if = "Vec::is_empty")]
101 pub shifts: Vec<ShiftAssignment>,
102}
103
104#[derive(Debug, Clone, Default, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct Contract {
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub name: Option<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub max: Option<String>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub min: Option<String>,
114 #[serde(default)]
115 pub max_consecutive_work_days: i64,
116 #[serde(default)]
117 pub max_shifts_day: i64,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub min_rest_between_shifts_same_day: Option<String>,
120 #[serde(default)]
121 pub max_working_days: i64,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub latest_shift_start: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub earliest_shift_start: Option<String>,
126 #[serde(default)]
127 pub minimum_consecutive_days_off: i64,
128 #[serde(default)]
129 pub minimum_hours_off_between_shifts: i64,
130}
131
132#[derive(Debug, Clone, Default, Serialize, Deserialize)]
134#[serde(rename_all = "camelCase")]
135pub struct Weights {
136 pub required_skills: i64,
137 pub shift_capacity: i64,
138 pub minimum_staffing: i64,
139 pub no_double_booking: i64,
140 pub rest_between_shifts: i64,
141 pub employee_availability: i64,
142 pub shift_preferences: i64,
143 pub cost_minimization: i64,
144 pub workload_balance: i64,
145 pub fairness: i64,
146 pub max_consecutive_work_days: i64,
147 pub max_shifts_per_day: i64,
148 pub max_working_days_per_week: i64,
149 pub contract_rest_between_shifts: i64,
150 pub earliest_shift_start: i64,
151 pub latest_shift_start: i64,
152 pub minimum_consecutive_days_off: i64,
153 pub period_rule_violation: i64,
154 pub desired_skills: i64,
155 pub desired_day_off: i64,
156 pub shift_off_request: i64,
157 pub balance_time_worked: i64,
158 pub employee_affinity: i64,
159 pub avoid_shift_close_to_day_off: i64,
160}
161
162#[derive(Debug, Clone, Default, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct Options {
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub partial_planning: Option<bool>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub max_iterations: Option<i64>,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub time_limit: Option<i64>,
172}
173
174#[derive(Debug, Clone, Default, Serialize, Deserialize)]
176#[serde(rename_all = "camelCase")]
177pub struct Fairness {
178 #[serde(default, skip_serializing_if = "Vec::is_empty")]
179 pub fairness_buckets: Vec<FairnessBucket>,
180}
181
182#[derive(Debug, Clone, Default, Serialize, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct FairnessBucket {
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub name: Option<String>,
188 #[serde(default, skip_serializing_if = "Vec::is_empty")]
189 pub employees: Vec<String>,
190 #[serde(default, skip_serializing_if = "Vec::is_empty")]
191 pub shifts: Vec<String>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub period: Option<String>,
194}
195
196#[derive(Debug, Clone, Default, Serialize, Deserialize)]
198#[serde(rename_all = "camelCase")]
199pub struct DayOffRequest {
200 #[serde(skip_serializing_if = "Option::is_none")]
201 pub id: Option<String>,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub employee_name: Option<String>,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub date: Option<String>,
206 #[serde(default)]
207 pub weight: i64,
208}
209
210#[derive(Debug, Clone, Default, Serialize, Deserialize)]
212#[serde(rename_all = "camelCase")]
213pub struct ShiftOffRequest {
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub id: Option<String>,
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub employee_name: Option<String>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub shift_name: Option<String>,
220 #[serde(default)]
221 pub weight: i64,
222}
223
224#[derive(Debug, Clone, Default, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct PeriodRule {
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub period: Option<PlanningPeriod>,
230 #[serde(default)]
231 pub max_working_days: i64,
232 #[serde(default)]
233 pub min_working_days: i64,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub min_working_duration: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub max_working_duration: Option<String>,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub min_rest_duration_between_shifts_same_day: Option<String>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub min_rest_duration: Option<String>,
242}
243
244#[derive(Debug, Clone, Default, Serialize, Deserialize)]
246#[serde(rename_all = "camelCase")]
247pub struct PlanningPeriod {
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub from: Option<String>,
250 #[serde(skip_serializing_if = "Option::is_none")]
251 pub to: Option<String>,
252}
253
254#[derive(Debug, Clone, Default, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub struct ConstraintWeightOverrides {
258 #[serde(default, skip_serializing_if = "Vec::is_empty")]
259 pub known_constraint_names: Vec<String>,
260}
261
262#[derive(Debug, Clone, Default, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct ShiftStartResponse {
266 pub job_id: String,
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub solver_job_id: Option<String>,
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub result: Option<String>,
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub error: Option<String>,
273}
274
275#[derive(Debug, Clone, Default, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct ShiftResultResponse {
279 #[serde(default, skip_serializing_if = "Option::is_none")]
280 pub job_id: Option<String>,
281 pub employees: Vec<ScheduledEmployee>,
282 pub tasks: Vec<ScheduledTask>,
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub score: Option<String>,
285 #[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
286 pub weights: std::collections::HashMap<String, String>,
287}
288
289#[derive(Debug, Clone, Default, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub struct ScheduledEmployee {
293 pub id: String,
294 pub shifts: Vec<Shift>,
295 pub skills: Vec<String>,
296 pub tasks: Vec<String>,
297}
298
299#[derive(Debug, Clone, Default, Serialize, Deserialize)]
301#[serde(rename_all = "camelCase")]
302pub struct ScheduledTask {
303 pub id: String,
304 pub name: String,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub employee: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub start_time: Option<String>,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub end_time: Option<String>,
311 #[serde(default)]
312 pub duration: f64,
313 pub priority: String,
314 pub required_skills: Vec<String>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub deadline: Option<String>,
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn serializes_and_omits_empty_collections() {
325 let json = serde_json::to_string(&ShiftRequest::default()).unwrap();
326 assert!(!json.contains("\"shifts\""));
328 assert!(!json.contains("\"employees\""));
329 }
330
331 #[test]
332 fn serializes_populated_assignment() {
333 let request = ShiftRequest {
334 shifts: vec![ShiftAssignment {
335 name: Some("Night".into()),
336 cost: 12.5,
337 priority: 3,
338 pinned_by_user: true,
339 ..Default::default()
340 }],
341 ..Default::default()
342 };
343 let json = serde_json::to_string(&request).unwrap();
344 assert!(json.contains("\"shifts\""));
345 assert!(json.contains("\"pinnedByUser\":true"));
346 assert!(json.contains("\"cost\":12.5"));
347 }
348
349 #[test]
350 fn deserializes_result_response() {
351 let json = r#"{
352 "employees": [
353 {"id": "e1", "shifts": [], "skills": ["nurse"], "tasks": ["t1"]}
354 ],
355 "tasks": [
356 {"id": "t1", "name": "Ward A", "employee": "e1",
357 "startTime": "2024-01-15T22:00:00", "endTime": "2024-01-16T06:00:00",
358 "duration": 8.0, "priority": "HIGH", "requiredSkills": ["nurse"]}
359 ],
360 "score": "0hard/0medium/0soft"
361 }"#;
362
363 let result: ShiftResultResponse = serde_json::from_str(json).unwrap();
364 assert_eq!(result.employees.len(), 1);
365 assert_eq!(result.tasks[0].employee.as_deref(), Some("e1"));
366 assert_eq!(
367 result.tasks[0].start_time.as_deref(),
368 Some("2024-01-15T22:00:00")
369 );
370 assert_eq!(result.tasks[0].duration, 8.0);
371 }
372}