use crate::presentation::tui::input::Key;
use crate::application::dtos::requests::MarkDoseTakenRequest;
use crate::presentation::tui::app::App;
use crate::presentation::tui::handlers::port::{Handler, HandlerResult};
use crate::presentation::tui::screen::Screen;
pub struct MarkDoseHandler;
impl Default for MarkDoseHandler {
fn default() -> Self {
MarkDoseHandler
}
}
impl Handler for MarkDoseHandler {
fn handle(&mut self, app: &mut App, key: Key) -> HandlerResult {
let (med_id, recs, sel_idx) = if let Screen::MarkDose {
medication_id,
records,
selected_index,
} = &app.current_screen
{
(medication_id.clone(), records.clone(), *selected_index)
} else {
return HandlerResult::Continue;
};
match key {
Key::Esc => {
app.current_screen = Screen::HomeScreen;
}
Key::Char('j') | Key::Down => {
let idx = (sel_idx + 1).min(recs.len().saturating_sub(1));
app.current_screen = Screen::MarkDose {
medication_id: med_id.clone(),
records: recs.to_vec(),
selected_index: idx,
};
}
Key::Char('k') | Key::Up => {
let idx = sel_idx.saturating_sub(1);
app.current_screen = Screen::MarkDose {
medication_id: med_id.clone(),
records: recs.to_vec(),
selected_index: idx,
};
}
Key::Enter => {
if recs.is_empty() {
app.set_status("No records to mark", 3000);
app.current_screen = Screen::HomeScreen;
} else {
let rec = &recs[sel_idx];
if rec.id.starts_with("slot:") {
match crate::application::ports::inbound::mark_dose_taken_port::MarkDoseTakenPort::execute(
&*app.services.mark_dose_taken,
crate::application::dtos::requests::MarkDoseTakenRequest::new_with_schedule(
rec.medication_id.clone(),
rec.scheduled_at,
),
) {
Ok(_) => app.set_status("Marked scheduled slot as taken", 3000),
Err(e) => app.status_message = Some(format!("Error: {e}")),
}
app.load_medications();
app.current_screen = Screen::MedicationDetails {
id: rec.medication_id.clone(),
};
} else {
let req = MarkDoseTakenRequest::new(rec.id.clone());
match crate::application::ports::inbound::mark_dose_taken_port::MarkDoseTakenPort::execute(&*app.services.mark_dose_taken, req) {
Ok(_) => {
app.set_status("Marked as taken", 3000);
app.load_medications();
let new_records: Vec<crate::application::dtos::responses::DoseRecordDto> = match crate::application::ports::inbound::list_dose_records_port::ListDoseRecordsPort::execute(
&*app.services.list_dose_records,
crate::application::dtos::requests::ListDoseRecordsRequest {
medication_id: med_id.clone(),
},
) {
Ok(resp) => {
let today = chrono::Local::now().date_naive();
resp.records.into_iter()
.filter(|r| r.scheduled_at.date() == today && r.taken_at.is_none())
.collect()
}
Err(_) => vec![],
};
let new_len = new_records.len();
app.current_screen = Screen::MarkDose {
medication_id: med_id,
records: new_records,
selected_index: sel_idx.min(new_len.saturating_sub(1)),
};
}
Err(e) => app.status_message = Some(format!("Error: {e}")),
}
}
}
}
_ => {}
}
HandlerResult::Continue
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::application::dtos::responses::DoseRecordDto;
use crate::presentation::tui::app::App;
use crate::presentation::tui::app_services::AppServices;
use crate::presentation::tui::screen::Screen;
use chrono::NaiveDate;
use crossterm::event::KeyCode;
fn app_with_mark_dose(records: Vec<DoseRecordDto>) -> App {
let mut app = App::new(AppServices::fake());
app.current_screen = Screen::MarkDose {
medication_id: "med-1".to_string(),
records,
selected_index: 0,
};
app
}
fn key(code: KeyCode) -> crate::presentation::tui::input::Key {
crate::presentation::tui::input::from_code(code)
}
fn dto(id: &str) -> DoseRecordDto {
DoseRecordDto {
id: id.to_string(),
medication_id: "med-1".to_string(),
scheduled_at: NaiveDate::from_ymd_opt(2025, 1, 1)
.unwrap()
.and_hms_opt(8, 0, 0)
.unwrap(),
taken_at: None,
}
}
#[test]
fn handle_dispatches_correctly_through_trait_object() {
let mut app = app_with_mark_dose(vec![]);
let mut handler: Box<dyn Handler> = Box::new(MarkDoseHandler);
handler.handle(&mut app, key(KeyCode::Esc));
assert!(matches!(app.current_screen, Screen::HomeScreen));
}
#[test]
fn esc_goes_to_home() {
let mut app = app_with_mark_dose(vec![]);
let mut h = MarkDoseHandler;
h.handle(&mut app, key(KeyCode::Esc));
assert!(matches!(app.current_screen, Screen::HomeScreen));
}
#[test]
fn down_arrow_increments_selected_index() {
let mut app = app_with_mark_dose(vec![dto("r1"), dto("r2")]);
let mut h = MarkDoseHandler;
h.handle(&mut app, key(KeyCode::Down));
let Screen::MarkDose { selected_index, .. } = &app.current_screen else {
panic!("expected MarkDose screen")
};
assert_eq!(*selected_index, 1);
}
#[test]
fn j_key_increments_selected_index() {
let mut app = app_with_mark_dose(vec![dto("r1"), dto("r2")]);
let mut h = MarkDoseHandler;
h.handle(&mut app, key(KeyCode::Char('j')));
let Screen::MarkDose { selected_index, .. } = &app.current_screen else {
panic!("expected MarkDose screen")
};
assert_eq!(*selected_index, 1);
}
#[test]
fn down_does_not_exceed_last_index() {
let mut app = app_with_mark_dose(vec![dto("r1")]);
let mut h = MarkDoseHandler;
h.handle(&mut app, key(KeyCode::Down));
let Screen::MarkDose { selected_index, .. } = &app.current_screen else {
panic!("expected MarkDose screen")
};
assert_eq!(*selected_index, 0);
}
#[test]
fn up_arrow_decrements_selected_index_clamps_at_zero() {
let mut app = app_with_mark_dose(vec![dto("r1"), dto("r2")]);
let mut h = MarkDoseHandler;
h.handle(&mut app, key(KeyCode::Up));
let Screen::MarkDose { selected_index, .. } = &app.current_screen else {
panic!("expected MarkDose screen")
};
assert_eq!(*selected_index, 0);
}
#[test]
fn k_key_decrements_selected_index() {
let mut app = app_with_mark_dose(vec![dto("r1"), dto("r2")]);
if let Screen::MarkDose {
ref mut selected_index,
..
} = app.current_screen
{
*selected_index = 1;
}
let mut h = MarkDoseHandler;
h.handle(&mut app, key(KeyCode::Char('k')));
let Screen::MarkDose { selected_index, .. } = &app.current_screen else {
panic!("expected MarkDose screen")
};
assert_eq!(*selected_index, 0);
}
#[test]
fn enter_with_empty_records_sets_status_and_goes_home() {
let mut app = app_with_mark_dose(vec![]);
let mut h = MarkDoseHandler;
h.handle(&mut app, key(KeyCode::Enter));
assert!(matches!(app.current_screen, Screen::HomeScreen));
assert!(app.status_message.is_some());
}
#[test]
fn enter_with_real_record_calls_mark_dose_taken_and_stays_on_mark_dose() {
let mut app = app_with_mark_dose(vec![dto("real-id")]);
let mut h = MarkDoseHandler;
h.handle(&mut app, key(KeyCode::Enter));
assert!(matches!(app.current_screen, Screen::MarkDose { .. }));
}
#[test]
fn enter_with_slot_record_calls_mark_medication_taken() {
let slot = DoseRecordDto {
id: "slot:0".to_string(),
medication_id: "med-1".to_string(),
scheduled_at: NaiveDate::from_ymd_opt(2025, 1, 1)
.unwrap()
.and_hms_opt(8, 0, 0)
.unwrap(),
taken_at: None,
};
let mut app = app_with_mark_dose(vec![slot]);
let mut h = MarkDoseHandler;
h.handle(&mut app, key(KeyCode::Enter));
assert!(matches!(
app.current_screen,
Screen::MedicationDetails { .. }
));
}
#[test]
fn handler_does_nothing_when_not_on_mark_dose_screen() {
let mut app = App::new(AppServices::fake());
app.current_screen = Screen::HomeScreen;
let mut h = MarkDoseHandler;
h.handle(&mut app, key(KeyCode::Enter));
assert!(matches!(app.current_screen, Screen::HomeScreen));
}
}