bitpill 0.3.5

A personal medication management TUI application built in Rust.
Documentation
use crate::application::dtos::responses::MedicationDto;
use crate::presentation::tui::styles::content_style;
use chrono::NaiveDateTime;
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;

pub fn medication_detail<'a>(m: &'a MedicationDto, taken_at: &[NaiveDateTime]) -> Paragraph<'a> {
    let mut lines = Vec::new();
    lines.push(Line::from(Span::styled(
        format!("Name: {}", m.name),
        content_style(),
    )));
    lines.push(Line::from(Span::styled(
        format!("Dose: {}mg", m.amount_mg),
        content_style(),
    )));
    let times = if m.scheduled_time.is_empty() {
        "(no schedule)".to_string()
    } else {
        m.scheduled_time
            .iter()
            .map(|(h, m)| format!("{:02}:{:02}", h, m))
            .collect::<Vec<_>>()
            .join(", ")
    };
    lines.push(Line::from(Span::styled(
        format!("Scheduled: {}", times),
        content_style(),
    )));
    lines.push(Line::from(Span::styled(
        format!("Taken: {} time(s)", taken_at.len()),
        content_style(),
    )));
    for ts in taken_at {
        lines.push(Line::from(Span::styled(
            format!("{}", ts.format("%Y-%m-%d %H:%M")),
            content_style(),
        )));
    }
    Paragraph::new(lines).style(content_style())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::application::dtos::responses::MedicationDto;
    use chrono::NaiveDate;
    use ratatui::Terminal;
    use ratatui::backend::TestBackend;
    use ratatui::layout::Rect;

    fn med(with_schedule: bool) -> MedicationDto {
        MedicationDto {
            id: "m1".to_string(),
            name: "Aspirin".to_string(),
            amount_mg: 100,
            dose_frequency: "OnceDaily".to_string(),
            scheduled_time: if with_schedule { vec![(8, 0)] } else { vec![] },
            taken_today: 0,
            scheduled_today: 0,
        }
    }

    fn ts() -> chrono::NaiveDateTime {
        NaiveDate::from_ymd_opt(2025, 1, 1)
            .unwrap()
            .and_hms_opt(8, 0, 0)
            .unwrap()
    }

    #[test]
    fn medication_detail_with_no_taken_renders() {
        let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
        let m = med(true);
        terminal
            .draw(|f| {
                let widget = medication_detail(&m, &[]);
                f.render_widget(widget, Rect::new(0, 0, 80, 24));
            })
            .unwrap();
        let content: String = terminal
            .backend()
            .buffer()
            .content
            .iter()
            .map(|c| c.symbol())
            .collect();
        assert!(content.contains("Aspirin"));
    }

    #[test]
    fn medication_detail_with_taken_renders_timestamp() {
        let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
        let m = med(false);
        let taken = [ts()];
        terminal
            .draw(|f| {
                let widget = medication_detail(&m, &taken);
                f.render_widget(widget, Rect::new(0, 0, 80, 24));
            })
            .unwrap();
        let content: String = terminal
            .backend()
            .buffer()
            .content
            .iter()
            .map(|c| c.symbol())
            .collect();
        assert!(content.contains("Taken: 1 time(s)") || content.contains("2025"));
    }

    #[test]
    fn medication_detail_no_schedule_shows_no_schedule() {
        let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
        let m = med(false);
        terminal
            .draw(|f| {
                let widget = medication_detail(&m, &[]);
                f.render_widget(widget, Rect::new(0, 0, 80, 24));
            })
            .unwrap();
        let content: String = terminal
            .backend()
            .buffer()
            .content
            .iter()
            .map(|c| c.symbol())
            .collect();
        assert!(content.contains("no schedule"));
    }
}