Files
komp_ac/client/src/services/ui_service.rs
2025-07-25 18:18:00 +02:00

320 lines
12 KiB
Rust

// 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<FormState> {
// 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<FieldDefinition> = 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<FieldDefinition> = 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<String> {
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<String> = 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<String> = 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<Vec<String>> {
match grpc_client.get_table_structure(profile_name.to_string(), table_name.to_string()).await {
Ok(response) => {
let column_names: Vec<String> = 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<String>)> {
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<String> {
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(())
}
}