commit
a47c4a0398
96 files changed
+25721
No files matched your search
+706
@@ -0,0 +1,706 @@
|
||||
use crate::error::{Result, ensure};
|
||||
use chrono::{DateTime, NaiveDate, NaiveTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Permission {
|
||||
View,
|
||||
EditSchedules,
|
||||
RequestOverrides,
|
||||
ForceOverrides,
|
||||
ManageMembers,
|
||||
ManageIntegration,
|
||||
ViewAudit,
|
||||
}
|
||||
|
||||
impl Permission {
|
||||
pub fn all() -> Vec<Self> {
|
||||
vec![
|
||||
Self::View,
|
||||
Self::EditSchedules,
|
||||
Self::RequestOverrides,
|
||||
Self::ForceOverrides,
|
||||
Self::ManageMembers,
|
||||
Self::ManageIntegration,
|
||||
Self::ViewAudit,
|
||||
]
|
||||
}
|
||||
pub fn member() -> Vec<Self> {
|
||||
vec![Self::View, Self::RequestOverrides]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct User {
|
||||
#[serde(default)]
|
||||
pub language: Option<String>,
|
||||
pub id: Uuid,
|
||||
pub username: String,
|
||||
pub display_name: String,
|
||||
pub country: String,
|
||||
/// None follows the browser timezone; this never changes scheduling rules.
|
||||
pub timezone: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Member {
|
||||
pub user: User,
|
||||
pub permissions: Vec<Permission>,
|
||||
}
|
||||
|
||||
/// Scheduling metadata, not an authentication principal. Registered users retain their ID.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Person {
|
||||
pub id: Uuid,
|
||||
pub display_name: String,
|
||||
pub email: Option<String>,
|
||||
pub country: Option<String>,
|
||||
pub user_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Holiday {
|
||||
pub country: String,
|
||||
pub date: NaiveDate,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub fn default_handover() -> NaiveTime {
|
||||
NaiveTime::from_hms_opt(11, 0, 0).unwrap()
|
||||
}
|
||||
|
||||
pub fn valid_country(country: &str) -> bool {
|
||||
// ISO 3166-1 alpha-2. No network dependency for validating scheduling state.
|
||||
const CODES: &str = "AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW";
|
||||
CODES.split_whitespace().any(|code| code == country)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkspaceDocument {
|
||||
pub working_interval: WorkingWindow,
|
||||
pub overwrite_drift: bool,
|
||||
pub name: String,
|
||||
pub timezone: String,
|
||||
pub horizon_days: u16,
|
||||
#[serde(default)]
|
||||
pub people: Vec<Person>,
|
||||
#[serde(default)]
|
||||
pub schedules: Vec<Schedule>,
|
||||
#[serde(default)]
|
||||
pub holidays: Vec<Holiday>,
|
||||
#[serde(default)]
|
||||
pub absences: Vec<Absence>,
|
||||
#[serde(default)]
|
||||
pub requests: Vec<ReplacementRequest>,
|
||||
#[serde(default)]
|
||||
pub escalations: Vec<Escalation>,
|
||||
#[serde(default)]
|
||||
pub routes: Vec<RoutingRule>,
|
||||
#[serde(default)]
|
||||
pub integration: Integration,
|
||||
#[serde(default)]
|
||||
pub generation: crate::generation::Generation,
|
||||
}
|
||||
|
||||
impl WorkspaceDocument {
|
||||
pub fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
working_interval: WorkingWindow::default(),
|
||||
overwrite_drift: false,
|
||||
timezone: "Europe/Bucharest".into(),
|
||||
horizon_days: 90,
|
||||
people: vec![],
|
||||
schedules: vec![],
|
||||
holidays: vec![],
|
||||
absences: vec![],
|
||||
requests: vec![],
|
||||
escalations: vec![],
|
||||
routes: vec![],
|
||||
integration: Integration::Mock,
|
||||
generation: Default::default(),
|
||||
}
|
||||
}
|
||||
pub fn is_absent(&self, user: Uuid, day: NaiveDate) -> bool {
|
||||
self.absences
|
||||
.iter()
|
||||
.any(|a| a.user_id == user && a.start <= day && day <= a.end)
|
||||
}
|
||||
pub fn schedule(&self, id: Uuid) -> Option<&Schedule> {
|
||||
self.schedules.iter().find(|s| s.id == id)
|
||||
}
|
||||
|
||||
pub fn person(&self, id: Uuid) -> Option<&Person> {
|
||||
self.people.iter().find(|p| p.id == id)
|
||||
}
|
||||
|
||||
pub fn person_ids(&self, members: &BTreeSet<Uuid>) -> BTreeSet<Uuid> {
|
||||
members
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(self.people.iter().map(|p| p.id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn country_holiday(&self, id: Uuid, day: NaiveDate) -> bool {
|
||||
let country = self
|
||||
.person(id)
|
||||
.and_then(|p| p.country.as_deref())
|
||||
.unwrap_or("CY");
|
||||
self.holidays
|
||||
.iter()
|
||||
.any(|h| h.date == day && h.country == country)
|
||||
}
|
||||
|
||||
pub fn validate(&self, members: &BTreeSet<Uuid>) -> Result<()> {
|
||||
ensure(
|
||||
self.people.len() <= 1000,
|
||||
"A team supports at most 1,000 people",
|
||||
)?;
|
||||
let mut person_ids = BTreeSet::new();
|
||||
let mut emails = BTreeSet::new();
|
||||
for person in &self.people {
|
||||
ensure(
|
||||
person_ids.insert(person.id),
|
||||
crate::i18n::text("err_person_ids_must_be_unique_2d45f9"),
|
||||
)?;
|
||||
validate_name(&person.display_name)?;
|
||||
ensure(
|
||||
person
|
||||
.user_id
|
||||
.is_none_or(|id| id == person.id && members.contains(&id)),
|
||||
crate::i18n::text("err_choose_a_user_who_has_access_to_this_workspace_41cf15"),
|
||||
)?;
|
||||
ensure(
|
||||
person.country.as_deref().is_none_or(valid_country),
|
||||
crate::i18n::text("err_choose_a_valid_country_code_e79c72"),
|
||||
)?;
|
||||
if let Some(email) = &person.email {
|
||||
ensure(
|
||||
email.len() <= 254 && email_address::EmailAddress::is_valid(email),
|
||||
crate::i18n::text("err_enter_a_valid_email_address_1577ea"),
|
||||
)?;
|
||||
ensure(
|
||||
email == &email.trim().to_lowercase(),
|
||||
crate::i18n::text("err_email_addresses_must_be_normalized_c8f704"),
|
||||
)?;
|
||||
ensure(
|
||||
emails.insert(email),
|
||||
crate::i18n::text("err_this_email_is_already_in_the_team_directory_2692f4"),
|
||||
)?;
|
||||
}
|
||||
ensure(
|
||||
person.user_id.is_some() || person.email.is_some(),
|
||||
"A contact needs an email address",
|
||||
)?;
|
||||
}
|
||||
let members = self.person_ids(members);
|
||||
validate_name(&self.name)?;
|
||||
ensure(
|
||||
self.timezone.parse::<chrono_tz::Tz>().is_ok(),
|
||||
crate::i18n::text("err_use_a_valid_iana_timezone_b90408"),
|
||||
)?;
|
||||
ensure(
|
||||
(7..=366).contains(&self.horizon_days),
|
||||
crate::i18n::text("err_publication_horizon_must_be_7_366_days_95023e"),
|
||||
)?;
|
||||
ensure(
|
||||
self.schedules.len() <= 200
|
||||
&& self.escalations.len() <= 200
|
||||
&& self.routes.len() <= 500,
|
||||
"A team supports at most 200 schedules, 200 policies, and 500 routing rules",
|
||||
)?;
|
||||
ensure(
|
||||
self.absences.len() <= 2000
|
||||
&& self.holidays.len() <= 2000
|
||||
&& self.requests.len() <= 5000,
|
||||
crate::i18n::text("err_team_history_is_too_large_archive_old_data_befor_fc0b97"),
|
||||
)?;
|
||||
let ids: BTreeSet<_> = self.schedules.iter().map(|s| s.id).collect();
|
||||
ensure(
|
||||
ids.len() == self.schedules.len(),
|
||||
crate::i18n::text("err_schedule_ids_must_be_unique_4f12c8"),
|
||||
)?;
|
||||
for s in &self.schedules {
|
||||
validate_name(&s.name)?;
|
||||
match &s.kind {
|
||||
ScheduleKind::Rotation {
|
||||
participants,
|
||||
anchor,
|
||||
shift_days,
|
||||
} => {
|
||||
ensure(
|
||||
!participants.is_empty() && participants.len() <= 100,
|
||||
"A rotation needs 1–100 participants",
|
||||
)?;
|
||||
ensure(
|
||||
participants.iter().all(|id| members.contains(id)),
|
||||
crate::i18n::text(
|
||||
"err_choose_a_workspace_user_or_email_contact_for_eve_879191",
|
||||
),
|
||||
)?;
|
||||
ensure(
|
||||
participants.iter().collect::<BTreeSet<_>>().len() == participants.len(),
|
||||
crate::i18n::text("err_a_rotation_cannot_repeat_a_participant_adc324"),
|
||||
)?;
|
||||
ensure(
|
||||
(1..=366).contains(shift_days),
|
||||
crate::i18n::text("err_shift_length_must_be_1_366_calendar_days_b772e3"),
|
||||
)?;
|
||||
ensure(
|
||||
(2000..=2100).contains(&chrono::Datelike::year(anchor)),
|
||||
crate::i18n::text("err_anchor_year_must_be_2000_2100_5009d7"),
|
||||
)?;
|
||||
}
|
||||
ScheduleKind::Priority {
|
||||
participants,
|
||||
fallback,
|
||||
} => {
|
||||
ensure(
|
||||
!participants.is_empty() && participants.len() <= 100,
|
||||
crate::i18n::text("err_priority_coverage_needs_1_100_people_b162ae"),
|
||||
)?;
|
||||
ensure(
|
||||
participants.iter().all(|id| members.contains(id)),
|
||||
crate::i18n::text(
|
||||
"err_choose_a_workspace_user_or_email_contact_for_eve_291413",
|
||||
),
|
||||
)?;
|
||||
ensure(
|
||||
participants.iter().collect::<BTreeSet<_>>().len() == participants.len(),
|
||||
crate::i18n::text("err_priority_coverage_cannot_repeat_a_person_c524c8"),
|
||||
)?;
|
||||
ensure(
|
||||
ids.contains(fallback),
|
||||
crate::i18n::text("err_fallback_schedule_does_not_exist_0ef8b9"),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut next = s.id;
|
||||
loop {
|
||||
ensure(
|
||||
seen.insert(next),
|
||||
crate::i18n::text("err_fallback_schedules_cannot_contain_a_cycle_b96e58"),
|
||||
)?;
|
||||
match &self
|
||||
.schedule(next)
|
||||
.ok_or_else(|| {
|
||||
crate::error::Error::Validation(
|
||||
crate::i18n::text("err_missing_fallback_schedule_6234b6").into(),
|
||||
)
|
||||
})?
|
||||
.kind
|
||||
{
|
||||
ScheduleKind::Priority { fallback, .. } => next = *fallback,
|
||||
ScheduleKind::Rotation { .. } => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
for a in &self.absences {
|
||||
ensure(
|
||||
members.contains(&a.user_id),
|
||||
crate::i18n::text("err_absent_employee_must_be_a_team_member_510f37"),
|
||||
)?;
|
||||
ensure(
|
||||
a.start <= a.end && (a.end - a.start).num_days() <= 730,
|
||||
crate::i18n::text("err_absence_must_be_a_valid_range_of_at_most_two_yea_db96bb"),
|
||||
)?;
|
||||
}
|
||||
let mut holidays = BTreeSet::new();
|
||||
for holiday in &self.holidays {
|
||||
validate_name(&holiday.name)?;
|
||||
ensure(
|
||||
holiday.country == "*" || valid_country(&holiday.country),
|
||||
crate::i18n::text("err_holiday_country_must_be_an_iso_country_code_09ebeb"),
|
||||
)?;
|
||||
ensure(
|
||||
(2000..=2100).contains(&chrono::Datelike::year(&holiday.date)),
|
||||
crate::i18n::text("err_holiday_year_must_be_2000_2100_e542b1"),
|
||||
)?;
|
||||
ensure(
|
||||
holidays.insert((&holiday.country, holiday.date)),
|
||||
"A country/date can have only one holiday",
|
||||
)?;
|
||||
}
|
||||
let policy_ids: BTreeSet<_> = self.escalations.iter().map(|e| e.id).collect();
|
||||
ensure(
|
||||
policy_ids.len() == self.escalations.len(),
|
||||
crate::i18n::text("err_escalation_ids_must_be_unique_8346e7"),
|
||||
)?;
|
||||
for e in &self.escalations {
|
||||
validate_name(&e.name)?;
|
||||
ensure(
|
||||
!e.steps.is_empty() && e.steps.len() <= 20,
|
||||
crate::i18n::text("err_an_escalation_needs_1_20_steps_644fbf"),
|
||||
)?;
|
||||
let mut previous = 0;
|
||||
for (index, step) in e.steps.iter().enumerate() {
|
||||
ensure(
|
||||
step.delay_minutes >= previous && step.delay_minutes <= 10080,
|
||||
crate::i18n::text(
|
||||
"err_escalation_delays_must_be_ordered_and_at_most_on_93513d",
|
||||
),
|
||||
)?;
|
||||
previous = step.delay_minutes;
|
||||
match step.recipient {
|
||||
Recipient::Schedule { id } => ensure(
|
||||
ids.contains(&id),
|
||||
crate::i18n::message(
|
||||
"err_escalation_p0_step_p1_references_a_missing_sched_a8026c",
|
||||
&[
|
||||
("p0", (e.name).to_string()),
|
||||
("p1", (index + 1).to_string()),
|
||||
],
|
||||
),
|
||||
)?,
|
||||
Recipient::User { id } => ensure(
|
||||
members.contains(&id),
|
||||
crate::i18n::text("err_escalation_employee_must_be_a_team_member_8b2c9f"),
|
||||
)?,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
ensure(
|
||||
e.repeat.count <= 20 && e.repeat.wait_minutes <= 10080,
|
||||
crate::i18n::text("err_invalid_escalation_repeat_settings_508f67"),
|
||||
)?;
|
||||
}
|
||||
let mut orders = BTreeSet::new();
|
||||
let mut route_ids = BTreeSet::new();
|
||||
for r in &self.routes {
|
||||
validate_name(&r.name)?;
|
||||
ensure(
|
||||
route_ids.insert(r.id) && orders.insert(r.order),
|
||||
crate::i18n::text("err_routing_ids_and_priorities_must_be_unique_1753f6"),
|
||||
)?;
|
||||
ensure(
|
||||
r.escalation_id.is_none_or(|id| policy_ids.contains(&id)),
|
||||
crate::i18n::text("err_routing_escalation_does_not_exist_0c1581"),
|
||||
)?;
|
||||
ensure(
|
||||
r.conditions.len() <= 20,
|
||||
crate::i18n::text("err_too_many_routing_conditions_7c419b"),
|
||||
)?;
|
||||
for c in &r.conditions {
|
||||
ensure(
|
||||
[
|
||||
"tags",
|
||||
"priority",
|
||||
"message",
|
||||
"description",
|
||||
"source",
|
||||
"alias",
|
||||
]
|
||||
.contains(&c.field.as_str()),
|
||||
crate::i18n::text("err_unsupported_routing_field_0b98b6"),
|
||||
)?;
|
||||
ensure(
|
||||
[
|
||||
"matches",
|
||||
"contains",
|
||||
"equals",
|
||||
"greater-than",
|
||||
"less-than",
|
||||
"starts-with",
|
||||
"ends-with",
|
||||
]
|
||||
.contains(&c.operation.as_str()),
|
||||
crate::i18n::text("err_unsupported_routing_operation_c5c07c"),
|
||||
)?;
|
||||
ensure(
|
||||
!c.value.is_empty() && c.value.len() <= 500,
|
||||
crate::i18n::text("err_routing_value_must_contain_1_500_characters_0f3a17"),
|
||||
)?;
|
||||
}
|
||||
if let Some(window) = &r.window {
|
||||
window.validate()?;
|
||||
}
|
||||
}
|
||||
if let Integration::Jsm {
|
||||
cloud_id,
|
||||
team_id,
|
||||
users,
|
||||
..
|
||||
} = &self.integration
|
||||
{
|
||||
ensure(
|
||||
Uuid::parse_str(cloud_id).is_ok() && Uuid::parse_str(team_id).is_ok(),
|
||||
crate::i18n::text("err_jsm_cloud_and_team_ids_must_be_uuids_f75b5f"),
|
||||
)?;
|
||||
ensure(
|
||||
users.keys().all(|id| members.contains(id)),
|
||||
crate::i18n::text("err_jsm_mapping_contains_a_non_member"),
|
||||
)?;
|
||||
ensure(
|
||||
users
|
||||
.values()
|
||||
.all(|v| !v.trim().is_empty() && v.len() <= 200),
|
||||
crate::i18n::text("err_jsm_account_ids_must_contain_1_200_characters_9fb430"),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_name(name: &str) -> Result<()> {
|
||||
ensure(
|
||||
!name.trim().is_empty() && name.len() <= 120,
|
||||
crate::i18n::text("err_names_must_contain_1_120_characters_8071e9"),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Schedule {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
#[serde(default = "default_handover")]
|
||||
pub handover: NaiveTime,
|
||||
#[serde(flatten)]
|
||||
pub kind: ScheduleKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case", from = "ScheduleKindWire")]
|
||||
pub enum ScheduleKind {
|
||||
/// First available person wins; the ordered list does not rotate.
|
||||
Priority {
|
||||
participants: Vec<Uuid>,
|
||||
fallback: Uuid,
|
||||
},
|
||||
Rotation {
|
||||
participants: Vec<Uuid>,
|
||||
anchor: NaiveDate,
|
||||
shift_days: u16,
|
||||
},
|
||||
}
|
||||
|
||||
// Legacy JSON is normalized on read, including immutable history snapshots.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum ScheduleKindWire {
|
||||
Priority {
|
||||
participants: Vec<Uuid>,
|
||||
fallback: Uuid,
|
||||
},
|
||||
Coverage {
|
||||
primary: Uuid,
|
||||
secondary: Uuid,
|
||||
fallback: Uuid,
|
||||
},
|
||||
Rotation {
|
||||
participants: Vec<Uuid>,
|
||||
anchor: NaiveDate,
|
||||
shift_days: u16,
|
||||
},
|
||||
}
|
||||
impl From<ScheduleKindWire> for ScheduleKind {
|
||||
fn from(value: ScheduleKindWire) -> Self {
|
||||
match value {
|
||||
ScheduleKindWire::Priority {
|
||||
participants,
|
||||
fallback,
|
||||
} => Self::Priority {
|
||||
participants,
|
||||
fallback,
|
||||
},
|
||||
ScheduleKindWire::Coverage {
|
||||
primary,
|
||||
secondary,
|
||||
fallback,
|
||||
} => Self::Priority {
|
||||
participants: vec![primary, secondary],
|
||||
fallback,
|
||||
},
|
||||
ScheduleKindWire::Rotation {
|
||||
participants,
|
||||
anchor,
|
||||
shift_days,
|
||||
} => Self::Rotation {
|
||||
participants,
|
||||
anchor,
|
||||
shift_days,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Absence {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub start: NaiveDate,
|
||||
pub end: NaiveDate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReplacementLeg {
|
||||
pub schedule_id: Uuid,
|
||||
pub start: DateTime<Utc>,
|
||||
pub end: DateTime<Utc>,
|
||||
pub from_user: Uuid,
|
||||
pub to_user: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RequestStatus {
|
||||
Pending,
|
||||
Applied,
|
||||
Rejected,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ReplacementRequest {
|
||||
pub id: Uuid,
|
||||
#[serde(default)]
|
||||
pub absence_id: Option<Uuid>,
|
||||
pub requested_by: Uuid,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub reason: String,
|
||||
pub legs: Vec<ReplacementLeg>,
|
||||
/// Remaining active spans; original legs remain available for history.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub effective_legs: Option<Vec<ReplacementLeg>>,
|
||||
pub required_approvers: BTreeSet<Uuid>,
|
||||
pub approvals: BTreeSet<Uuid>,
|
||||
pub status: RequestStatus,
|
||||
pub applied_at: Option<DateTime<Utc>>,
|
||||
pub forced_by: Option<Uuid>,
|
||||
pub force_reason: Option<String>,
|
||||
}
|
||||
|
||||
impl ReplacementRequest {
|
||||
pub fn effective_legs(&self) -> &[ReplacementLeg] {
|
||||
self.effective_legs.as_deref().unwrap_or(&self.legs)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(tag = "provider", rename_all = "snake_case")]
|
||||
pub enum Integration {
|
||||
#[default]
|
||||
Mock,
|
||||
Jsm {
|
||||
cloud_id: String,
|
||||
team_id: String,
|
||||
advanced_escalations: bool,
|
||||
users: BTreeMap<Uuid, String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Escalation {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub steps: Vec<EscalationStep>,
|
||||
#[serde(default)]
|
||||
pub repeat: EscalationRepeat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EscalationStep {
|
||||
pub delay_minutes: u16,
|
||||
pub condition: EscalationCondition,
|
||||
pub recipient: Recipient,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum EscalationCondition {
|
||||
IfNotAcked,
|
||||
IfNotClosed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Recipient {
|
||||
Schedule { id: Uuid },
|
||||
User { id: Uuid },
|
||||
TeamAll,
|
||||
TeamRandom,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct EscalationRepeat {
|
||||
pub count: u8,
|
||||
pub wait_minutes: u16,
|
||||
pub reset_recipient_states: bool,
|
||||
pub close_alert_after_all: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RoutingRule {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub order: u16,
|
||||
pub conditions: Vec<RouteCondition>,
|
||||
pub window: Option<WorkingWindow>,
|
||||
pub escalation_id: Option<Uuid>,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RouteCondition {
|
||||
pub field: String,
|
||||
pub operation: String,
|
||||
pub value: String,
|
||||
#[serde(default)]
|
||||
pub negate: bool,
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct WorkingWindow {
|
||||
pub timezone: String,
|
||||
pub weekdays: Vec<u8>,
|
||||
pub start: NaiveTime,
|
||||
pub end: NaiveTime,
|
||||
}
|
||||
|
||||
impl Default for WorkingWindow {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timezone: "Europe/Bucharest".into(),
|
||||
weekdays: vec![1, 2, 3, 4, 5],
|
||||
start: default_handover(),
|
||||
end: NaiveTime::from_hms_opt(19, 0, 0).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkingWindow {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
ensure(
|
||||
self.timezone.parse::<chrono_tz::Tz>().is_ok(),
|
||||
crate::i18n::text("err_use_a_valid_iana_timezone_for_working_hours_b4e2c6"),
|
||||
)?;
|
||||
ensure(
|
||||
!self.weekdays.is_empty()
|
||||
&& self.weekdays.iter().all(|d| (1..=7).contains(d))
|
||||
&& self.weekdays.iter().collect::<BTreeSet<_>>().len() == self.weekdays.len(),
|
||||
crate::i18n::text("err_choose_distinct_working_days_9a4ea4"),
|
||||
)?;
|
||||
ensure(
|
||||
self.start < self.end,
|
||||
crate::i18n::text("err_working_hours_must_end_after_they_start_on_the_s_a19afe"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Assignment {
|
||||
pub schedule_id: Uuid,
|
||||
pub start: DateTime<Utc>,
|
||||
pub end: DateTime<Utc>,
|
||||
pub user_id: Uuid,
|
||||
pub reason: String,
|
||||
pub unavailable: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Timeline {
|
||||
pub from: DateTime<Utc>,
|
||||
pub to: DateTime<Utc>,
|
||||
pub assignments: Vec<Assignment>,
|
||||
}
|
||||
Reference in new issue
Block a user