diff options
| author | UMTS at Teleco <crt@teleco.ch> | 2025-12-13 02:51:15 +0100 |
|---|---|---|
| committer | UMTS at Teleco <crt@teleco.ch> | 2025-12-13 02:51:15 +0100 |
| commit | 8323fdd73272a2882781aba3c499ba0be3dff2a6 (patch) | |
| tree | ffbf86473933e69cfaeef30d5c6ea7e5b494856c /src/ui/audits.rs | |
Diffstat (limited to 'src/ui/audits.rs')
| -rw-r--r-- | src/ui/audits.rs | 898 |
1 files changed, 898 insertions, 0 deletions
diff --git a/src/ui/audits.rs b/src/ui/audits.rs new file mode 100644 index 0000000..b6773f6 --- /dev/null +++ b/src/ui/audits.rs @@ -0,0 +1,898 @@ +use eframe::egui; +use serde_json::{json, Value}; + +use crate::api::ApiClient; +use crate::core::components::interactions::ConfirmDialog; +use crate::core::tables::{get_audit_tasks, get_recent_audit_logs, get_recent_audits}; +use crate::core::workflows::AuditWorkflow; +use crate::core::{ColumnConfig, TableRenderer}; + +pub struct AuditsView { + audits: Vec<Value>, + logs: Vec<Value>, + tasks: Vec<Value>, + is_loading: bool, + last_error: Option<String>, + init_loaded: bool, + workflow: AuditWorkflow, + zone_code_input: String, + start_error: Option<String>, + start_success: Option<String>, + audits_table: TableRenderer, + logs_table: TableRenderer, + tasks_table: TableRenderer, + tasks_loading: bool, + task_error: Option<String>, + task_success: Option<String>, + task_delete_dialog: ConfirmDialog, + pending_task_delete_id: Option<i64>, + pending_task_delete_name: Option<String>, + task_editor: AuditTaskEditor, +} + +impl AuditsView { + pub fn new() -> Self { + let audit_columns = Self::build_audit_columns(); + let log_columns = Self::build_log_columns(); + + Self { + audits: vec![], + logs: vec![], + tasks: vec![], + is_loading: false, + last_error: None, + init_loaded: false, + workflow: AuditWorkflow::new(), + zone_code_input: String::new(), + start_error: None, + start_success: None, + audits_table: TableRenderer::new() + .with_columns(audit_columns) + .with_default_sort("completed_at", false), + logs_table: TableRenderer::new() + .with_columns(log_columns) + .with_default_sort("audit_date", false), + tasks_table: TableRenderer::new() + .with_columns(Self::build_task_columns()) + .with_default_sort("updated_at", false) + .with_search_fields(vec!["task_name".into(), "sequence_preview".into()]), + tasks_loading: false, + task_error: None, + task_success: None, + task_delete_dialog: ConfirmDialog::new( + "Delete Audit Task", + "Are you sure you want to delete this audit task? This cannot be undone.", + ) + .confirm_text("Delete Task") + .dangerous(true), + pending_task_delete_id: None, + pending_task_delete_name: None, + task_editor: AuditTaskEditor::new(), + } + } + + fn load(&mut self, api: &ApiClient) { + if self.is_loading { + return; + } + + self.is_loading = true; + self.tasks_loading = true; + self.last_error = None; + + match get_recent_audits(api, Some(50)) { + Ok(rows) => { + self.audits = rows; + self.audits_table.selection.clear_selection(); + } + Err(err) => { + self.last_error = Some(err.to_string()); + } + } + + if self.last_error.is_none() { + match get_recent_audit_logs(api, Some(200)) { + Ok(rows) => { + self.logs = rows; + self.logs_table.selection.clear_selection(); + } + Err(err) => { + self.last_error = Some(err.to_string()); + } + } + } + + if self.last_error.is_none() { + match get_audit_tasks(api, Some(200)) { + Ok(rows) => { + self.tasks = rows; + self.tasks_table.selection.clear_selection(); + } + Err(err) => { + self.last_error = Some(err.to_string()); + } + } + } + + self.is_loading = false; + self.tasks_loading = false; + self.init_loaded = true; + } + + fn render_launch_controls( + &mut self, + ui: &mut egui::Ui, + api: &ApiClient, + current_user_id: Option<i32>, + ) { + egui::Frame::group(ui.style()) + .fill(ui.style().visuals.extreme_bg_color) + .inner_margin(egui::Margin { + left: 12, + right: 12, + top: 2, + bottom: 2, + }) + .corner_radius(8.0) + .show(ui, |ui| { + ui.set_width(ui.available_width()); + let control_height = ui.spacing().interact_size.y; + let needs_error_margin = self.start_error.is_some(); + let needs_progress_msg = self.workflow.is_active(); + + if !needs_error_margin { + let extra = if needs_progress_msg { 16.0 } else { 8.0 }; + ui.set_max_height(control_height + extra); + } + + if self.workflow.is_active() { + ui.colored_label( + egui::Color32::from_rgb(66, 133, 244), + "Audit in progress. Continue in the workflow window.", + ); + } + + ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| { + let btn_w: f32 = 140.0; + + ui.label("Zone Code:"); + + // Compute input width based on remaining space after two fixed-width buttons + let spacing = ui.spacing().item_spacing.x; + let remaining = ui.available_width(); + let reserve_for_buttons = btn_w * 2.0 + spacing * 2.0; + let input_w = (remaining - reserve_for_buttons).max(200.0); + + let text_resp = ui.add( + egui::TextEdit::singleline(&mut self.zone_code_input) + .hint_text("ZONE-ABC") + .desired_width(input_w), + ); + + let disable_new = self.workflow.is_active(); + let start_zone_clicked_button = ui + .add_enabled( + !disable_new, + egui::Button::new("Start Zone Audit").min_size(egui::vec2(btn_w, 0.0)), + ) + .clicked(); + + let start_spot_clicked = ui + .add_enabled( + !disable_new, + egui::Button::new("Start Spot Check").min_size(egui::vec2(btn_w, 0.0)), + ) + .clicked(); + + let start_zone_pressed_enter = + text_resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); + let start_zone_clicked = start_zone_clicked_button || start_zone_pressed_enter; + + if start_zone_clicked { + if let Some(user_id) = current_user_id { + let code = self.zone_code_input.trim(); + if code.is_empty() { + self.start_error = + Some("Enter a zone code to start an audit".to_string()); + self.start_success = None; + } else { + match self.workflow.start_zone_audit(api, code, user_id as i64) { + Ok(()) => { + self.start_error = None; + self.start_success = + Some(format!("Zone audit started for {}", code)); + self.zone_code_input.clear(); + } + Err(err) => { + self.start_error = Some(err.to_string()); + self.start_success = None; + } + } + } + } else { + self.start_error = + Some("You must be logged in to start an audit".to_string()); + self.start_success = None; + } + } + + if start_spot_clicked { + if let Some(user_id) = current_user_id { + self.workflow.start_spot_check(user_id as i64); + self.start_error = None; + self.start_success = Some("Spot check started".to_string()); + } else { + self.start_error = + Some("You must be logged in to start a spot check".to_string()); + self.start_success = None; + } + } + }); + + if let Some(err) = &self.start_error { + ui.add_space(4.0); + ui.colored_label(egui::Color32::from_rgb(244, 67, 54), err); + } + + if self.workflow.is_active() { + if let Some(msg) = &self.start_success { + ui.add_space(4.0); + ui.colored_label(egui::Color32::from_rgb(76, 175, 80), msg); + } + } + }); + } + + fn build_audit_columns() -> Vec<ColumnConfig> { + vec![ + ColumnConfig::new("ID", "id").with_width(60.0), + ColumnConfig::new("Type", "audit_type").with_width(90.0), + ColumnConfig::new("Zone", "zone_display").with_width(140.0), + ColumnConfig::new("Audit Name", "audit_name").with_width(160.0), + ColumnConfig::new("Started By", "started_by_name").with_width(140.0), + ColumnConfig::new("Started At", "started_at").with_width(150.0), + ColumnConfig::new("Completed At", "completed_at").with_width(150.0), + ColumnConfig::new("Status", "status").with_width(110.0), + ColumnConfig::new("Timeout (min)", "timeout_minutes").with_width(110.0), + ColumnConfig::new("Issues", "issues_summary").with_width(220.0), + ColumnConfig::new("Expected", "assets_expected").with_width(90.0), + ColumnConfig::new("Found", "assets_found").with_width(90.0), + ColumnConfig::new("Notes", "notes").with_width(200.0), + ColumnConfig::new("Cancelled Reason", "cancelled_reason").with_width(220.0), + ] + } + + fn build_log_columns() -> Vec<ColumnConfig> { + vec![ + ColumnConfig::new("ID", "id").with_width(60.0), + ColumnConfig::new("Audit ID", "physical_audit_id").with_width(80.0), + ColumnConfig::new("Asset", "asset_display").with_width(160.0), + ColumnConfig::new("Audit Date", "audit_date").with_width(140.0), + ColumnConfig::new("Audited By", "audited_by_name").with_width(140.0), + ColumnConfig::new("Status Found", "status_found").with_width(110.0), + ColumnConfig::new("Task ID", "audit_task_id").with_width(80.0), + ColumnConfig::new("Task Responses", "task_responses_text").with_width(240.0), + ColumnConfig::new("Exception", "exception_type").with_width(120.0), + ColumnConfig::new("Details", "exception_details").with_width(220.0), + ColumnConfig::new("Found Zone", "found_zone_display").with_width(160.0), + ColumnConfig::new("Action", "auditor_action").with_width(140.0), + ColumnConfig::new("Notes", "notes").with_width(200.0), + ] + } + + fn build_task_columns() -> Vec<ColumnConfig> { + vec![ + ColumnConfig::new("ID", "id").with_width(60.0), + ColumnConfig::new("Task Name", "task_name").with_width(180.0), + ColumnConfig::new("Step Count", "step_count").with_width(90.0), + ColumnConfig::new("Sequence Preview", "sequence_preview").with_width(280.0), + ColumnConfig::new("Created", "created_at").with_width(150.0), + ColumnConfig::new("Updated", "updated_at").with_width(150.0), + ] + } + pub fn show( + &mut self, + ctx: &egui::Context, + ui: &mut egui::Ui, + api_client: Option<&ApiClient>, + current_user_id: Option<i32>, + ) { + egui::ScrollArea::vertical() + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.set_width(ui.available_width()); + + ui.horizontal(|ui| { + ui.heading("Audits"); + if self.is_loading { + ui.spinner(); + ui.label("Loading..."); + } + if let Some(err) = &self.last_error { + ui.colored_label(egui::Color32::RED, err); + if ui.button("Refresh").clicked() { + if let Some(api) = api_client { + self.load(api); + } + } + } else if ui.button("Refresh").clicked() { + if let Some(api) = api_client { + self.load(api); + } + } + if let Some(msg) = &self.start_success { + if !self.workflow.is_active() { + ui.colored_label(egui::Color32::from_rgb(76, 175, 80), msg); + } + } + }); + ui.separator(); + + if let Some(api) = api_client { + self.render_launch_controls(ui, api, current_user_id); + } + + if !self.init_loaded { + if let Some(api) = api_client { + self.load(api); + } + } + + if let Some(msg) = &self.start_success { + if !self.workflow.is_active() { + ui.add_space(6.0); + ui.colored_label(egui::Color32::from_rgb(76, 175, 80), msg); + } + } + + self.render_summary(ui); + + egui::CollapsingHeader::new("Recent Audits") + .default_open(true) + .show(ui, |ui| { + self.render_audits_table(ui); + }); + + ui.add_space(10.0); + + egui::CollapsingHeader::new("Audit Logs") + .default_open(true) + .show(ui, |ui| { + self.render_logs_table(ui); + }); + + ui.add_space(10.0); + + egui::CollapsingHeader::new("Audit Task Library") + .default_open(false) + .show(ui, |ui| { + self.render_tasks_section(ui, api_client); + }); + }); + + if let Some(result) = self.task_editor.show(ctx) { + if let Some(api) = api_client { + self.save_task(api, result); + } else { + self.task_error = + Some("No API client available; cannot save audit task changes.".to_string()); + } + } + + if let Some(decision) = self.task_delete_dialog.show_dialog(ctx) { + if decision { + if let (Some(api), Some(id)) = (api_client, self.pending_task_delete_id) { + self.delete_task(api, id); + } else { + self.task_error = + Some("No API client available; cannot delete audit tasks.".to_string()); + } + } else { + self.pending_task_delete_id = None; + self.pending_task_delete_name = None; + } + } + + if let Some(api) = api_client { + if self.workflow.show(ctx, api) { + // Window stays open, nothing else to do here. + } + if let Some(completion) = self.workflow.take_recent_completion() { + self.load(api); + let banner = match completion.status.as_str() { + "cancelled" => "Audit cancelled".to_string(), + "all-good" => "Audit completed successfully".to_string(), + other => format!("Audit finished with status: {}", other), + }; + self.start_success = Some(banner); + } + } + } + + fn render_summary(&self, ui: &mut egui::Ui) { + // derive counts from loaded audits + let total = self.audits.len() as i64; + let mut in_progress = 0; + let mut attention = 0; + let mut timeout = 0; + let mut cancelled = 0; + let mut all_good = 0; + for a in &self.audits { + match a.get("status").and_then(|v| v.as_str()).unwrap_or("") { + "in-progress" => in_progress += 1, + "attention" => attention += 1, + "timeout" => timeout += 1, + "cancelled" => cancelled += 1, + "all-good" => all_good += 1, + _ => {} + } + } + ui.horizontal_wrapped(|ui| { + ui.label(egui::RichText::new(format!("Total: {}", total)).strong()); + ui.separator(); + chip( + ui, + format!("In progress: {}", in_progress), + egui::Color32::from_rgb(66, 133, 244), + ); + chip( + ui, + format!("Attention: {}", attention), + egui::Color32::from_rgb(255, 152, 0), + ); + chip( + ui, + format!("Timeout: {}", timeout), + egui::Color32::from_rgb(244, 67, 54), + ); + chip( + ui, + format!("Cancelled: {}", cancelled), + egui::Color32::from_rgb(158, 158, 158), + ); + chip( + ui, + format!("All good: {}", all_good), + egui::Color32::from_rgb(76, 175, 80), + ); + }); + ui.add_space(6.0); + + fn chip(ui: &mut egui::Ui, text: String, color: egui::Color32) { + egui::Frame::new() + .fill(color.linear_multiply(0.14)) + .corner_radius(6.0) + .inner_margin(egui::Margin { + left: 8, + right: 8, + top: 4, + bottom: 4, + }) + .show(ui, |ui| { + ui.label(egui::RichText::new(text).color(color)); + }); + } + } + + fn render_audits_table(&mut self, ui: &mut egui::Ui) { + if self.audits.is_empty() { + ui.label("No recent audits found."); + return; + } + + let prepared = self.audits_table.prepare_json_data(&self.audits); + self.audits_table.render_json_table(ui, &prepared, None); + } + + fn render_logs_table(&mut self, ui: &mut egui::Ui) { + if self.logs.is_empty() { + ui.label("No audit logs found."); + return; + } + + let prepared = self.logs_table.prepare_json_data(&self.logs); + self.logs_table.render_json_table(ui, &prepared, None); + } + + fn render_tasks_section(&mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>) { + ui.horizontal(|ui| { + ui.label("Search:"); + ui.text_edit_singleline(&mut self.tasks_table.search_query); + ui.separator(); + + let has_api = api_client.is_some(); + if ui + .add_enabled(has_api, egui::Button::new("New Task")) + .clicked() + { + self.task_error = None; + self.task_success = None; + self.task_editor.open_new(); + } + + if ui + .add_enabled(has_api, egui::Button::new("Refresh")) + .clicked() + { + if let Some(api) = api_client { + self.task_error = None; + self.task_success = None; + self.refresh_tasks(api); + } + } + }); + + if let Some(err) = &self.task_error { + ui.add_space(6.0); + ui.colored_label(egui::Color32::from_rgb(244, 67, 54), err); + } + + if let Some(msg) = &self.task_success { + ui.add_space(6.0); + ui.colored_label(egui::Color32::from_rgb(76, 175, 80), msg); + } + + ui.add_space(6.0); + + if self.tasks_loading { + ui.horizontal(|ui| { + ui.spinner(); + ui.label("Loading audit tasks..."); + }); + return; + } + + if self.tasks.is_empty() { + ui.label("No audit tasks found."); + return; + } + + let prepared = self.tasks_table.prepare_json_data(&self.tasks); + + let mut edit_task: Option<Value> = None; + let mut clone_task: Option<Value> = None; + let mut delete_task: Option<Value> = None; + + struct TaskEventHandler<'a> { + edit_action: &'a mut Option<Value>, + clone_action: &'a mut Option<Value>, + delete_action: &'a mut Option<Value>, + } + + impl<'a> crate::core::table_renderer::TableEventHandler<Value> for TaskEventHandler<'a> { + fn on_double_click(&mut self, item: &Value, _row_index: usize) { + *self.edit_action = Some(item.clone()); + } + + fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) { + if ui + .button(format!("{} Edit Task", egui_phosphor::regular::PENCIL)) + .clicked() + { + *self.edit_action = Some(item.clone()); + ui.close(); + } + + if ui + .button(format!("{} Clone Task", egui_phosphor::regular::COPY)) + .clicked() + { + *self.clone_action = Some(item.clone()); + ui.close(); + } + + ui.separator(); + + if ui + .button(format!("{} Delete Task", egui_phosphor::regular::TRASH)) + .clicked() + { + *self.delete_action = Some(item.clone()); + ui.close(); + } + } + + fn on_selection_changed(&mut self, _selected_indices: &[usize]) {} + } + + let mut handler = TaskEventHandler { + edit_action: &mut edit_task, + clone_action: &mut clone_task, + delete_action: &mut delete_task, + }; + + self.tasks_table + .render_json_table(ui, &prepared, Some(&mut handler)); + + if let Some(task) = edit_task { + self.task_error = None; + self.task_success = None; + self.task_editor.open_edit(&task); + } + + if let Some(task) = clone_task { + self.task_error = None; + self.task_success = None; + self.task_editor.open_clone(&task); + } + + if let Some(task) = delete_task { + if let Some(id) = task.get("id").and_then(|v| v.as_i64()) { + let name = task + .get("task_name") + .and_then(|v| v.as_str()) + .unwrap_or("Unnamed Task") + .to_string(); + self.pending_task_delete_id = Some(id); + self.pending_task_delete_name = Some(name.clone()); + self.task_delete_dialog.open(name, id.to_string()); + } + } + } + + fn refresh_tasks(&mut self, api: &ApiClient) { + self.tasks_loading = true; + match get_audit_tasks(api, Some(200)) { + Ok(rows) => { + self.tasks = rows; + self.tasks_table.selection.clear_selection(); + self.task_error = None; + } + Err(err) => { + self.task_error = Some(err.to_string()); + } + } + self.tasks_loading = false; + } + + fn save_task(&mut self, api: &ApiClient, result: AuditTaskEditorResult) { + self.task_error = None; + self.task_success = None; + + let AuditTaskEditorResult { + id, + name, + sequence, + is_new, + } = result; + + let mut payload = serde_json::Map::new(); + payload.insert("task_name".into(), Value::String(name.clone())); + payload.insert("json_sequence".into(), sequence); + let payload_value = Value::Object(payload); + + if is_new { + match api.insert("audit_tasks", payload_value) { + Ok(resp) if resp.success => { + self.task_success = Some(format!("Created audit task \"{}\".", name)); + self.refresh_tasks(api); + } + Ok(resp) => { + self.task_error = Some(format!("Insert failed: {:?}", resp.error)); + } + Err(err) => { + self.task_error = Some(format!("Insert error: {}", err)); + } + } + } else if let Some(task_id) = id { + let where_clause = json!({ "id": task_id }); + match api.update("audit_tasks", payload_value, where_clause) { + Ok(resp) if resp.success => { + self.task_success = Some(format!("Updated audit task \"{}\".", name)); + self.refresh_tasks(api); + } + Ok(resp) => { + self.task_error = Some(format!("Update failed: {:?}", resp.error)); + } + Err(err) => { + self.task_error = Some(format!("Update error: {}", err)); + } + } + } else { + self.task_error = Some("Missing task identifier; cannot update.".to_string()); + } + } + + fn delete_task(&mut self, api: &ApiClient, id: i64) { + let where_clause = json!({ "id": id }); + match api.delete("audit_tasks", where_clause) { + Ok(resp) if resp.success => { + let name = self + .pending_task_delete_name + .take() + .unwrap_or_else(|| format!("Task #{id}")); + self.task_success = Some(format!("Deleted audit task \"{}\".", name)); + self.refresh_tasks(api); + } + Ok(resp) => { + self.task_error = Some(format!("Delete failed: {:?}", resp.error)); + } + Err(err) => { + self.task_error = Some(format!("Delete error: {}", err)); + } + } + self.pending_task_delete_id = None; + self.pending_task_delete_name = None; + } +} + +struct AuditTaskEditor { + open: bool, + is_new: bool, + current_id: Option<i64>, + task_name: String, + sequence_text: String, + error: Option<String>, +} + +impl AuditTaskEditor { + fn new() -> Self { + Self { + open: false, + is_new: true, + current_id: None, + task_name: String::new(), + sequence_text: "[]".to_string(), + error: None, + } + } + + fn open_new(&mut self) { + self.open = true; + self.is_new = true; + self.current_id = None; + self.task_name.clear(); + self.sequence_text = "[]".to_string(); + self.error = None; + } + + fn open_edit(&mut self, task: &Value) { + self.open = true; + self.is_new = false; + self.current_id = task.get("id").and_then(|v| v.as_i64()); + self.task_name = task + .get("task_name") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + self.sequence_text = task + .get("json_sequence") + .map(|seq| serde_json::to_string_pretty(seq).unwrap_or_else(|_| seq.to_string())) + .unwrap_or_else(|| "[]".to_string()); + self.error = None; + } + + fn open_clone(&mut self, task: &Value) { + self.open_edit(task); + self.is_new = true; + self.current_id = None; + if self.task_name.is_empty() { + self.task_name = "Copied Task".to_string(); + } else { + self.task_name = format!("{} (Copy)", self.task_name); + } + } + + fn show(&mut self, ctx: &egui::Context) -> Option<AuditTaskEditorResult> { + if !self.open { + return None; + } + + let mut window_open = true; + let mut close_requested = false; + let mut outcome: Option<AuditTaskEditorResult> = None; + let title = if self.is_new { + "New Audit Task" + } else { + "Edit Audit Task" + }; + + let root_bounds = ctx.available_rect(); + let screen_bounds = ctx.input(|i| { + i.viewport().inner_rect.unwrap_or(egui::Rect::from_min_size( + egui::Pos2::ZERO, + egui::vec2(800.0, 600.0), + )) + }); + let horizontal_margin = 24.0_f32; + let vertical_margin = 24.0_f32; + + let available_max_w = (root_bounds.width() - horizontal_margin).max(420.0_f32); + let screen_max_w = (screen_bounds.width() - horizontal_margin).max(420.0_f32); + let max_w = available_max_w.min(screen_max_w); + + let available_max_h = (root_bounds.height() - vertical_margin).max(360.0_f32); + let screen_max_h = (screen_bounds.height() - vertical_margin).max(360.0_f32); + let max_h = available_max_h.min(screen_max_h); + + let default_w = 520.0_f32.clamp(360.0_f32.min(max_w), max_w); + let default_h = (root_bounds.height() * 0.6_f32) + .max(360.0_f32) + .clamp(320.0_f32.min(max_h), max_h); + let min_w = max_w.min(380.0_f32).max(320.0_f32.min(max_w)); + let min_h = max_h.min(340.0_f32).max(300.0_f32.min(max_h)); + + egui::Window::new(title) + .collapsible(false) + .resizable(true) + .movable(true) + .default_size(egui::vec2(default_w, default_h)) + .min_size(egui::vec2(min_w, min_h)) + .max_size(egui::vec2(max_w, max_h)) + .constrain_to(screen_bounds.shrink2(egui::vec2(12.0_f32, 12.0_f32))) + .open(&mut window_open) + .show(ctx, |ui| { + if let Some(err) = &self.error { + ui.colored_label(egui::Color32::from_rgb(244, 67, 54), err); + ui.add_space(8.0); + } + + let reserved_footer = 72.0_f32; + let scroll_height = (ui.available_height() - reserved_footer).max(160.0_f32); + + egui::ScrollArea::vertical() + .max_height(scroll_height) + .auto_shrink([false, false]) + .show(ui, |ui| { + ui.label("Task Name"); + ui.text_edit_singleline(&mut self.task_name); + ui.add_space(8.0); + + ui.label("JSON Sequence"); + ui.add( + egui::TextEdit::multiline(&mut self.sequence_text) + .desired_rows(14) + .desired_width(f32::INFINITY), + ); + }); + + ui.add_space(12.0); + ui.separator(); + ui.add_space(8.0); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("Save Task").clicked() { + let name = self.task_name.trim(); + if name.is_empty() { + self.error = Some("Task name cannot be empty.".to_string()); + } else { + match serde_json::from_str::<Value>(&self.sequence_text) { + Ok(sequence) => { + self.error = None; + outcome = Some(AuditTaskEditorResult { + id: self.current_id, + name: name.to_string(), + sequence, + is_new: self.is_new, + }); + close_requested = true; + } + Err(err) => { + self.error = Some(format!("Invalid JSON: {}", err)); + } + } + } + } + + ui.add_space(12.0); + + if ui.button("Cancel").clicked() { + close_requested = true; + } + }); + }); + + if !window_open || close_requested { + self.open = false; + } + + outcome + } +} + +struct AuditTaskEditorResult { + id: Option<i64>, + name: String, + sequence: Value, + is_new: bool, +} |
