use ratatui::{
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph},
Frame,
};
use crate::models::{CategoryId, Money};
use crate::tui::app::App;
use crate::tui::layout::centered_rect_fixed;
#[derive(Debug, Clone, Default)]
pub struct AdjustmentDialogState {
pub adjustment_amount: Money,
pub category_input: String,
pub selected_category: Option<CategoryId>,
pub categories: Vec<(CategoryId, String)>,
pub selected_index: usize,
pub selecting_category: bool,
}
impl AdjustmentDialogState {
pub fn new(adjustment_amount: Money) -> Self {
Self {
adjustment_amount,
category_input: String::new(),
selected_category: None,
categories: Vec::new(),
selected_index: 0,
selecting_category: false,
}
}
pub fn load_categories(&mut self, categories: Vec<(CategoryId, String)>) {
self.categories = categories;
}
pub fn move_up(&mut self) {
if self.selecting_category && self.selected_index > 0 {
self.selected_index -= 1;
}
}
pub fn move_down(&mut self) {
if self.selecting_category && self.selected_index < self.categories.len().saturating_sub(1)
{
self.selected_index += 1;
}
}
pub fn filtered_categories(&self) -> Vec<&(CategoryId, String)> {
if self.category_input.is_empty() {
self.categories.iter().collect()
} else {
let search = self.category_input.to_lowercase();
self.categories
.iter()
.filter(|(_, name)| name.to_lowercase().contains(&search))
.collect()
}
}
pub fn select_current(&mut self) {
let selection: Option<(CategoryId, String)> = {
let filtered = self.filtered_categories();
filtered
.get(self.selected_index)
.map(|(id, name)| (*id, name.clone()))
};
if let Some((id, name)) = selection {
self.selected_category = Some(id);
self.category_input = name;
self.selecting_category = false;
}
}
}
pub fn render(frame: &mut Frame, app: &App) {
let area = centered_rect_fixed(60, 14, frame.area());
frame.render_widget(Clear, area);
let state = &app.adjustment_dialog_state;
let block = Block::default()
.title(" Create Adjustment Transaction ")
.title_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow));
let inner = block.inner(area);
frame.render_widget(block, area);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Length(2), Constraint::Length(2), Constraint::Length(2), Constraint::Length(3), Constraint::Length(2), ])
.split(inner);
let message = Paragraph::new(Line::from(vec![Span::styled(
"Your cleared balance doesn't match the statement balance.",
Style::default().fg(Color::White),
)]));
frame.render_widget(message, chunks[1]);
let amount_style = if state.adjustment_amount.is_negative() {
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
} else {
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD)
};
let amount_text = Paragraph::new(Line::from(vec![
Span::styled("Adjustment needed: ", Style::default().fg(Color::Yellow)),
Span::styled(format!("{}", state.adjustment_amount), amount_style),
]));
frame.render_widget(amount_text, chunks[2]);
let category_style = if state.selecting_category {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::White)
};
let category_display = if state.category_input.is_empty() {
"(Optional) Enter category...".to_string()
} else {
state.category_input.clone()
};
let category_text = Paragraph::new(Line::from(vec![
Span::styled("Category: ", Style::default().fg(Color::Yellow)),
Span::styled(category_display, category_style),
]));
frame.render_widget(category_text, chunks[3]);
if state.selecting_category {
let filtered = state.filtered_categories();
let items: Vec<Line> = filtered
.iter()
.enumerate()
.take(3)
.map(|(i, (_, name))| {
let style = if i == state.selected_index {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default().fg(Color::Yellow)
};
Line::from(Span::styled(format!(" {}", name), style))
})
.collect();
let list = Paragraph::new(items);
frame.render_widget(list, chunks[4]);
}
let instructions = Paragraph::new(Line::from(vec![
Span::styled("[Enter]", Style::default().fg(Color::Green)),
Span::raw(" Create "),
Span::styled("[Tab]", Style::default().fg(Color::Cyan)),
Span::raw(" Select category "),
Span::styled("[Esc]", Style::default().fg(Color::Yellow)),
Span::raw(" Cancel"),
]));
frame.render_widget(instructions, chunks[5]);
}
pub fn handle_key(app: &mut App, key: crossterm::event::KeyCode) -> bool {
use crossterm::event::KeyCode;
let state = &mut app.adjustment_dialog_state;
match key {
KeyCode::Tab => {
state.selecting_category = !state.selecting_category;
true
}
KeyCode::Up | KeyCode::Char('k') if state.selecting_category => {
state.move_up();
true
}
KeyCode::Down | KeyCode::Char('j') if state.selecting_category => {
state.move_down();
true
}
KeyCode::Enter if state.selecting_category => {
state.select_current();
true
}
KeyCode::Char(c) if !state.selecting_category => {
state.category_input.push(c);
state.selecting_category = true;
state.selected_index = 0;
true
}
KeyCode::Backspace if !state.selecting_category => {
state.category_input.pop();
true
}
_ => false,
}
}