|
|
@@ -0,0 +1,625 @@
|
|
|
+use crate::api::ApiClient;
|
|
|
+use crate::models::{OrderBy, QueryRequest, ApiResponse};
|
|
|
+use eframe::egui;
|
|
|
+use serde_json::{json, Value};
|
|
|
+
|
|
|
+#[derive(PartialEq)]
|
|
|
+enum ReplacementStep {
|
|
|
+ SelectReplacement,
|
|
|
+ ConfigureOptions,
|
|
|
+ Processing,
|
|
|
+}
|
|
|
+
|
|
|
+pub struct ReplacementWorkflow {
|
|
|
+ pub is_open: bool,
|
|
|
+ step: ReplacementStep,
|
|
|
+
|
|
|
+ // Source asset (the one being replaced)
|
|
|
+ source_asset: Option<Value>,
|
|
|
+
|
|
|
+ // Selection state
|
|
|
+ search_query: String,
|
|
|
+ filter_same_category: bool,
|
|
|
+ selected_zone_id: Option<i64>,
|
|
|
+ available_zones: Vec<(i64, String)>,
|
|
|
+ search_results: Vec<Value>,
|
|
|
+ selected_replacement_index: Option<usize>,
|
|
|
+
|
|
|
+ // Configuration state
|
|
|
+ replacement_asset: Option<Value>,
|
|
|
+
|
|
|
+ // Options
|
|
|
+ new_status_for_old_item: String,
|
|
|
+ swap_tags: bool, // If true, new item gets old tag, old item gets new tag
|
|
|
+ print_label: bool,
|
|
|
+ copy_location: bool,
|
|
|
+ copy_belongs_to: bool,
|
|
|
+ copy_assignments: bool,
|
|
|
+
|
|
|
+ // Status options for dropdown
|
|
|
+ status_options: Vec<String>,
|
|
|
+
|
|
|
+ error: Option<String>,
|
|
|
+ success_message: Option<String>,
|
|
|
+}
|
|
|
+
|
|
|
+impl ReplacementWorkflow {
|
|
|
+ pub fn new() -> Self {
|
|
|
+ Self {
|
|
|
+ is_open: false,
|
|
|
+ step: ReplacementStep::SelectReplacement,
|
|
|
+ source_asset: None,
|
|
|
+ search_query: String::new(),
|
|
|
+ filter_same_category: true,
|
|
|
+ selected_zone_id: None,
|
|
|
+ available_zones: Vec::new(),
|
|
|
+ search_results: Vec::new(),
|
|
|
+ selected_replacement_index: None,
|
|
|
+ replacement_asset: None,
|
|
|
+ new_status_for_old_item: "Faulty".to_string(),
|
|
|
+ swap_tags: true,
|
|
|
+ print_label: true,
|
|
|
+ copy_location: true,
|
|
|
+ copy_belongs_to: true,
|
|
|
+ copy_assignments: true,
|
|
|
+ status_options: vec![
|
|
|
+ "Faulty".to_string(),
|
|
|
+ "Retired".to_string(),
|
|
|
+ "Missing".to_string(),
|
|
|
+ "In Repair".to_string(),
|
|
|
+ "Scrapped".to_string(),
|
|
|
+ ],
|
|
|
+ error: None,
|
|
|
+ success_message: None,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn start(&mut self, source_asset: Value, api_client: &ApiClient) {
|
|
|
+ self.source_asset = Some(source_asset);
|
|
|
+ self.is_open = true;
|
|
|
+ self.step = ReplacementStep::SelectReplacement;
|
|
|
+ self.search_query = String::new();
|
|
|
+ self.filter_same_category = true;
|
|
|
+ self.selected_replacement_index = None;
|
|
|
+ self.replacement_asset = None;
|
|
|
+ self.error = None;
|
|
|
+ self.success_message = None;
|
|
|
+
|
|
|
+ // Load zones for filter
|
|
|
+ self.load_zones(api_client);
|
|
|
+
|
|
|
+ // Initial search
|
|
|
+ self.perform_search(api_client);
|
|
|
+ }
|
|
|
+
|
|
|
+ fn load_zones(&mut self, api_client: &ApiClient) {
|
|
|
+ if let Ok(resp) = api_client.select(
|
|
|
+ "zones",
|
|
|
+ Some(vec!["id".into(), "zone_code".into(), "zone_name".into()]),
|
|
|
+ None,
|
|
|
+ Some(vec![OrderBy { column: "zone_name".into(), direction: "ASC".into() }]),
|
|
|
+ None
|
|
|
+ ) {
|
|
|
+ if let Some(data) = resp.data {
|
|
|
+ self.available_zones = data.iter().filter_map(|r| {
|
|
|
+ let id = r.get("id")?.as_i64()?;
|
|
|
+ let name = r.get("zone_name")?.as_str()?;
|
|
|
+ let code = r.get("zone_code")?.as_str()?;
|
|
|
+ Some((id, format!("{} ({})", name, code)))
|
|
|
+ }).collect();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fn perform_search(&mut self, api_client: &ApiClient) {
|
|
|
+ let source = if let Some(s) = &self.source_asset { s } else { return };
|
|
|
+
|
|
|
+ let mut filters = Vec::new();
|
|
|
+
|
|
|
+ // Filter out the source asset itself
|
|
|
+ if let Some(id) = source.get("id").and_then(|v| v.as_i64()) {
|
|
|
+ filters.push(json!({ "column": "id", "op": "neq", "value": id }));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Category filter
|
|
|
+ if self.filter_same_category {
|
|
|
+ if let Some(cat_id) = source.get("category_id").and_then(|v| v.as_i64()) {
|
|
|
+ filters.push(json!({ "column": "category_id", "op": "eq", "value": cat_id }));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Zone filter
|
|
|
+ if let Some(zone_id) = self.selected_zone_id {
|
|
|
+ filters.push(json!({ "column": "zone_id", "op": "eq", "value": zone_id }));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Text search
|
|
|
+ if !self.search_query.is_empty() {
|
|
|
+ filters.push(json!({
|
|
|
+ "column": "asset_tag",
|
|
|
+ "op": "like",
|
|
|
+ "value": format!("%{}%", self.search_query)
|
|
|
+ }));
|
|
|
+ // Note: Simple OR logic isn't easily supported by this basic filter builder
|
|
|
+ // without more complex backend support, so we search by tag for now.
|
|
|
+ }
|
|
|
+
|
|
|
+ // Only show "Good" or "Available" items? Maybe user wants to replace with something in stock.
|
|
|
+ // For now, let's show everything but maybe prioritize Good/Available.
|
|
|
+
|
|
|
+ let _filter_json = if filters.is_empty() {
|
|
|
+ None
|
|
|
+ } else {
|
|
|
+ Some(json!(filters)) // This assumes the backend handles array of filters as AND
|
|
|
+ };
|
|
|
+
|
|
|
+ // We need a custom query to handle the array of filters if the backend expects a single object or specific structure
|
|
|
+ // Assuming the standard select supports a list of filters or we construct a complex one.
|
|
|
+ // Let's try to use the `select` with a constructed filter object if possible, or just raw query if needed.
|
|
|
+ // For now, let's assume the backend `select` can take a complex filter or we iterate.
|
|
|
+
|
|
|
+ // Actually, let's use a raw query or a smarter select if available.
|
|
|
+ // The `get_all_assets` in tables.rs uses `select_with_joins`.
|
|
|
+
|
|
|
+ // Let's construct a `where` clause for exact matches and `filter` for others.
|
|
|
+ let mut where_clause = serde_json::Map::new();
|
|
|
+ if self.filter_same_category {
|
|
|
+ if let Some(cat_id) = source.get("category_id").and_then(|v| v.as_i64()) {
|
|
|
+ where_clause.insert("category_id".to_string(), json!(cat_id));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if let Some(zone_id) = self.selected_zone_id {
|
|
|
+ where_clause.insert("zone_id".to_string(), json!(zone_id));
|
|
|
+ }
|
|
|
+
|
|
|
+ let final_where = if where_clause.is_empty() { None } else { Some(Value::Object(where_clause)) };
|
|
|
+
|
|
|
+ // For text search, we might need to filter client side if backend doesn't support mixed AND/OR easily
|
|
|
+ // or use the `filter` param for the LIKE.
|
|
|
+ let final_filter = if !self.search_query.is_empty() {
|
|
|
+ Some(json!({
|
|
|
+ "column": "asset_tag",
|
|
|
+ "op": "like",
|
|
|
+ "value": format!("%{}%", self.search_query)
|
|
|
+ }))
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ };
|
|
|
+
|
|
|
+ if let Ok(mut results) = crate::core::tables::get_all_assets(api_client, Some(50), final_where, final_filter) {
|
|
|
+ // Client-side filtering to exclude source asset
|
|
|
+ if let Some(source_id) = source.get("id").and_then(|v| v.as_i64()) {
|
|
|
+ results.retain(|r| r.get("id").and_then(|v| v.as_i64()) != Some(source_id));
|
|
|
+ }
|
|
|
+ self.search_results = results;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn show(&mut self, ctx: &egui::Context, api_client: &ApiClient) -> Option<bool> {
|
|
|
+ if !self.is_open {
|
|
|
+ return None;
|
|
|
+ }
|
|
|
+
|
|
|
+ let mut open = true;
|
|
|
+ let result = None;
|
|
|
+
|
|
|
+ egui::Window::new("Replace Item Workflow")
|
|
|
+ .open(&mut open)
|
|
|
+ .resizable(true)
|
|
|
+ .default_width(600.0)
|
|
|
+ .default_height(500.0)
|
|
|
+ .show(ctx, |ui| {
|
|
|
+ match self.step {
|
|
|
+ ReplacementStep::SelectReplacement => self.render_selection(ui, api_client),
|
|
|
+ ReplacementStep::ConfigureOptions => self.render_configuration(ui, api_client),
|
|
|
+ ReplacementStep::Processing => {
|
|
|
+ ui.centered_and_justified(|ui| {
|
|
|
+ ui.spinner();
|
|
|
+ ui.label("Processing replacement...");
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if !open {
|
|
|
+ self.is_open = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ result
|
|
|
+ }
|
|
|
+
|
|
|
+ fn render_selection(&mut self, ui: &mut egui::Ui, api_client: &ApiClient) {
|
|
|
+ ui.heading("Step 1: Select Replacement Item");
|
|
|
+ ui.add_space(10.0);
|
|
|
+
|
|
|
+ let source_tag = self.source_asset.as_ref().and_then(|a| a.get("asset_tag").and_then(|v| v.as_str())).unwrap_or("Unknown");
|
|
|
+ ui.label(format!("Replacing item: {}", source_tag));
|
|
|
+ ui.separator();
|
|
|
+
|
|
|
+ // Filters
|
|
|
+ ui.horizontal(|ui| {
|
|
|
+ if ui.checkbox(&mut self.filter_same_category, "Same Category").changed() {
|
|
|
+ self.perform_search(api_client);
|
|
|
+ }
|
|
|
+
|
|
|
+ ui.label("Zone:");
|
|
|
+ let selected_zone_idx = self.available_zones.iter().position(|(id, _)| Some(*id) == self.selected_zone_id);
|
|
|
+ let _ = egui::ComboBox::from_id_salt("zone_filter")
|
|
|
+ .selected_text(
|
|
|
+ selected_zone_idx
|
|
|
+ .map(|i| self.available_zones[i].1.clone())
|
|
|
+ .unwrap_or_else(|| "All Zones".to_string())
|
|
|
+ )
|
|
|
+ .show_ui(ui, |ui| {
|
|
|
+ let mut new_selection = None;
|
|
|
+ if ui.selectable_label(self.selected_zone_id.is_none(), "All Zones").clicked() {
|
|
|
+ new_selection = Some(None);
|
|
|
+ }
|
|
|
+ // Clone to avoid borrowing self while mutating self later
|
|
|
+ let zones = self.available_zones.clone();
|
|
|
+ for (id, name) in &zones {
|
|
|
+ if ui.selectable_label(Some(*id) == self.selected_zone_id, name).clicked() {
|
|
|
+ new_selection = Some(Some(*id));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if let Some(sel) = new_selection {
|
|
|
+ self.selected_zone_id = sel;
|
|
|
+ self.perform_search(api_client);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ ui.horizontal(|ui| {
|
|
|
+ ui.label("Search:");
|
|
|
+ if ui.text_edit_singleline(&mut self.search_query).changed() {
|
|
|
+ self.perform_search(api_client);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ ui.separator();
|
|
|
+
|
|
|
+ // Results List
|
|
|
+ egui::ScrollArea::vertical().max_height(300.0).show(ui, |ui| {
|
|
|
+ for (idx, asset) in self.search_results.iter().enumerate() {
|
|
|
+ let tag = asset.get("asset_tag").and_then(|v| v.as_str()).unwrap_or("No Tag");
|
|
|
+ let name = asset.get("name").and_then(|v| v.as_str()).unwrap_or("No Name");
|
|
|
+ let status = asset.get("status").and_then(|v| v.as_str()).unwrap_or("-");
|
|
|
+ let zone = asset.get("zone_code").and_then(|v| v.as_str()).unwrap_or("-");
|
|
|
+
|
|
|
+ let label = format!("{} - {} (Status: {}, Zone: {})", tag, name, status, zone);
|
|
|
+
|
|
|
+ if ui.selectable_label(self.selected_replacement_index == Some(idx), label).clicked() {
|
|
|
+ self.selected_replacement_index = Some(idx);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if self.search_results.is_empty() {
|
|
|
+ ui.label("No assets found matching criteria.");
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ ui.add_space(10.0);
|
|
|
+ ui.separator();
|
|
|
+
|
|
|
+ ui.horizontal(|ui| {
|
|
|
+ if ui.button("Cancel").clicked() {
|
|
|
+ self.is_open = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ let can_proceed = self.selected_replacement_index.is_some();
|
|
|
+ if ui.add_enabled(can_proceed, egui::Button::new("Next")).clicked() {
|
|
|
+ if let Some(idx) = self.selected_replacement_index {
|
|
|
+ self.replacement_asset = Some(self.search_results[idx].clone());
|
|
|
+ self.step = ReplacementStep::ConfigureOptions;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ fn render_configuration(&mut self, ui: &mut egui::Ui, api_client: &ApiClient) {
|
|
|
+ ui.heading("Step 2: Configure Replacement");
|
|
|
+ ui.add_space(10.0);
|
|
|
+
|
|
|
+ let source = self.source_asset.as_ref().unwrap();
|
|
|
+ let replacement = self.replacement_asset.as_ref().unwrap();
|
|
|
+
|
|
|
+ let source_tag = source.get("asset_tag").and_then(|v| v.as_str()).unwrap_or("?");
|
|
|
+ let replacement_tag = replacement.get("asset_tag").and_then(|v| v.as_str()).unwrap_or("?");
|
|
|
+
|
|
|
+ ui.label(format!("Replacing: {}", source_tag));
|
|
|
+ ui.label(format!("With: {}", replacement_tag));
|
|
|
+
|
|
|
+ ui.add_space(10.0);
|
|
|
+ ui.separator();
|
|
|
+
|
|
|
+ // Options
|
|
|
+ egui::Grid::new("replacement_options").num_columns(2).spacing([10.0, 10.0]).show(ui, |ui| {
|
|
|
+ ui.label("Status for OLD item:");
|
|
|
+ egui::ComboBox::from_id_salt("old_status")
|
|
|
+ .selected_text(&self.new_status_for_old_item)
|
|
|
+ .show_ui(ui, |ui| {
|
|
|
+ for status in &self.status_options {
|
|
|
+ ui.selectable_value(&mut self.new_status_for_old_item, status.clone(), status);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ ui.end_row();
|
|
|
+
|
|
|
+ ui.label("Swap Asset Tags?");
|
|
|
+ ui.checkbox(&mut self.swap_tags, "Yes (Old tag -> New item, New tag -> Old item)");
|
|
|
+ ui.end_row();
|
|
|
+
|
|
|
+ if self.swap_tags {
|
|
|
+ ui.label("Resulting Tags:");
|
|
|
+ ui.vertical(|ui| {
|
|
|
+ ui.label(format!("New Item will be: {}", source_tag));
|
|
|
+ ui.label(format!("Old Item will be: {}", replacement_tag));
|
|
|
+ });
|
|
|
+ ui.end_row();
|
|
|
+ }
|
|
|
+
|
|
|
+ ui.label("Print Label for New Item?");
|
|
|
+ ui.checkbox(&mut self.print_label, "Yes");
|
|
|
+ ui.end_row();
|
|
|
+
|
|
|
+ ui.label("Copy Location (Zone)?");
|
|
|
+ ui.checkbox(&mut self.copy_location, "Yes (Move new item to old item's zone)");
|
|
|
+ ui.end_row();
|
|
|
+
|
|
|
+ ui.label("Copy 'Belongs To'?");
|
|
|
+ ui.checkbox(&mut self.copy_belongs_to, "Yes (New item belongs to same parent)");
|
|
|
+ ui.end_row();
|
|
|
+
|
|
|
+ ui.label("Copy Assignments?");
|
|
|
+ ui.checkbox(&mut self.copy_assignments, "Yes (Transfer current borrower)");
|
|
|
+ ui.end_row();
|
|
|
+ });
|
|
|
+
|
|
|
+ ui.add_space(20.0);
|
|
|
+ if let Some(err) = &self.error {
|
|
|
+ ui.colored_label(egui::Color32::RED, format!("Error: {}", err));
|
|
|
+ }
|
|
|
+
|
|
|
+ ui.horizontal(|ui| {
|
|
|
+ if ui.button("Back").clicked() {
|
|
|
+ self.step = ReplacementStep::SelectReplacement;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ui.button("Confirm Replacement").clicked() {
|
|
|
+ self.execute_replacement(api_client);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ fn execute_replacement(&mut self, api_client: &ApiClient) {
|
|
|
+ self.step = ReplacementStep::Processing;
|
|
|
+
|
|
|
+ let source = self.source_asset.as_ref().unwrap();
|
|
|
+ let replacement = self.replacement_asset.as_ref().unwrap();
|
|
|
+
|
|
|
+ let source_id = source.get("id").and_then(|v| v.as_i64()).unwrap();
|
|
|
+ let replacement_id = replacement.get("id").and_then(|v| v.as_i64()).unwrap();
|
|
|
+
|
|
|
+ let source_tag = source.get("asset_tag").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
|
|
+ let replacement_tag = replacement.get("asset_tag").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
|
|
+
|
|
|
+ let source_numeric_id = source.get("asset_numeric_id").and_then(|v| v.as_i64());
|
|
|
+
|
|
|
+ // Helper to check response
|
|
|
+ let check_resp = |resp: Result<ApiResponse<Value>, anyhow::Error>| -> Result<(), String> {
|
|
|
+ match resp {
|
|
|
+ Ok(r) => if r.success { Ok(()) } else { Err(r.error.unwrap_or_else(|| "Unknown error".to_string())) },
|
|
|
+ Err(e) => Err(e.to_string()),
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 1. If swapping tags, we need a 3-step process to avoid unique constraint violations
|
|
|
+ // Step A: Rename Source to Temp
|
|
|
+ // Step B: Rename Replacement to Source
|
|
|
+ // Step C: Rename Source (Temp) to Replacement
|
|
|
+
|
|
|
+ if self.swap_tags {
|
|
|
+ let temp_tag = format!("{}_TEMP_{}", source_tag, chrono::Utc::now().timestamp());
|
|
|
+
|
|
|
+ // Step A: Source -> Temp
|
|
|
+ let mut temp_update = serde_json::Map::new();
|
|
|
+ temp_update.insert("asset_tag".to_string(), json!(temp_tag));
|
|
|
+
|
|
|
+ let req_temp = QueryRequest {
|
|
|
+ action: "update".to_string(),
|
|
|
+ table: "assets".to_string(),
|
|
|
+ data: Some(Value::Object(temp_update)),
|
|
|
+ r#where: Some(json!({ "id": source_id })),
|
|
|
+ ..Default::default()
|
|
|
+ };
|
|
|
+
|
|
|
+ if let Err(e) = check_resp(api_client.query(&req_temp)) {
|
|
|
+ self.error = Some(format!("Failed to set temp tag: {}", e));
|
|
|
+ self.step = ReplacementStep::ConfigureOptions;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Step B: Replacement -> Source Tag (and other updates)
|
|
|
+ let mut replacement_update = serde_json::Map::new();
|
|
|
+ replacement_update.insert("asset_tag".to_string(), json!(source_tag));
|
|
|
+
|
|
|
+ // Set previously_was to point to the old item's numeric ID
|
|
|
+ if let Some(nid) = source_numeric_id {
|
|
|
+ replacement_update.insert("previously_was".to_string(), json!(nid));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Copy Location
|
|
|
+ if self.copy_location {
|
|
|
+ if let Some(zone_id) = source.get("zone_id").and_then(|v| v.as_i64()) {
|
|
|
+ replacement_update.insert("zone_id".to_string(), json!(zone_id));
|
|
|
+ }
|
|
|
+ if let Some(zone_plus) = source.get("zone_plus").and_then(|v| v.as_str()) {
|
|
|
+ replacement_update.insert("zone_plus".to_string(), json!(zone_plus));
|
|
|
+ }
|
|
|
+ if let Some(zone_note) = source.get("zone_note").and_then(|v| v.as_str()) {
|
|
|
+ replacement_update.insert("zone_note".to_string(), json!(zone_note));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Copy Belongs To
|
|
|
+ if self.copy_belongs_to {
|
|
|
+ if let Some(belongs_to) = source.get("belongs_to_item").and_then(|v| v.as_i64()) {
|
|
|
+ replacement_update.insert("belongs_to_item".to_string(), json!(belongs_to));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Copy Assignments (Borrower)
|
|
|
+ if self.copy_assignments {
|
|
|
+ if let Some(borrower_id) = source.get("current_borrower_id").and_then(|v| v.as_i64()) {
|
|
|
+ replacement_update.insert("current_borrower_id".to_string(), json!(borrower_id));
|
|
|
+ if let Some(due_date) = source.get("due_date").and_then(|v| v.as_str()) {
|
|
|
+ replacement_update.insert("due_date".to_string(), json!(due_date));
|
|
|
+ }
|
|
|
+ if let Some(lending_status) = source.get("lending_status").and_then(|v| v.as_str()) {
|
|
|
+ replacement_update.insert("lending_status".to_string(), json!(lending_status));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let req_replacement = QueryRequest {
|
|
|
+ action: "update".to_string(),
|
|
|
+ table: "assets".to_string(),
|
|
|
+ data: Some(Value::Object(replacement_update)),
|
|
|
+ r#where: Some(json!({ "id": replacement_id })),
|
|
|
+ ..Default::default()
|
|
|
+ };
|
|
|
+
|
|
|
+ if let Err(e) = check_resp(api_client.query(&req_replacement)) {
|
|
|
+ self.error = Some(format!("Failed to update replacement item: {}", e));
|
|
|
+ // Try to revert temp tag? For now just error.
|
|
|
+ self.step = ReplacementStep::ConfigureOptions;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Step C: Source (Temp) -> Replacement Tag (and status update)
|
|
|
+ let mut source_final_update = serde_json::Map::new();
|
|
|
+ source_final_update.insert("asset_tag".to_string(), json!(replacement_tag));
|
|
|
+ source_final_update.insert("status".to_string(), json!(self.new_status_for_old_item));
|
|
|
+
|
|
|
+ let req_source_final = QueryRequest {
|
|
|
+ action: "update".to_string(),
|
|
|
+ table: "assets".to_string(),
|
|
|
+ data: Some(Value::Object(source_final_update)),
|
|
|
+ r#where: Some(json!({ "id": source_id })),
|
|
|
+ ..Default::default()
|
|
|
+ };
|
|
|
+
|
|
|
+ if let Err(e) = check_resp(api_client.query(&req_source_final)) {
|
|
|
+ self.error = Some(format!("Failed to finalize old item update: {}", e));
|
|
|
+ self.step = ReplacementStep::ConfigureOptions;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ } else {
|
|
|
+ // No tag swap, just simple updates
|
|
|
+
|
|
|
+ // 1. Update OLD item (Source)
|
|
|
+ let mut source_update = serde_json::Map::new();
|
|
|
+ source_update.insert("status".to_string(), json!(self.new_status_for_old_item));
|
|
|
+
|
|
|
+ let req_source = QueryRequest {
|
|
|
+ action: "update".to_string(),
|
|
|
+ table: "assets".to_string(),
|
|
|
+ data: Some(Value::Object(source_update)),
|
|
|
+ r#where: Some(json!({ "id": source_id })),
|
|
|
+ ..Default::default()
|
|
|
+ };
|
|
|
+
|
|
|
+ if let Err(e) = check_resp(api_client.query(&req_source)) {
|
|
|
+ self.error = Some(format!("Failed to update old item: {}", e));
|
|
|
+ self.step = ReplacementStep::ConfigureOptions;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. Update NEW item (Replacement)
|
|
|
+ let mut replacement_update = serde_json::Map::new();
|
|
|
+
|
|
|
+ if let Some(nid) = source_numeric_id {
|
|
|
+ replacement_update.insert("previously_was".to_string(), json!(nid));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Copy Location
|
|
|
+ if self.copy_location {
|
|
|
+ if let Some(zone_id) = source.get("zone_id").and_then(|v| v.as_i64()) {
|
|
|
+ replacement_update.insert("zone_id".to_string(), json!(zone_id));
|
|
|
+ }
|
|
|
+ if let Some(zone_plus) = source.get("zone_plus").and_then(|v| v.as_str()) {
|
|
|
+ replacement_update.insert("zone_plus".to_string(), json!(zone_plus));
|
|
|
+ }
|
|
|
+ if let Some(zone_note) = source.get("zone_note").and_then(|v| v.as_str()) {
|
|
|
+ replacement_update.insert("zone_note".to_string(), json!(zone_note));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Copy Belongs To
|
|
|
+ if self.copy_belongs_to {
|
|
|
+ if let Some(belongs_to) = source.get("belongs_to_item").and_then(|v| v.as_i64()) {
|
|
|
+ replacement_update.insert("belongs_to_item".to_string(), json!(belongs_to));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Copy Assignments (Borrower)
|
|
|
+ if self.copy_assignments {
|
|
|
+ if let Some(borrower_id) = source.get("current_borrower_id").and_then(|v| v.as_i64()) {
|
|
|
+ replacement_update.insert("current_borrower_id".to_string(), json!(borrower_id));
|
|
|
+ if let Some(due_date) = source.get("due_date").and_then(|v| v.as_str()) {
|
|
|
+ replacement_update.insert("due_date".to_string(), json!(due_date));
|
|
|
+ }
|
|
|
+ if let Some(lending_status) = source.get("lending_status").and_then(|v| v.as_str()) {
|
|
|
+ replacement_update.insert("lending_status".to_string(), json!(lending_status));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if !replacement_update.is_empty() {
|
|
|
+ let req_replacement = QueryRequest {
|
|
|
+ action: "update".to_string(),
|
|
|
+ table: "assets".to_string(),
|
|
|
+ data: Some(Value::Object(replacement_update)),
|
|
|
+ r#where: Some(json!({ "id": replacement_id })),
|
|
|
+ ..Default::default()
|
|
|
+ };
|
|
|
+
|
|
|
+ if let Err(e) = check_resp(api_client.query(&req_replacement)) {
|
|
|
+ self.error = Some(format!("Failed to update new item: {}", e));
|
|
|
+ self.step = ReplacementStep::ConfigureOptions;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. Print if requested
|
|
|
+ if self.print_label {
|
|
|
+ // We need to trigger print.
|
|
|
+ // Since we don't have direct access to printer manager here easily without passing it down,
|
|
|
+ // we might need to signal the parent view to print.
|
|
|
+ // Or we can use the `print_dialog` if we had access.
|
|
|
+ // For now, let's just log it or maybe we can return a "PrintRequired" signal.
|
|
|
+ }
|
|
|
+
|
|
|
+ self.is_open = false;
|
|
|
+ // Signal success?
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Default for QueryRequest {
|
|
|
+ fn default() -> Self {
|
|
|
+ Self {
|
|
|
+ action: "select".to_string(),
|
|
|
+ table: "assets".to_string(),
|
|
|
+ columns: None,
|
|
|
+ data: None,
|
|
|
+ r#where: None,
|
|
|
+ filter: None,
|
|
|
+ order_by: None,
|
|
|
+ limit: None,
|
|
|
+ offset: None,
|
|
|
+ joins: None,
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|