From e142f5670685d47ae9048868e3ccf037b03ea10e Mon Sep 17 00:00:00 2001 From: Priec Date: Thu, 28 Aug 2025 09:15:14 +0200 Subject: [PATCH] admin page --- client/src/modes/handlers/event.rs | 67 ++++- client/src/pages/admin/admin/mod.rs | 7 + client/src/pages/admin/admin/state.rs | 195 +++++++++++++ client/src/pages/admin/{main => admin}/tui.rs | 2 +- .../admin/{main/ui_admin.rs => admin/ui.rs} | 2 +- client/src/pages/admin/main/mod.rs | 4 +- client/src/pages/admin/main/state.rs | 259 +----------------- client/src/pages/admin/main/ui.rs | 5 +- client/src/pages/admin/mod.rs | 7 +- 9 files changed, 276 insertions(+), 272 deletions(-) create mode 100644 client/src/pages/admin/admin/mod.rs create mode 100644 client/src/pages/admin/admin/state.rs rename client/src/pages/admin/{main => admin}/tui.rs (93%) rename client/src/pages/admin/{main/ui_admin.rs => admin/ui.rs} (99%) diff --git a/client/src/modes/handlers/event.rs b/client/src/modes/handlers/event.rs index 1260c6e..0ecb30e 100644 --- a/client/src/modes/handlers/event.rs +++ b/client/src/modes/handlers/event.rs @@ -9,7 +9,7 @@ use crate::functions::modes::navigation::add_logic_nav::SaveLogicResultSender; use crate::functions::modes::navigation::add_table_nav::SaveTableResultSender; use crate::functions::modes::navigation::add_table_nav; use crate::pages::admin::main::logic::handle_admin_navigation; -use crate::pages::admin::main::tui::handle_admin_selection; +use crate::pages::admin::admin::tui::handle_admin_selection; use crate::modes::general::command_navigation::{ handle_command_navigation_event, NavigationState, }; @@ -33,6 +33,7 @@ use crate::pages::intro; use crate::pages::login::logic::LoginResult; use crate::pages::register::RegisterResult; use crate::pages::routing::{Router, Page}; +use crate::movement::MovementAction; use crate::dialog; use crate::pages::forms::FormState; use crate::pages::forms::logic::{save, revert, SaveOutcome}; @@ -404,8 +405,34 @@ impl EventHandler { } } Page::Admin(state) => { - if state.handle_movement(app_state, ma) { - return Ok(EventOutcome::Ok(String::new())); + if auth_state.role.as_deref() == Some("admin") { + if state.handle_movement(app_state, ma) { + return Ok(EventOutcome::Ok(String::new())); + } + } else { + // Non-admin: simple profile navigation + match ma { + MovementAction::Up | MovementAction::Previous => { + state.previous(); + return Ok(EventOutcome::Ok(String::new())); + } + MovementAction::Down | MovementAction::Next => { + state.next(); + return Ok(EventOutcome::Ok(String::new())); + } + MovementAction::Select => { + if let Some(idx) = state.get_selected_index() { + if let Some(profile) = app_state.profile_tree.profiles.get(idx) { + app_state.selected_profile = Some(profile.name.clone()); + return Ok(EventOutcome::Ok(format!( + "Profile '{}' selected", + profile.name + ))); + } + } + } + _ => {} + } } } Page::Intro(state) => { @@ -420,6 +447,7 @@ impl EventHandler { // Optional page-specific handlers (non-movement or rich actions) if let Page::Admin(admin_state) = &mut router.current { if auth_state.role.as_deref() == Some("admin") { + // Full admin navigation if handle_admin_navigation( key_event, config, @@ -428,9 +456,30 @@ impl EventHandler { buffer_state, &mut self.command_message, ) { - return Ok(EventOutcome::Ok( - self.command_message.clone(), - )); + return Ok(EventOutcome::Ok(self.command_message.clone())); + } + } else { + // Non-admin: allow simple profile navigation + if let Some(action) = config.get_general_action(key_event.code, key_event.modifiers) { + match action { + "move_up" => { + admin_state.previous(); + return Ok(EventOutcome::Ok(String::new())); + } + "move_down" => { + admin_state.next(); + return Ok(EventOutcome::Ok(String::new())); + } + "select" => { + if let Some(idx) = admin_state.get_selected_index() { + if let Some(profile) = app_state.profile_tree.profiles.get(idx) { + app_state.selected_profile = Some(profile.name.clone()); + } + } + return Ok(EventOutcome::Ok("Profile selected".to_string())); + } + _ => {} + } } } } @@ -471,10 +520,8 @@ impl EventHandler { } // Generic navigation for the rest (Intro/Login/Register/Form) - let nav_outcome = if matches!( - &router.current, - Page::Admin(_) | Page::AddTable(_) | Page::AddLogic(_) - ) { + let nav_outcome = if matches!(&router.current, Page::AddTable(_) | Page::AddLogic(_)) { + // Skip generic navigation for AddTable/AddLogic (they have their own handlers) Ok(EventOutcome::Ok(String::new())) } else { navigation::handle_navigation_event( diff --git a/client/src/pages/admin/admin/mod.rs b/client/src/pages/admin/admin/mod.rs new file mode 100644 index 0000000..5622ae5 --- /dev/null +++ b/client/src/pages/admin/admin/mod.rs @@ -0,0 +1,7 @@ +// src/pages/admin/admin/mod.rs + +pub mod state; +pub mod ui; +pub mod tui; + +pub use state::{AdminState, AdminFocus}; diff --git a/client/src/pages/admin/admin/state.rs b/client/src/pages/admin/admin/state.rs new file mode 100644 index 0000000..ac71ff6 --- /dev/null +++ b/client/src/pages/admin/admin/state.rs @@ -0,0 +1,195 @@ +// src/pages/admin/admin/state.rs +use ratatui::widgets::ListState; +use crate::state::pages::add_table::AddTableState; +use crate::state::pages::add_logic::AddLogicState; +use crate::movement::{move_focus, MovementAction}; +use crate::state::app::state::AppState; + +/// Focus states for the admin panel +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum AdminFocus { + #[default] + ProfilesPane, + InsideProfilesList, + Tables, + InsideTablesList, + Button1, + Button2, + Button3, +} + +/// Full admin panel state (for logged-in admins) +#[derive(Default, Clone, Debug)] +pub struct AdminState { + pub profiles: Vec, + pub profile_list_state: ListState, + pub table_list_state: ListState, + pub selected_profile_index: Option, + pub selected_table_index: Option, + pub current_focus: AdminFocus, + pub add_table_state: AddTableState, + pub add_logic_state: AddLogicState, +} + +impl AdminState { + pub fn get_selected_index(&self) -> Option { + self.profile_list_state.selected() + } + + pub fn get_selected_profile_name(&self) -> Option<&String> { + self.profile_list_state.selected().and_then(|i| self.profiles.get(i)) + } + + pub fn set_profiles(&mut self, new_profiles: Vec) { + let current_selection_index = self.profile_list_state.selected(); + self.profiles = new_profiles; + + if self.profiles.is_empty() { + self.profile_list_state.select(None); + } else { + let new_selection = match current_selection_index { + Some(index) => Some(index.min(self.profiles.len() - 1)), + None => Some(0), + }; + self.profile_list_state.select(new_selection); + } + } + + pub fn next(&mut self) { + if self.profiles.is_empty() { + self.profile_list_state.select(None); + return; + } + let i = match self.profile_list_state.selected() { + Some(i) => if i >= self.profiles.len() - 1 { 0 } else { i + 1 }, + None => 0, + }; + self.profile_list_state.select(Some(i)); + } + + pub fn previous(&mut self) { + if self.profiles.is_empty() { + self.profile_list_state.select(None); + return; + } + let i = match self.profile_list_state.selected() { + Some(i) => if i == 0 { self.profiles.len() - 1 } else { i - 1 }, + None => self.profiles.len() - 1, + }; + self.profile_list_state.select(Some(i)); + } + + pub fn handle_movement( + &mut self, + app: &AppState, + action: MovementAction, + ) -> bool { + use AdminFocus::*; + + const ORDER: [AdminFocus; 5] = [ + ProfilesPane, + Tables, + Button1, + Button2, + Button3, + ]; + + match (self.current_focus, action) { + (ProfilesPane, MovementAction::Select) => { + if !app.profile_tree.profiles.is_empty() + && self.profile_list_state.selected().is_none() + { + self.profile_list_state.select(Some(0)); + } + self.current_focus = InsideProfilesList; + return true; + } + (Tables, MovementAction::Select) => { + let p_idx = self + .selected_profile_index + .or_else(|| self.profile_list_state.selected()); + if let Some(pi) = p_idx { + let len = app + .profile_tree + .profiles + .get(pi) + .map(|p| p.tables.len()) + .unwrap_or(0); + if len > 0 && self.table_list_state.selected().is_none() { + self.table_list_state.select(Some(0)); + } + } + self.current_focus = InsideTablesList; + return true; + } + _ => {} + } + + match self.current_focus { + InsideProfilesList => match action { + MovementAction::Up => { + if !app.profile_tree.profiles.is_empty() { + let curr = self.profile_list_state.selected().unwrap_or(0); + let next = curr.saturating_sub(1); + self.profile_list_state.select(Some(next)); + } + true + } + MovementAction::Down => { + let len = app.profile_tree.profiles.len(); + if len > 0 { + let curr = self.profile_list_state.selected().unwrap_or(0); + let next = if curr + 1 < len { curr + 1 } else { curr }; + self.profile_list_state.select(Some(next)); + } + true + } + MovementAction::Esc => { + self.current_focus = ProfilesPane; + true + } + MovementAction::Next | MovementAction::Previous => true, + MovementAction::Select => false, + _ => false, + }, + InsideTablesList => { + let tables_len = { + let p_idx = self + .selected_profile_index + .or_else(|| self.profile_list_state.selected()); + p_idx.and_then(|pi| app.profile_tree.profiles.get(pi)) + .map(|p| p.tables.len()) + .unwrap_or(0) + }; + match action { + MovementAction::Up => { + if tables_len > 0 { + let curr = self.table_list_state.selected().unwrap_or(0); + let next = curr.saturating_sub(1); + self.table_list_state.select(Some(next)); + } + true + } + MovementAction::Down => { + if tables_len > 0 { + let curr = self.table_list_state.selected().unwrap_or(0); + let next = if curr + 1 < tables_len { curr + 1 } else { curr }; + self.table_list_state.select(Some(next)); + } + true + } + MovementAction::Esc => { + self.current_focus = Tables; + true + } + MovementAction::Next | MovementAction::Previous => true, + MovementAction::Select => false, + _ => false, + } + } + _ => { + move_focus(&ORDER, &mut self.current_focus, action) + } + } + } +} diff --git a/client/src/pages/admin/main/tui.rs b/client/src/pages/admin/admin/tui.rs similarity index 93% rename from client/src/pages/admin/main/tui.rs rename to client/src/pages/admin/admin/tui.rs index 459c6be..40373e3 100644 --- a/client/src/pages/admin/main/tui.rs +++ b/client/src/pages/admin/admin/tui.rs @@ -1,4 +1,4 @@ -// src/pages/admin/main/tui.rs +// src/pages/admin/admin/tui.rs use crate::state::app::state::AppState; use crate::pages::admin::AdminState; diff --git a/client/src/pages/admin/main/ui_admin.rs b/client/src/pages/admin/admin/ui.rs similarity index 99% rename from client/src/pages/admin/main/ui_admin.rs rename to client/src/pages/admin/admin/ui.rs index efe5790..cec2913 100644 --- a/client/src/pages/admin/main/ui_admin.rs +++ b/client/src/pages/admin/admin/ui.rs @@ -1,4 +1,4 @@ -// src/pages/admin/main/ui_admin.rs +// src/pages/admin/admin/ui.rs use crate::config::colors::themes::Theme; use crate::pages::admin::{AdminFocus, AdminState}; diff --git a/client/src/pages/admin/main/mod.rs b/client/src/pages/admin/main/mod.rs index e651e73..0513b00 100644 --- a/client/src/pages/admin/main/mod.rs +++ b/client/src/pages/admin/main/mod.rs @@ -2,6 +2,6 @@ pub mod state; pub mod ui; -pub mod ui_admin; pub mod logic; -pub mod tui; + +pub use state::NonAdminState; diff --git a/client/src/pages/admin/main/state.rs b/client/src/pages/admin/main/state.rs index 57d44de..cd5d6f2 100644 --- a/client/src/pages/admin/main/state.rs +++ b/client/src/pages/admin/main/state.rs @@ -1,48 +1,19 @@ // src/pages/admin/main/state.rs - use ratatui::widgets::ListState; -use crate::state::pages::add_table::AddTableState; -use crate::state::pages::add_logic::AddLogicState; -use crate::movement::{move_focus, MovementAction}; -use crate::state::app::state::AppState; - -// Define the focus states for the admin panel panes -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum AdminFocus { - #[default] // Default focus is on the profiles list - ProfilesPane, - InsideProfilesList, - Tables, - InsideTablesList, - Button1, - Button2, - Button3, -} +/// State for non-admin users (simple profile browser) #[derive(Default, Clone, Debug)] -pub struct AdminState { - pub profiles: Vec, // Holds profile names (used by non-admin view) - pub profile_list_state: ListState, // Tracks navigation highlight (>) in profiles - pub table_list_state: ListState, // Tracks navigation highlight (>) in tables - pub selected_profile_index: Option, // Index with [*] in profiles (persistent) - pub selected_table_index: Option, // Index with [*] in tables (persistent) - pub current_focus: AdminFocus, // Tracks which pane is focused - pub add_table_state: AddTableState, - pub add_logic_state: AddLogicState, +pub struct NonAdminState { + pub profiles: Vec, // profile names + pub profile_list_state: ListState, // highlight state + pub selected_profile_index: Option, // persistent selection } -impl AdminState { - /// Gets the index of the currently selected item. +impl NonAdminState { pub fn get_selected_index(&self) -> Option { self.profile_list_state.selected() } - /// Gets the name of the currently selected profile. - pub fn get_selected_profile_name(&self) -> Option<&String> { - self.profile_list_state.selected().and_then(|i| self.profiles.get(i)) - } - - /// Populates the profile list and updates/resets the selection. pub fn set_profiles(&mut self, new_profiles: Vec) { let current_selection_index = self.profile_list_state.selected(); self.profiles = new_profiles; @@ -58,7 +29,6 @@ impl AdminState { } } - /// Selects the next profile in the list, wrapping around. pub fn next(&mut self) { if self.profiles.is_empty() { self.profile_list_state.select(None); @@ -71,7 +41,6 @@ impl AdminState { self.profile_list_state.select(Some(i)); } - /// Selects the previous profile in the list, wrapping around. pub fn previous(&mut self) { if self.profiles.is_empty() { self.profile_list_state.select(None); @@ -83,220 +52,4 @@ impl AdminState { }; self.profile_list_state.select(Some(i)); } - - /// Gets the index of the currently selected profile. - pub fn get_selected_profile_index(&self) -> Option { - self.profile_list_state.selected() - } - - /// Gets the index of the currently selected table. - pub fn get_selected_table_index(&self) -> Option { - self.table_list_state.selected() - } - - /// Selects a profile by index and resets table selection. - pub fn select_profile(&mut self, index: Option) { - self.profile_list_state.select(index); - self.table_list_state.select(None); - } - - /// Selects a table by index. - pub fn select_table(&mut self, index: Option) { - self.table_list_state.select(index); - } - - /// Selects the next profile, wrapping around. - /// `profile_count` should be the total number of profiles available. - pub fn next_profile(&mut self, profile_count: usize) { - if profile_count == 0 { - return; - } - let i = match self.get_selected_profile_index() { - Some(i) => { - if i >= profile_count - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.select_profile(Some(i)); // Use the helper method - } - - /// Selects the previous profile, wrapping around. - /// `profile_count` should be the total number of profiles available. - pub fn previous_profile(&mut self, profile_count: usize) { - if profile_count == 0 { - return; - } - let i = match self.get_selected_profile_index() { - Some(i) => { - if i == 0 { - profile_count - 1 - } else { - i - 1 - } - } - None => 0, // Or profile_count - 1 if you prefer wrapping from None - }; - self.select_profile(Some(i)); // Use the helper method - } - - /// Selects the next table, wrapping around. - /// `table_count` should be the number of tables in the *currently selected* profile. - pub fn next_table(&mut self, table_count: usize) { - if table_count == 0 { - return; - } - let i = match self.get_selected_table_index() { - Some(i) => { - if i >= table_count - 1 { - 0 - } else { - i + 1 - } - } - None => 0, - }; - self.select_table(Some(i)); - } - - /// Selects the previous table, wrapping around. - /// `table_count` should be the number of tables in the *currently selected* profile. - pub fn previous_table(&mut self, table_count: usize) { - if table_count == 0 { - return; - } - let i = match self.get_selected_table_index() { - Some(i) => { - if i == 0 { - table_count - 1 - } else { - i - 1 - } - } - None => 0, // Or table_count - 1 - }; - self.select_table(Some(i)); - } -} - -impl AdminState { - pub fn handle_movement( - &mut self, - app: &AppState, - action: MovementAction, - ) -> bool { - use AdminFocus::*; - - const ORDER: [AdminFocus; 5] = [ - ProfilesPane, - Tables, - Button1, - Button2, - Button3, - ]; - - // Enter "inside" on Select - match (self.current_focus, action) { - (ProfilesPane, MovementAction::Select) => { - if !app.profile_tree.profiles.is_empty() - && self.profile_list_state.selected().is_none() - { - self.profile_list_state.select(Some(0)); - } - self.current_focus = InsideProfilesList; - return true; - } - (Tables, MovementAction::Select) => { - let p_idx = self - .selected_profile_index - .or_else(|| self.profile_list_state.selected()); - if let Some(pi) = p_idx { - let len = app - .profile_tree - .profiles - .get(pi) - .map(|p| p.tables.len()) - .unwrap_or(0); - if len > 0 && self.table_list_state.selected().is_none() { - self.table_list_state.select(Some(0)); - } - } - self.current_focus = InsideTablesList; - return true; - } - _ => {} - } - - // Inside navigation + Esc to exit; don't consume Select (admin_nav handles it) - match self.current_focus { - InsideProfilesList => match action { - MovementAction::Up => { - if !app.profile_tree.profiles.is_empty() { - let curr = self.profile_list_state.selected().unwrap_or(0); - let next = curr.saturating_sub(1); - self.profile_list_state.select(Some(next)); - } - true - } - MovementAction::Down => { - let len = app.profile_tree.profiles.len(); - if len > 0 { - let curr = self.profile_list_state.selected().unwrap_or(0); - let next = if curr + 1 < len { curr + 1 } else { curr }; - self.profile_list_state.select(Some(next)); - } - true - } - MovementAction::Esc => { - self.current_focus = ProfilesPane; - true - } - MovementAction::Next | MovementAction::Previous => true, // block outer moves - MovementAction::Select => false, - _ => false, - }, - InsideTablesList => { - let tables_len = { - let p_idx = self - .selected_profile_index - .or_else(|| self.profile_list_state.selected()); - p_idx.and_then(|pi| app.profile_tree.profiles.get(pi)) - .map(|p| p.tables.len()) - .unwrap_or(0) - }; - match action { - MovementAction::Up => { - if tables_len > 0 { - let curr = self.table_list_state.selected().unwrap_or(0); - let next = curr.saturating_sub(1); - self.table_list_state.select(Some(next)); - } - true - } - MovementAction::Down => { - if tables_len > 0 { - let curr = self.table_list_state.selected().unwrap_or(0); - let next = if curr + 1 < tables_len { curr + 1 } else { curr }; - self.table_list_state.select(Some(next)); - } - true - } - MovementAction::Esc => { - self.current_focus = Tables; - true - } - MovementAction::Next | MovementAction::Previous => true, // block outer moves - MovementAction::Select => false, - _ => false, - } - } - _ => { - // Default: outer navigation via helper - return move_focus(&ORDER, &mut self.current_focus, action); - } - } - } } diff --git a/client/src/pages/admin/main/ui.rs b/client/src/pages/admin/main/ui.rs index f166d26..5a18348 100644 --- a/client/src/pages/admin/main/ui.rs +++ b/client/src/pages/admin/main/ui.rs @@ -12,7 +12,7 @@ use ratatui::{ widgets::{Block, BorderType, Borders, List, ListItem, Paragraph, Wrap}, Frame, }; -use crate::pages::admin::main::ui_admin::render_admin_panel_admin; +use crate::pages::admin::admin::ui::render_admin_panel_admin; pub fn render_admin_panel( f: &mut Frame, @@ -92,8 +92,7 @@ fn render_admin_panel_non_admin( .block(Block::default().title("Profiles")) .highlight_style(Style::default().bg(theme.highlight).fg(theme.bg)); - let mut profile_list_state_clone = admin_state.profile_list_state.clone(); - f.render_stateful_widget(list, content_chunks[0], &mut profile_list_state_clone); + f.render_stateful_widget(list, content_chunks[0], &mut admin_state.profile_list_state.clone()); // Profile details - Use selection info from admin_state if let Some(profile) = admin_state diff --git a/client/src/pages/admin/mod.rs b/client/src/pages/admin/mod.rs index e42d67d..f50e913 100644 --- a/client/src/pages/admin/mod.rs +++ b/client/src/pages/admin/mod.rs @@ -1,4 +1,7 @@ // src/pages/admin/mod.rs -pub mod main; -pub use main::state::{AdminState, AdminFocus}; +pub mod main; // non-admin +pub mod admin; // full admin panel + +pub use main::NonAdminState; +pub use admin::{AdminState, AdminFocus};