//! Screen-specific input handlers. use std::path::Path; use crossterm::event::{KeyCode, KeyEvent}; use tokio::sync::mpsc; use crate::api::MnwApiClient; use super::loading::*; use super::{ item, App, AppEvent, BlogCreateStep, ConfirmAction, DataPayload, EditField, PromoCreateStep, Screen, }; pub(super) async fn handle_home_input( key: KeyEvent, app: &mut App, screen: &mut Screen, api: &MnwApiClient, tx: &mpsc::Sender, staging_dir: &Path, ) { match key.code { KeyCode::Char('j') | KeyCode::Down => app.move_down(screen), KeyCode::Char('k') | KeyCode::Up => app.move_up(screen), KeyCode::Enter => { if !app.projects.is_empty() { let idx = app.selected_index; let project_id = app.projects[idx].id.clone(); let user_id = app.user.user_id.clone(); *screen = Screen::Project(idx); app.items.clear(); app.selected_index = 0; app.loading = true; let api = api.clone(); let tx = tx.clone(); tokio::spawn(async move { load_project_items(&api, &project_id, &user_id, &tx).await; }); } } KeyCode::Char('u') | KeyCode::Char('U') => { *screen = Screen::Upload; app.selected_index = 0; app.loading = true; app.upload_status = None; app.editing_field = None; let staging_dir = staging_dir.to_path_buf(); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_staged_files(&staging_dir, &api, &user_id, &tx).await; }); } KeyCode::Char('a') | KeyCode::Char('A') => { *screen = Screen::Analytics; app.analytics_data = None; app.analytics_status = None; app.analytics_show_transactions = false; app.selected_index = 0; app.loading = true; let api = api.clone(); let user_id = app.user.user_id.clone(); let range = app.analytics_range.clone(); let tx = tx.clone(); tokio::spawn(async move { load_analytics(&api, &user_id, &range, &tx).await; }); } KeyCode::Char('p') | KeyCode::Char('P') => { *screen = Screen::Promo; app.promo_codes.clear(); app.promo_status = None; app.promo_editing_step = None; app.selected_index = 0; app.loading = true; let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_promo_codes(&api, &user_id, &tx).await; }); } KeyCode::Char('s') | KeyCode::Char('S') => { *screen = Screen::Settings; app.ssh_keys.clear(); app.settings_status = None; app.selected_index = 0; app.loading = true; let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_settings(&api, &user_id, &tx).await; }); } KeyCode::Char('r') | KeyCode::Char('R') => { app.loading = true; let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_home_data(&api, &user_id, &tx).await; }); } _ => {} } } pub(super) async fn handle_project_input( key: KeyEvent, app: &mut App, screen: &mut Screen, api: &MnwApiClient, tx: &mpsc::Sender, ) { match key.code { KeyCode::Char('j') | KeyCode::Down => app.move_down(screen), KeyCode::Char('k') | KeyCode::Up => app.move_up(screen), KeyCode::Esc => { *screen = Screen::Home; app.items.clear(); app.selected_index = 0; } KeyCode::Enter => { if let Screen::Project(pidx) = screen && !app.items.is_empty() { let item_id = app.items[app.selected_index].id.clone(); let pidx = *pidx; *screen = Screen::Item(pidx, item_id.clone()); app.item_detail = None; app.item_versions.clear(); app.item_status = None; app.item_editing = None; app.selected_index = 0; app.loading = true; let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_item_detail(&api, &user_id, &item_id, &tx).await; }); } } KeyCode::Char('b') | KeyCode::Char('B') => { // Open blog screen for this project if let Screen::Project(idx) = screen && let Some(p) = app.projects.get(*idx) { let pidx = *idx; let project_id = p.id.clone(); let project_title = p.title.clone(); *screen = Screen::Blog(pidx, project_id.clone()); app.blog_posts.clear(); app.blog_project_title = Some(project_title); app.blog_status = None; app.blog_create_step = None; app.selected_index = 0; app.loading = true; let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_blog_posts(&api, &user_id, &project_id, &tx).await; }); } } KeyCode::Char('r') | KeyCode::Char('R') => { if let Screen::Project(idx) = screen && let Some(p) = app.projects.get(*idx) { app.loading = true; let api = api.clone(); let project_id = p.id.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_project_items(&api, &project_id, &user_id, &tx).await; }); } } _ => {} } } pub(super) async fn handle_upload_input( key: KeyEvent, app: &mut App, screen: &mut Screen, api: &MnwApiClient, tx: &mpsc::Sender, staging_dir: &Path, ) { // Handle editing mode if let Some(field) = app.editing_field { match key.code { KeyCode::Esc => { app.editing_field = None; app.edit_buffer.clear(); } KeyCode::Enter => { let idx = app.selected_index; if idx < app.file_metadata.len() { match field { EditField::Title => { if !app.edit_buffer.is_empty() { app.file_metadata[idx].title = Some(app.edit_buffer.clone()); } // Show project selection app.edit_buffer.clear(); app.editing_field = Some(EditField::Project); let project_list = app .projects .iter() .enumerate() .map(|(i, p)| format!("{}:{}", i + 1, p.title)) .collect::>() .join(" "); app.upload_status = Some(format!("Project #: _ | {}", project_list)); return; } EditField::Project => { if let Ok(n) = app.edit_buffer.parse::() && n > 0 && n <= app.projects.len() { let pidx = n - 1; app.file_metadata[idx].project_idx = Some(pidx); app.file_metadata[idx].project_name = Some(app.projects[pidx].title.clone()); } // Advance to Price field app.edit_buffer.clear(); app.editing_field = Some(EditField::Price); app.upload_status = Some("Price ($): _ (0 or empty for free)".to_string()); return; } EditField::Price => { let cents = super::parse_price(&app.edit_buffer); app.file_metadata[idx].price_cents = cents; } } } app.editing_field = None; app.edit_buffer.clear(); app.upload_status = None; } KeyCode::Backspace => { app.edit_buffer.pop(); } KeyCode::Char(c) => { app.edit_buffer.push(c); if let Some(field) = app.editing_field { app.upload_status = Some(super::format_edit_prompt(field, &app.edit_buffer)); } } _ => {} } return; } // Normal mode match key.code { KeyCode::Char('j') | KeyCode::Down => app.move_down(screen), KeyCode::Char('k') | KeyCode::Up => app.move_up(screen), KeyCode::Esc => { *screen = Screen::Home; app.staged_files.clear(); app.file_metadata.clear(); app.upload_status = None; app.selected_index = 0; } KeyCode::Char('e') | KeyCode::Char('E') => { if !app.staged_files.is_empty() { let idx = app.selected_index; let current_title = app.file_metadata.get(idx).and_then(|m| m.title.clone()); app.editing_field = Some(EditField::Title); app.edit_buffer = current_title.unwrap_or_default(); app.upload_status = Some(super::format_edit_prompt(EditField::Title, &app.edit_buffer)); } } KeyCode::Char('p') | KeyCode::Char('P') => { if !app.staged_files.is_empty() && !app.publishing { let idx = app.selected_index; let file = &app.staged_files[idx]; let meta = app.file_metadata.get(idx).cloned().unwrap_or_default(); // Validate let Some(classification) = file.classification else { app.upload_status = Some("Unsupported file type".to_string()); return; }; let Some(project_idx) = meta.project_idx else { app.upload_status = Some("Set project first (press [e] to edit)".to_string()); return; }; let Some(project) = app.projects.get(project_idx) else { app.upload_status = Some("Invalid project".to_string()); return; }; let title = meta .title .unwrap_or_else(|| crate::staging::derive_title(&file.filename)); let project_id = project.id.clone(); let user_id = app.user.user_id.clone(); let filename = file.filename.clone(); let file_path = staging_dir.join(&filename); let price_cents = meta.price_cents; let item_type = classification.item_type.to_string(); let file_type = classification.file_type.to_string(); let content_type = classification.content_type.to_string(); let api = api.clone(); let tx = tx.clone(); app.publishing = true; app.upload_status = Some(format!("Publishing {}...", filename)); tokio::spawn(async move { let result = publish_file( &api, &user_id, &project_id, &title, &item_type, &file_type, &filename, &content_type, price_cents, &file_path, ) .await; let (success, error) = match result { Ok(()) => (true, None), Err(e) => (false, Some(e.to_string())), }; let _ = tx .send(AppEvent::DataLoaded(DataPayload::PublishResult { filename, success, error, })) .await; }); } } KeyCode::Char('d') | KeyCode::Char('D') => { if !app.staged_files.is_empty() { let idx = app.selected_index; let filename = app.staged_files[idx].filename.clone(); let file_path = staging_dir.join(&filename); let staging_dir = staging_dir.to_path_buf(); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); app.upload_status = Some(format!("Deleting {}...", filename)); tokio::spawn(async move { if let Err(e) = tokio::fs::remove_file(&file_path).await { tracing::warn!(error = %e, "failed to delete staged file"); } load_staged_files(&staging_dir, &api, &user_id, &tx).await; }); } } KeyCode::Char('r') | KeyCode::Char('R') => { app.loading = true; let staging_dir = staging_dir.to_path_buf(); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_staged_files(&staging_dir, &api, &user_id, &tx).await; }); } _ => {} } } pub(super) async fn handle_item_input( key: KeyEvent, app: &mut App, screen: &mut Screen, api: &MnwApiClient, tx: &mpsc::Sender, ) { use item::ItemEditField; // Cancel pending confirmation on any key other than the confirmation key if app.confirm_action.is_some() && !matches!(key.code, KeyCode::Char('d') | KeyCode::Char('D')) { app.confirm_action = None; app.item_status = None; } // Handle editing mode if let Some(field) = app.item_editing { match key.code { KeyCode::Esc => { app.item_editing = None; app.edit_buffer.clear(); app.item_status = None; } KeyCode::Enter => { if let Some(ref detail) = app.item_detail { let item_id = detail.id.clone(); let user_id = app.user.user_id.clone(); let api = api.clone(); let tx = tx.clone(); let buffer = app.edit_buffer.clone(); match field { ItemEditField::Title => { if !buffer.is_empty() { let title = buffer; tokio::spawn(async move { match api .update_item(&user_id, &item_id, Some(&title), None, None, None) .await { Ok(d) => { let _ = tx .send(AppEvent::DataLoaded( DataPayload::ItemUpdated { detail: d }, )) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded( DataPayload::ItemActionError { error: e.to_string(), }, )) .await; } } }); } else { app.item_editing = None; app.edit_buffer.clear(); } } ItemEditField::Description => { let desc = if buffer.is_empty() { None } else { Some(buffer.as_str().to_string()) }; tokio::spawn(async move { match api .update_item( &user_id, &item_id, None, desc.as_deref(), None, None, ) .await { Ok(d) => { let _ = tx .send(AppEvent::DataLoaded( DataPayload::ItemUpdated { detail: d }, )) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded( DataPayload::ItemActionError { error: e.to_string(), }, )) .await; } } }); } ItemEditField::Price => { let cents = super::parse_price(&buffer); tokio::spawn(async move { match api .update_item(&user_id, &item_id, None, None, Some(cents), None) .await { Ok(d) => { let _ = tx .send(AppEvent::DataLoaded( DataPayload::ItemUpdated { detail: d }, )) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded( DataPayload::ItemActionError { error: e.to_string(), }, )) .await; } } }); } } } if app.item_editing.is_some() { // Only clear if not already cleared by the empty-buffer path app.item_editing = None; app.edit_buffer.clear(); } return; } KeyCode::Backspace => { app.edit_buffer.pop(); } KeyCode::Char(c) => { app.edit_buffer.push(c); } _ => {} } return; } // Normal mode match key.code { KeyCode::Char('j') | KeyCode::Down => app.move_down(screen), KeyCode::Char('k') | KeyCode::Up => app.move_up(screen), KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => { // Go back to project view if let Screen::Item(project_idx, _) = screen { let pidx = *project_idx; *screen = Screen::Project(pidx); app.item_detail = None; app.item_versions.clear(); app.item_status = None; app.item_editing = None; app.selected_index = 0; } } KeyCode::Char('e') | KeyCode::Char('E') => { // Start editing — cycle through Title → Description → Price app.item_editing = Some(ItemEditField::Title); app.edit_buffer.clear(); app.item_status = Some("Editing title (Enter to save, Esc to cancel)".to_string()); } KeyCode::Tab => { // Cycle edit fields when in edit mode (already started with [e]) if let Some(field) = app.item_editing { let next = match field { ItemEditField::Title => ItemEditField::Description, ItemEditField::Description => ItemEditField::Price, ItemEditField::Price => ItemEditField::Title, }; app.item_editing = Some(next); app.edit_buffer.clear(); let label = match next { ItemEditField::Title => "title", ItemEditField::Description => "description", ItemEditField::Price => "price", }; app.item_status = Some(format!("Editing {} (Enter to save, Esc to cancel)", label)); } } KeyCode::Char('p') | KeyCode::Char('P') => { if let Some(ref detail) = app.item_detail && !detail.is_public { let item_id = detail.id.clone(); let user_id = app.user.user_id.clone(); let api = api.clone(); let tx = tx.clone(); app.item_status = Some("Publishing...".to_string()); tokio::spawn(async move { match api.publish_item(&user_id, &item_id).await { Ok(d) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemUpdated { detail: d, })) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemActionError { error: e.to_string(), })) .await; } } }); } } KeyCode::Char('u') | KeyCode::Char('U') => { if let Some(ref detail) = app.item_detail && detail.is_public { let item_id = detail.id.clone(); let user_id = app.user.user_id.clone(); let api = api.clone(); let tx = tx.clone(); app.item_status = Some("Unpublishing...".to_string()); tokio::spawn(async move { match api.unpublish_item(&user_id, &item_id).await { Ok(d) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemUpdated { detail: d, })) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemActionError { error: e.to_string(), })) .await; } } }); } } KeyCode::Char('d') | KeyCode::Char('D') => { if let Some(ref detail) = app.item_detail { if matches!(app.confirm_action, Some(ConfirmAction::DeleteItem)) { // Confirmed — execute delete app.confirm_action = None; let item_id = detail.id.clone(); let user_id = app.user.user_id.clone(); let api = api.clone(); let tx = tx.clone(); app.item_status = Some("Deleting...".to_string()); tokio::spawn(async move { match api.delete_item(&user_id, &item_id).await { Ok(()) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemDeleted)) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ItemActionError { error: e.to_string(), })) .await; } } }); } else { // First press — ask for confirmation app.confirm_action = Some(ConfirmAction::DeleteItem); app.item_status = Some(format!( "Delete '{}'? Press d again to confirm", detail.title )); } } } KeyCode::Char('l') | KeyCode::Char('L') => { // Open license keys screen if let Screen::Item(project_idx, item_id) = &*screen { let pidx = *project_idx; let iid = item_id.clone(); let item_title = app .item_detail .as_ref() .map(|d| d.title.clone()) .unwrap_or_default(); *screen = Screen::Keys(pidx, iid.clone()); app.license_keys.clear(); app.keys_item_title = Some(item_title); app.keys_status = None; app.selected_index = 0; app.loading = true; let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_license_keys(&api, &user_id, &iid, &tx).await; }); } } KeyCode::Char('r') | KeyCode::Char('R') => { if let Screen::Item(_, item_id) = &*screen { app.loading = true; let item_id = item_id.clone(); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_item_detail(&api, &user_id, &item_id, &tx).await; }); } } _ => {} } } pub(super) async fn handle_blog_input( key: KeyEvent, app: &mut App, screen: &mut Screen, api: &MnwApiClient, tx: &mpsc::Sender, ) { // Cancel pending confirmation on any key other than the confirmation key if app.confirm_action.is_some() && !matches!(key.code, KeyCode::Char('d') | KeyCode::Char('D')) { app.confirm_action = None; app.blog_status = None; } // Creating mode if let Some(step) = app.blog_create_step { match key.code { KeyCode::Esc => { app.blog_create_step = None; app.blog_create_title.clear(); app.edit_buffer.clear(); app.blog_status = None; } KeyCode::Enter => match step { BlogCreateStep::Title => { if !app.edit_buffer.is_empty() { app.blog_create_title = app.edit_buffer.clone(); app.edit_buffer.clear(); app.blog_create_step = Some(BlogCreateStep::Body); app.blog_status = Some("Body (markdown, Enter to submit empty for draft):".to_string()); } } BlogCreateStep::Body => { let title = app.blog_create_title.clone(); let body = app.edit_buffer.clone(); if let Screen::Blog(_, project_id) = &*screen { let project_id = project_id.clone(); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); app.blog_creating = true; app.blog_status = Some("Creating post...".to_string()); tokio::spawn(async move { match api .create_blog_post(&user_id, &project_id, &title, &body, false) .await { Ok(_post) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::BlogCreated)) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::GenericError { error: e.to_string(), })) .await; } } }); } app.blog_create_step = None; app.blog_create_title.clear(); app.edit_buffer.clear(); } }, KeyCode::Backspace => { app.edit_buffer.pop(); } KeyCode::Char(c) => { app.edit_buffer.push(c); } _ => {} } return; } // Normal mode match key.code { KeyCode::Char('j') | KeyCode::Down => app.move_down(screen), KeyCode::Char('k') | KeyCode::Up => app.move_up(screen), KeyCode::Esc => { if let Screen::Blog(pidx, _) = screen { *screen = Screen::Project(*pidx); app.blog_posts.clear(); app.blog_status = None; app.selected_index = 0; } } KeyCode::Char('n') | KeyCode::Char('N') => { app.blog_create_step = Some(BlogCreateStep::Title); app.edit_buffer.clear(); app.blog_status = Some("Title: _".to_string()); } KeyCode::Char('d') | KeyCode::Char('D') => { if !app.blog_posts.is_empty() { let idx = app.selected_index; if matches!(app.confirm_action, Some(ConfirmAction::DeleteBlogPost { post_idx }) if post_idx == idx) { // Confirmed — execute delete app.confirm_action = None; let post_id = app.blog_posts[idx].id.clone(); let user_id = app.user.user_id.clone(); let api = api.clone(); let tx = tx.clone(); if let Screen::Blog(_, project_id) = &*screen { let project_id = project_id.clone(); app.blog_status = Some("Deleting...".to_string()); tokio::spawn(async move { match api.delete_blog_post(&user_id, &post_id).await { Ok(()) => { load_blog_posts(&api, &user_id, &project_id, &tx).await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::GenericError { error: e.to_string(), })) .await; } } }); } } else { // First press — ask for confirmation app.confirm_action = Some(ConfirmAction::DeleteBlogPost { post_idx: idx }); app.blog_status = Some(format!( "Delete '{}'? Press d again to confirm", app.blog_posts[idx].title )); } } } KeyCode::Char('r') | KeyCode::Char('R') => { if let Screen::Blog(_, project_id) = &*screen { app.loading = true; let project_id = project_id.clone(); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_blog_posts(&api, &user_id, &project_id, &tx).await; }); } } _ => {} } } pub(super) async fn handle_promo_input( key: KeyEvent, app: &mut App, screen: &mut Screen, api: &MnwApiClient, tx: &mpsc::Sender, ) { // Cancel pending confirmation on any key other than the confirmation key if app.confirm_action.is_some() && !matches!(key.code, KeyCode::Char('d') | KeyCode::Char('D')) { app.confirm_action = None; app.promo_status = None; } // Creating mode if let Some(step) = app.promo_editing_step { match key.code { KeyCode::Esc => { app.promo_editing_step = None; app.promo_create_code.clear(); app.promo_create_discount.clear(); app.edit_buffer.clear(); app.promo_status = None; } KeyCode::Enter => match step { PromoCreateStep::Code => { if !app.edit_buffer.is_empty() { app.promo_create_code = app.edit_buffer.clone(); app.edit_buffer.clear(); app.promo_editing_step = Some(PromoCreateStep::Discount); app.promo_status = Some("Discount % (e.g. 25): _".to_string()); } } PromoCreateStep::Discount => { let code = app.promo_create_code.clone(); let discount: i32 = app.edit_buffer.parse().unwrap_or(0); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); app.promo_status = Some("Creating...".to_string()); tokio::spawn(async move { match api .create_promo_code( &user_id, &code, "percentage", discount, None, None, ) .await { Ok(_) => { load_promo_codes(&api, &user_id, &tx).await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::GenericError { error: e.to_string(), })) .await; } } }); app.promo_editing_step = None; app.promo_create_code.clear(); app.promo_create_discount.clear(); app.edit_buffer.clear(); } }, KeyCode::Backspace => { app.edit_buffer.pop(); } KeyCode::Char(c) => { app.edit_buffer.push(c); } _ => {} } return; } // Normal mode match key.code { KeyCode::Char('j') | KeyCode::Down => app.move_down(screen), KeyCode::Char('k') | KeyCode::Up => app.move_up(screen), KeyCode::Esc => { *screen = Screen::Home; app.promo_codes.clear(); app.promo_status = None; app.selected_index = 0; } KeyCode::Char('n') | KeyCode::Char('N') => { app.promo_editing_step = Some(PromoCreateStep::Code); app.edit_buffer.clear(); app.promo_status = Some("Code: _".to_string()); } KeyCode::Char('d') | KeyCode::Char('D') => { if !app.promo_codes.is_empty() { let idx = app.selected_index; if matches!(app.confirm_action, Some(ConfirmAction::DeletePromoCode { code_idx }) if code_idx == idx) { // Confirmed — execute delete app.confirm_action = None; let code_id = app.promo_codes[idx].id.clone(); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); app.promo_status = Some("Deleting...".to_string()); tokio::spawn(async move { match api.delete_promo_code(&user_id, &code_id).await { Ok(()) => { load_promo_codes(&api, &user_id, &tx).await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::GenericError { error: e.to_string(), })) .await; } } }); } else { // First press — ask for confirmation app.confirm_action = Some(ConfirmAction::DeletePromoCode { code_idx: idx }); app.promo_status = Some(format!( "Delete '{}'? Press d again to confirm", app.promo_codes[idx].code )); } } } KeyCode::Char('r') | KeyCode::Char('R') => { app.loading = true; let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_promo_codes(&api, &user_id, &tx).await; }); } _ => {} } } pub(super) async fn handle_keys_input( key: KeyEvent, app: &mut App, screen: &mut Screen, api: &MnwApiClient, tx: &mpsc::Sender, ) { // Cancel pending confirmation on any key other than the confirmation key if app.confirm_action.is_some() && !matches!(key.code, KeyCode::Char('x') | KeyCode::Char('X')) { app.confirm_action = None; app.keys_status = None; } match key.code { KeyCode::Char('j') | KeyCode::Down => app.move_down(screen), KeyCode::Char('k') | KeyCode::Up => app.move_up(screen), KeyCode::Esc => { if let Screen::Keys(pidx, item_id) = &*screen { let pidx = *pidx; let iid = item_id.clone(); *screen = Screen::Item(pidx, iid.clone()); app.license_keys.clear(); app.keys_status = None; app.selected_index = 0; app.loading = true; let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_item_detail(&api, &user_id, &iid, &tx).await; }); } } KeyCode::Char('g') | KeyCode::Char('G') => { if let Screen::Keys(_, item_id) = &*screen { let item_id = item_id.clone(); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); app.keys_status = Some("Generating...".to_string()); let iid = item_id.clone(); tokio::spawn(async move { match api.generate_license_key(&user_id, &item_id).await { Ok(key) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::GenericSuccess { message: format!("Generated: {}", key.key_code), })) .await; load_license_keys(&api, &user_id, &iid, &tx).await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::GenericError { error: e.to_string(), })) .await; } } }); } } KeyCode::Char('x') | KeyCode::Char('X') => { if !app.license_keys.is_empty() { let idx = app.selected_index; if matches!(app.confirm_action, Some(ConfirmAction::RevokeLicenseKey { key_idx }) if key_idx == idx) { // Confirmed — execute revoke app.confirm_action = None; let key_id = app.license_keys[idx].id.clone(); if let Screen::Keys(_, item_id) = &*screen { let item_id = item_id.clone(); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); app.keys_status = Some("Revoking...".to_string()); tokio::spawn(async move { match api.revoke_license_key(&user_id, &key_id).await { Ok(()) => { load_license_keys(&api, &user_id, &item_id, &tx).await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::GenericError { error: e.to_string(), })) .await; } } }); } } else { // First press — ask for confirmation app.confirm_action = Some(ConfirmAction::RevokeLicenseKey { key_idx: idx }); app.keys_status = Some(format!( "Revoke '{}'? Press x again to confirm", app.license_keys[idx].key_code )); } } } KeyCode::Char('r') | KeyCode::Char('R') => { if let Screen::Keys(_, item_id) = &*screen { app.loading = true; let item_id = item_id.clone(); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_license_keys(&api, &user_id, &item_id, &tx).await; }); } } _ => {} } } pub(super) async fn handle_analytics_input( key: KeyEvent, app: &mut App, screen: &mut Screen, api: &MnwApiClient, tx: &mpsc::Sender, ) { match key.code { KeyCode::Char('j') | KeyCode::Down => { if app.analytics_show_transactions { app.move_down(screen); } } KeyCode::Char('k') | KeyCode::Up => { if app.analytics_show_transactions { app.move_up(screen); } } KeyCode::Esc => { if app.analytics_show_transactions { app.analytics_show_transactions = false; app.selected_index = 0; } else { *screen = Screen::Home; app.analytics_data = None; app.analytics_status = None; app.selected_index = 0; } } // Range selection: 1=7d, 2=30d, 3=90d, 4=all KeyCode::Char('1') => { app.analytics_range = "7d".to_string(); reload_analytics(app, api, tx); } KeyCode::Char('2') => { app.analytics_range = "30d".to_string(); reload_analytics(app, api, tx); } KeyCode::Char('3') => { app.analytics_range = "90d".to_string(); reload_analytics(app, api, tx); } KeyCode::Char('4') => { app.analytics_range = "all".to_string(); reload_analytics(app, api, tx); } KeyCode::Char('t') | KeyCode::Char('T') => { if app.analytics_show_transactions { app.analytics_show_transactions = false; } else { app.analytics_show_transactions = true; app.selected_index = 0; if app.transactions.is_empty() { app.loading = true; let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_transactions(&api, &user_id, &tx).await; }); } } } KeyCode::Char('e') | KeyCode::Char('E') => { app.analytics_status = Some("Exporting...".to_string()); let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { match api.export_sales_csv(&user_id).await { Ok(result) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::ExportCsv { csv: result.csv, row_count: result.row_count, })) .await; } Err(e) => { let _ = tx .send(AppEvent::DataLoaded(DataPayload::GenericError { error: e.to_string(), })) .await; } } }); } KeyCode::Char('r') | KeyCode::Char('R') => { reload_analytics(app, api, tx); } _ => {} } } fn reload_analytics(app: &mut App, api: &MnwApiClient, tx: &mpsc::Sender) { app.loading = true; app.analytics_status = None; let api = api.clone(); let user_id = app.user.user_id.clone(); let range = app.analytics_range.clone(); let tx = tx.clone(); tokio::spawn(async move { load_analytics(&api, &user_id, &range, &tx).await; }); } pub(super) async fn handle_settings_input( key: KeyEvent, app: &mut App, screen: &mut Screen, api: &MnwApiClient, tx: &mpsc::Sender, ) { match key.code { KeyCode::Char('j') | KeyCode::Down => app.move_down(screen), KeyCode::Char('k') | KeyCode::Up => app.move_up(screen), KeyCode::Esc => { *screen = Screen::Home; app.ssh_keys.clear(); app.settings_status = None; app.selected_index = 0; } KeyCode::Char('r') | KeyCode::Char('R') => { app.loading = true; app.settings_status = None; let api = api.clone(); let user_id = app.user.user_id.clone(); let tx = tx.clone(); tokio::spawn(async move { load_settings(&api, &user_id, &tx).await; }); } _ => {} } }