// src/services/ui_service.rs use crate::services::grpc_client::GrpcClient; use crate::state::app::state::AppState; use crate::state::pages::add_logic::AddLogicState; use crate::state::pages::form::{FieldDefinition, FormState}; use crate::tui::functions::common::form::SaveOutcome; use crate::utils::columns::filter_user_columns; use anyhow::{anyhow, Context, Result}; use std::sync::Arc; pub struct UiService; impl UiService { pub async fn load_table_view( grpc_client: &mut GrpcClient, app_state: &mut AppState, profile_name: &str, table_name: &str, ) -> Result { // 1. & 2. Fetch and Cache Schema - UNCHANGED let table_structure = grpc_client .get_table_structure(profile_name.to_string(), table_name.to_string()) .await .context(format!( "Failed to get table structure for {}.{}", profile_name, table_name ))?; let cache_key = format!("{}.{}", profile_name, table_name); app_state .schema_cache .insert(cache_key, Arc::new(table_structure.clone())); tracing::info!("Schema for '{}.{}' cached.", profile_name, table_name); // --- START: FINAL, SIMPLIFIED, CORRECT LOGIC --- // 3a. Create definitions for REGULAR fields first. let mut fields: Vec = table_structure .columns .iter() .filter(|col| { !col.is_primary_key && col.name != "deleted" && col.name != "created_at" && !col.name.ends_with("_id") // Filter out ALL potential links }) .map(|col| FieldDefinition { display_name: col.name.clone(), data_key: col.name.clone(), is_link: false, link_target_table: None, }) .collect(); // 3b. Now, find and APPEND definitions for LINK fields based on the `_id` convention. let link_fields: Vec = table_structure .columns .iter() .filter(|col| col.name.ends_with("_id")) // Find all foreign key columns .map(|col| { // The table we link to is derived from the column name. // e.g., "test_diacritics_id" -> "test_diacritics" let target_table_base = col .name .strip_suffix("_id") .unwrap_or(&col.name); // Find the full table name from the profile tree for display. // e.g., "test_diacritics" -> "2025_test_diacritics" let full_target_table_name = app_state .profile_tree .profiles .iter() .find(|p| p.name == profile_name) .and_then(|p| p.tables.iter().find(|t| t.name.ends_with(target_table_base))) .map_or(target_table_base.to_string(), |t| t.name.clone()); FieldDefinition { display_name: full_target_table_name.clone(), data_key: col.name.clone(), // The actual FK column name is_link: true, link_target_table: Some(full_target_table_name), } }) .collect(); fields.extend(link_fields); // Append the link fields to the end // --- END: FINAL, SIMPLIFIED, CORRECT LOGIC --- Ok(FormState::new( profile_name.to_string(), table_name.to_string(), fields, )) } pub async fn initialize_add_logic_table_data( grpc_client: &mut GrpcClient, add_logic_state: &mut AddLogicState, profile_tree: &common::proto::komp_ac::table_definition::ProfileTreeResponse, ) -> Result { let profile_name_clone_opt = Some(add_logic_state.profile_name.clone()); let table_name_opt_clone = add_logic_state.selected_table_name.clone(); // Collect table names from SAME profile only let same_profile_table_names: Vec = profile_tree.profiles .iter() .find(|profile| profile.name == add_logic_state.profile_name) .map(|profile| profile.tables.iter().map(|table| table.name.clone()).collect()) .unwrap_or_default(); // Set same profile table names for autocomplete add_logic_state.set_same_profile_table_names(same_profile_table_names.clone()); if let (Some(profile_name_clone), Some(table_name_clone)) = (profile_name_clone_opt, table_name_opt_clone) { match grpc_client.get_table_structure(profile_name_clone.clone(), table_name_clone.clone()).await { Ok(response) => { let column_names: Vec = response.columns .into_iter() .map(|col| col.name) .collect(); add_logic_state.set_table_columns(column_names.clone()); Ok(format!( "Loaded {} columns for table '{}' and {} tables from profile '{}'", column_names.len(), table_name_clone, same_profile_table_names.len(), add_logic_state.profile_name )) } Err(e) => { tracing::warn!( "Failed to fetch table structure for {}.{}: {}", profile_name_clone, table_name_clone, e ); Ok(format!( "Warning: Could not load table structure for '{}'. Autocomplete will use {} tables from profile '{}'.", table_name_clone, same_profile_table_names.len(), add_logic_state.profile_name )) } } } else { Ok(format!( "No table selected for Add Logic. Loaded {} tables from profile '{}' for autocomplete.", same_profile_table_names.len(), add_logic_state.profile_name )) } } /// Fetches columns for a specific table (used for table.column autocomplete) pub async fn fetch_columns_for_table( grpc_client: &mut GrpcClient, profile_name: &str, table_name: &str, ) -> Result> { match grpc_client.get_table_structure(profile_name.to_string(), table_name.to_string()).await { Ok(response) => { let column_names: Vec = response.columns .into_iter() .map(|col| col.name) .collect(); Ok(filter_user_columns(column_names)) } Err(e) => { tracing::warn!("Failed to fetch columns for {}.{}: {}", profile_name, table_name, e); Err(e.into()) } } } // REFACTOR THIS FUNCTION pub async fn initialize_app_state_and_form( grpc_client: &mut GrpcClient, app_state: &mut AppState, ) -> Result<(String, String, Vec)> { let profile_tree = grpc_client .get_profile_tree() .await .context("Failed to get profile tree")?; app_state.profile_tree = profile_tree; let initial_profile_name = app_state .profile_tree .profiles .first() .map(|p| p.name.clone()) .unwrap_or_else(|| "default".to_string()); let initial_table_name = app_state .profile_tree .profiles .first() .and_then(|p| p.tables.first().map(|t| t.name.clone())) .unwrap_or_else(|| "2025_company_data1".to_string()); app_state.set_current_view_table( initial_profile_name.clone(), initial_table_name.clone(), ); // NOW, just call our new central function. This avoids code duplication. let form_state = Self::load_table_view( grpc_client, app_state, &initial_profile_name, &initial_table_name, ) .await?; // The field names for the UI are derived from the new form_state let field_names = form_state.fields.iter().map(|f| f.display_name.clone()).collect(); Ok((initial_profile_name, initial_table_name, field_names)) } pub async fn fetch_and_set_table_count( grpc_client: &mut GrpcClient, form_state: &mut FormState, ) -> Result<()> { let total_count = grpc_client .get_table_data_count( form_state.profile_name.clone(), form_state.table_name.clone(), ) .await .context(format!( "Failed to get count for table {}.{}", form_state.profile_name, form_state.table_name ))?; form_state.total_count = total_count; if total_count > 0 { form_state.current_position = total_count; } else { form_state.current_position = 1; } Ok(()) } pub async fn load_table_data_by_position( grpc_client: &mut GrpcClient, form_state: &mut FormState, ) -> Result { if form_state.current_position == 0 || (form_state.total_count > 0 && form_state.current_position > form_state.total_count) { form_state.reset_to_empty(); return Ok(format!( "New entry mode for table {}.{}", form_state.profile_name, form_state.table_name )); } if form_state.total_count == 0 && form_state.current_position == 1 { form_state.reset_to_empty(); return Ok(format!( "New entry mode for empty table {}.{}", form_state.profile_name, form_state.table_name )); } match grpc_client .get_table_data_by_position( form_state.profile_name.clone(), form_state.table_name.clone(), form_state.current_position as i32, ) .await { Ok(response) => { // FIX: Pass the current position as the second argument form_state.update_from_response(&response.data, form_state.current_position); Ok(format!( "Loaded entry {}/{} for table {}.{}", form_state.current_position, form_state.total_count, form_state.profile_name, form_state.table_name )) } Err(e) => { tracing::error!( "Error loading entry {} for table {}.{}: {}", form_state.current_position, form_state.profile_name, form_state.table_name, e ); Err(anyhow::anyhow!( "Error loading entry {}: {}", form_state.current_position, e )) } } } pub async fn handle_save_outcome( save_outcome: SaveOutcome, _grpc_client: &mut GrpcClient, _app_state: &mut AppState, form_state: &mut FormState, ) -> Result<()> { match save_outcome { SaveOutcome::CreatedNew(new_id) => { form_state.id = new_id; } SaveOutcome::UpdatedExisting | SaveOutcome::NoChange => { // No action needed } } Ok(()) } }