use chrono::{DateTime, Months, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use tauri::{AppHandle, Emitter}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AutoScoreTrigger { pub event: String, pub value: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AutoScoreAction { pub event: String, pub value: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] enum IntervalUnit { Minute, Day, Month, } #[derive(Debug, Clone, Serialize, Deserialize)] struct IntervalTriggerValue { amount: i64, unit: IntervalUnit, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AutoScoreRule { pub id: i32, pub name: String, pub enabled: bool, #[serde(rename = "studentNames")] pub student_names: Vec, pub triggers: Vec, pub actions: Vec, #[serde(rename = "lastExecuted")] pub last_executed: Option, } impl Default for AutoScoreRule { fn default() -> Self { Self { id: 0, name: String::new(), enabled: true, student_names: Vec::new(), triggers: Vec::new(), actions: Vec::new(), last_executed: None, } } } pub struct AutoScoreService { rules: Vec, initialized: bool, } impl Default for AutoScoreService { fn default() -> Self { Self::new() } } impl AutoScoreService { pub fn new() -> Self { Self { rules: Vec::new(), initialized: false, } } pub async fn initialize( &mut self, _app_handle: &AppHandle, ) -> Result<(), Box> { if self.initialized { return Ok(()); } self.initialized = true; Ok(()) } pub fn load_rules(&mut self, rules_json: serde_json::Value) { if let serde_json::Value::Array(arr) = rules_json { self.rules = arr .into_iter() .filter_map(|v| serde_json::from_value(v).ok()) .collect(); } else { self.rules = Vec::new(); } } pub fn get_rules_json(&self) -> serde_json::Value { serde_json::to_value(&self.rules).unwrap_or(serde_json::Value::Array(vec![])) } pub fn get_rules(&self) -> &[AutoScoreRule] { &self.rules } pub fn get_rules_mut(&mut self) -> &mut Vec { &mut self.rules } pub fn add_rule(&mut self, mut rule: AutoScoreRule) -> i32 { let new_id = self.rules.iter().map(|r| r.id).max().unwrap_or(0) + 1; rule.id = new_id; rule.last_executed = None; self.rules.push(rule); new_id } pub fn update_rule(&mut self, rule: AutoScoreRule) -> bool { if let Some(existing) = self.rules.iter_mut().find(|r| r.id == rule.id) { *existing = rule; true } else { false } } pub fn delete_rule(&mut self, rule_id: i32) -> bool { let before_len = self.rules.len(); self.rules.retain(|r| r.id != rule_id); self.rules.len() < before_len } pub fn toggle_rule(&mut self, rule_id: i32, enabled: bool) -> bool { if let Some(rule) = self.rules.iter_mut().find(|r| r.id == rule_id) { rule.enabled = enabled; true } else { false } } pub fn sort_rules(&mut self, rule_ids: &[i32]) -> bool { let rule_map: HashMap = self.rules.drain(..).map(|r| (r.id, r)).collect(); let mut sorted_rules: Vec = Vec::new(); for id in rule_ids { if let Some(rule) = rule_map.get(id) { sorted_rules.push(rule.clone()); } } for (_, rule) in rule_map { if !rule_ids.contains(&rule.id) { sorted_rules.push(rule); } } self.rules = sorted_rules; true } pub fn is_enabled(&self) -> bool { self.rules.iter().any(|r| r.enabled) } pub fn get_rule_by_id(&self, id: i32) -> Option<&AutoScoreRule> { self.rules.iter().find(|r| r.id == id) } pub fn get_rule_by_id_mut(&mut self, id: i32) -> Option<&mut AutoScoreRule> { self.rules.iter_mut().find(|r| r.id == id) } pub fn mark_rule_executed(&mut self, rule_id: i32) { if let Some(rule) = self.rules.iter_mut().find(|r| r.id == rule_id) { rule.last_executed = Some(Utc::now().to_rfc3339()); } } pub fn check_interval_trigger(&self, rule: &AutoScoreRule) -> Option { let now = Utc::now(); for trigger in &rule.triggers { if trigger.event == "interval_time_passed" { let interval = parse_interval_trigger_value(trigger.value.as_ref()); let base_time = rule .last_executed .as_ref() .and_then(|value| DateTime::parse_from_rfc3339(value).ok()) .map(|value| value.with_timezone(&Utc)) .unwrap_or(now); let next_execute_time = add_interval_to_time(base_time, &interval)?; let delay_ms = (next_execute_time - now).num_milliseconds(); return Some(delay_ms.max(0)); } } None } pub async fn notify_rules_changed(&self, app_handle: &AppHandle) { let _ = app_handle.emit("auto-score:rulesChanged", &self.rules); } } fn parse_interval_trigger_value(value: Option<&String>) -> IntervalTriggerValue { if let Some(raw_value) = value { if let Ok(minutes) = raw_value.parse::() { if minutes > 0 { return IntervalTriggerValue { amount: minutes, unit: IntervalUnit::Minute, }; } } if let Ok(parsed_value) = serde_json::from_str::(raw_value) { if parsed_value.amount > 0 { return parsed_value; } } } IntervalTriggerValue { amount: 30, unit: IntervalUnit::Minute, } } fn add_interval_to_time( base_time: DateTime, interval: &IntervalTriggerValue, ) -> Option> { match interval.unit { IntervalUnit::Minute => { base_time.checked_add_signed(chrono::Duration::minutes(interval.amount)) } IntervalUnit::Day => base_time.checked_add_signed(chrono::Duration::days(interval.amount)), IntervalUnit::Month => { let months = u32::try_from(interval.amount).ok()?; base_time.checked_add_months(Months::new(months)) } } }