1use std::collections::HashMap;
5
6use ravenrs_app_i18n::{Language, Texts};
7use ravenrs_core::Insets;
8use ravenrs_layout::Align;
9use ravenrs_reactive::{Binding, State};
10use ravenrs_report::{Parameter, ParameterKind};
11use ravenrs_style::{SPACING, Semantic};
12use ravenrs_widgets::{
13 AnyView, BuildCx, Button, ButtonSize, ButtonStyle, Checkbox, Date, DatePicker, FormField, HStack, NodeId,
14 TextField, View, ViewExt,
15};
16use serde_json::{Value as Json, json};
17
18pub type ParameterValues = HashMap<String, Json>;
20
21#[derive(Clone, Debug)]
23#[non_exhaustive]
24pub struct ParameterLabels {
25 pub apply: String,
27 pub reset: String,
29}
30
31impl Default for ParameterLabels {
32 fn default() -> Self {
33 Self::from_texts(&Texts::new(Language::English))
34 }
35}
36
37impl ParameterLabels {
38 pub fn from_texts(texts: &Texts) -> Self {
40 Self { apply: texts.get("viewer.parameters.apply"), reset: texts.get("viewer.parameters.reset") }
41 }
42
43 pub fn pt_br() -> Self {
45 Self::from_texts(&Texts::new(Language::BrazilianPortuguese))
46 }
47}
48
49#[derive(Debug)]
51pub struct ParameterPanel {
52 parameters: Vec<Parameter>,
53 applied: State<ParameterValues>,
54 labels: ParameterLabels,
55 today: Date,
56}
57
58impl ParameterPanel {
59 pub fn new(parameters: Vec<Parameter>, applied: State<ParameterValues>, today: Date) -> Self {
61 Self { parameters, applied, labels: ParameterLabels::default(), today }
62 }
63
64 pub fn labels(mut self, labels: ParameterLabels) -> Self {
66 self.labels = labels;
67 self
68 }
69}
70
71fn date_text(date: Date) -> String {
73 format!("{:04}-{:02}-{:02}", date.year(), date.month(), date.day())
74}
75
76fn parse_date(value: &Json) -> Option<Date> {
78 let text = value.as_str()?;
79 let year = text.get(0..4)?.parse().ok()?;
80 let month = text.get(5..7)?.parse().ok()?;
81 let day = text.get(8..10)?.parse().ok()?;
82 Date::new(year, month, day)
83}
84
85pub(crate) fn parse_number(text: &str) -> Option<f64> {
87 let text = text.trim();
88 let plain = if text.contains(',') { text.replace('.', "").replace(',', ".") } else { text.to_owned() };
90 plain.parse().ok().filter(|n: &f64| n.is_finite())
91}
92
93fn value_text(value: Option<&Json>) -> String {
95 match value {
96 Some(Json::String(text)) => text.clone(),
97 Some(Json::Null) | None => String::new(),
98 Some(other) => other.to_string(),
99 }
100}
101
102impl View for ParameterPanel {
103 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
104 let draft: State<ParameterValues> = cx.use_state(|| self.applied.get());
106 let mut items: Vec<AnyView> = Vec::with_capacity(self.parameters.len() + 2);
107 for parameter in &self.parameters {
108 let name = parameter.name.clone();
109 let caption = parameter.label.clone().unwrap_or_else(|| name.clone());
110 let default = parameter.default.clone();
111 let current = {
112 let (draft, name, default) = (draft.clone(), name.clone(), default.clone());
113 move || draft.with(|values| values.get(&name).cloned()).or_else(|| default.clone())
114 };
115 let store = {
116 let (draft, name) = (draft.clone(), name.clone());
117 move |value: Json| {
118 draft.update(|values| {
119 values.insert(name.clone(), value);
120 });
121 }
122 };
123 let control: AnyView = match parameter.kind {
124 ParameterKind::Boolean => {
125 let read = current.clone();
126 Box::new(Checkbox::new(
127 caption.clone(),
128 Binding::new(
129 move || read().is_some_and(|v| v.as_bool().unwrap_or(false)),
130 move |checked: bool| store(json!(checked)),
131 ),
132 ))
133 }
134 ParameterKind::Date => {
135 let read = current.clone();
136 let month = current().as_ref().and_then(parse_date).unwrap_or(self.today);
137 Box::new(
138 DatePicker::new(
139 Binding::new(
140 move || read().as_ref().and_then(parse_date),
141 move |date: Option<Date>| {
142 store(date.map_or(Json::Null, |d| json!(date_text(d))));
143 },
144 ),
145 month,
146 )
147 .today(self.today)
148 .width(150.0)
149 .label(caption.clone()),
150 )
151 }
152 _ => {
153 let typed: State<Option<String>> = cx.use_state(|| None);
155 let is_number = parameter.kind == ParameterKind::Number;
156 let (read, read_typed, write_typed) = (current.clone(), typed.clone(), typed);
157 Box::new(
158 TextField::new(Binding::new(
159 move || read_typed.get().unwrap_or_else(|| value_text(read().as_ref())),
160 move |text: String| {
161 let value = if is_number {
162 parse_number(&text).map_or(Json::Null, |n| json!(n))
163 } else {
164 json!(text)
165 };
166 store(value);
167 write_typed.set(Some(text));
168 },
169 ))
170 .compact()
171 .width(if is_number { 110.0 } else { 180.0 }),
172 )
173 }
174 };
175 items.push(if parameter.kind == ParameterKind::Boolean {
176 control
177 } else {
178 Box::new(FormField::new(caption, ControlBox(control)))
179 });
180 }
181 let (write, source) = (self.applied.clone(), draft.clone());
182 items.push(Box::new(
183 Button::new(self.labels.apply.clone())
184 .size(ButtonSize::Small)
185 .on_click(move || write.set(source.get())),
186 ));
187 let (write, source) = (self.applied.clone(), draft);
188 items.push(Box::new(
189 Button::new(self.labels.reset.clone())
190 .style(ButtonStyle::Ghost)
191 .size(ButtonSize::Small)
192 .on_click(move || {
193 source.set(HashMap::new());
194 write.set(HashMap::new());
195 }),
196 ));
197 cx.child(
198 &HStack::new(items)
199 .spacing(SPACING.s3)
200 .align(Align::End)
201 .wrap()
202 .padding(Insets::symmetric(SPACING.s2, SPACING.s4))
203 .background(Semantic::Surface),
204 )
205 }
206}
207
208struct ControlBox(AnyView);
210
211impl View for ControlBox {
212 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
213 cx.child(&self.0)
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn numbers_read_with_either_separator() {
223 assert_eq!(parse_number("1,5"), Some(1.5));
224 assert_eq!(parse_number("1.5"), Some(1.5));
225 assert_eq!(parse_number("1.234,50"), Some(1234.5));
226 assert_eq!(parse_number(" 10 "), Some(10.0));
227 assert_eq!(parse_number("abc"), None);
228 assert_eq!(parse_number(""), None);
229 assert_eq!(parse_number("inf"), None);
230 }
231
232 #[test]
233 fn dates_read_with_or_without_a_time() {
234 let date = parse_date(&json!("2026-11-01T10:00:00")).map(date_text);
235 assert_eq!(date.as_deref(), Some("2026-11-01"));
236 assert_eq!(parse_date(&json!("2026-02-30")), None);
237 assert_eq!(parse_date(&json!("ontem")), None);
238 assert_eq!(parse_date(&json!(5)), None);
239 }
240}