Skip to main content

ratatui_form/block/
date_range.rs

1//! Date range composite block.
2
3use crossterm::event::KeyEvent;
4use ratatui::buffer::Buffer;
5use ratatui::layout::Rect;
6use serde_json::Value;
7
8use crate::block::Block;
9use crate::field::{Field, TextInput};
10use crate::style::FormStyle;
11use crate::validation::ValidationError;
12use crate::validation::rules::Pattern;
13
14/// A composite block for date ranges (start date and end date).
15pub struct DateRangeBlock {
16    prefix: String,
17    title: Option<String>,
18    required: bool,
19}
20
21impl DateRangeBlock {
22    /// Creates a new date range block with the given prefix.
23    pub fn new(prefix: impl Into<String>) -> Self {
24        Self {
25            prefix: prefix.into(),
26            title: None,
27            required: false,
28        }
29    }
30
31    /// Sets the block title.
32    pub fn title(mut self, title: impl Into<String>) -> Self {
33        self.title = Some(title.into());
34        self
35    }
36
37    /// Marks all fields in this block as required.
38    pub fn required(mut self) -> Self {
39        self.required = true;
40        self
41    }
42
43    fn field_id(&self, name: &str) -> String {
44        format!("{}_{}", self.prefix, name)
45    }
46}
47
48impl Block for DateRangeBlock {
49    fn prefix(&self) -> &str {
50        &self.prefix
51    }
52
53    fn title(&self) -> Option<&str> {
54        self.title.as_deref()
55    }
56
57    fn fields(&self) -> Vec<Box<dyn Field>> {
58        let mut fields: Vec<Box<dyn Field>> = Vec::new();
59
60        // Start Date
61        let mut start_date = TextInput::new(self.field_id("start"), "Start Date")
62            .placeholder("YYYY-MM-DD")
63            .validator(Box::new(Pattern::date()));
64        if self.required {
65            start_date = start_date.required();
66        }
67        fields.push(Box::new(start_date));
68
69        // End Date
70        let mut end_date = TextInput::new(self.field_id("end"), "End Date")
71            .placeholder("YYYY-MM-DD")
72            .validator(Box::new(Pattern::date()));
73        if self.required {
74            end_date = end_date.required();
75        }
76        fields.push(Box::new(end_date));
77
78        fields
79    }
80}
81
82/// A date range field that validates end >= start.
83/// This is used internally to wrap the date range fields with cross-field validation.
84#[allow(dead_code)]
85pub struct DateRangeField {
86    start_field: TextInput,
87    end_field: TextInput,
88    prefix: String,
89    current_focus: usize, // 0 = start, 1 = end
90}
91
92#[allow(dead_code)]
93impl DateRangeField {
94    /// Creates a new date range field.
95    pub fn new(prefix: impl Into<String>, required: bool) -> Self {
96        let prefix = prefix.into();
97
98        let mut start_field = TextInput::new(format!("{}_start", prefix), "Start Date")
99            .placeholder("YYYY-MM-DD")
100            .validator(Box::new(Pattern::date()));
101        if required {
102            start_field = start_field.required();
103        }
104
105        let mut end_field = TextInput::new(format!("{}_end", prefix), "End Date")
106            .placeholder("YYYY-MM-DD")
107            .validator(Box::new(Pattern::date()));
108        if required {
109            end_field = end_field.required();
110        }
111
112        Self {
113            start_field,
114            end_field,
115            prefix,
116            current_focus: 0,
117        }
118    }
119
120    fn validate_range(&self) -> Result<(), ValidationError> {
121        let start_value = match self.start_field.value() {
122            Value::String(s) if !s.is_empty() => s,
123            _ => return Ok(()),
124        };
125
126        let end_value = match self.end_field.value() {
127            Value::String(s) if !s.is_empty() => s,
128            _ => return Ok(()),
129        };
130
131        // Simple string comparison works for YYYY-MM-DD format
132        if end_value < start_value {
133            Err(ValidationError {
134                field_id: format!("{}_end", self.prefix),
135                message: "End date must be on or after start date".to_string(),
136            })
137        } else {
138            Ok(())
139        }
140    }
141}
142
143impl Field for DateRangeField {
144    fn id(&self) -> &str {
145        &self.prefix
146    }
147
148    fn label(&self) -> &str {
149        "Date Range"
150    }
151
152    fn render(&self, area: Rect, buf: &mut Buffer, focused: bool, style: &FormStyle) {
153        if area.height < 2 {
154            return;
155        }
156
157        let start_area = Rect {
158            x: area.x,
159            y: area.y,
160            width: area.width,
161            height: 1,
162        };
163
164        let end_area = Rect {
165            x: area.x,
166            y: area.y + 1,
167            width: area.width,
168            height: 1,
169        };
170
171        self.start_field.render(start_area, buf, focused && self.current_focus == 0, style);
172        self.end_field.render(end_area, buf, focused && self.current_focus == 1, style);
173    }
174
175    fn handle_input(&mut self, event: &KeyEvent) -> bool {
176        if self.current_focus == 0 {
177            self.start_field.handle_input(event)
178        } else {
179            self.end_field.handle_input(event)
180        }
181    }
182
183    fn value(&self) -> Value {
184        serde_json::json!({
185            "start": self.start_field.value(),
186            "end": self.end_field.value()
187        })
188    }
189
190    fn validate(&self) -> Result<(), Vec<ValidationError>> {
191        let mut errors = Vec::new();
192
193        if let Err(mut e) = self.start_field.validate() {
194            errors.append(&mut e);
195        }
196
197        if let Err(mut e) = self.end_field.validate() {
198            errors.append(&mut e);
199        }
200
201        if let Err(e) = self.validate_range() {
202            errors.push(e);
203        }
204
205        if errors.is_empty() {
206            Ok(())
207        } else {
208            Err(errors)
209        }
210    }
211
212    fn height(&self) -> u16 {
213        2
214    }
215}