1
//! Generic input form used by all overlay modals that collect user input.
2
//!
3
//! Replaces the per-modal `ConfigSetModal` / `ReportParamsModal` duplicates.
4
//! [`Form`] is a list of [`Field`]s with a focused index and a [`FormKind`]
5
//! that identifies the submit target. [`validate`] is a pure function that
6
//! extracts and validates the form's buffers into a [`FormSubmit`] payload.
7

            
8
mod validate;
9

            
10
use cli_core::forms::{EditableTransaction, LogicalSplitInput};
11

            
12
use crate::tabs::reports::ReportKind;
13
use crate::widgets::{
14
    AmountWidget, DateWidget, EditMode, Editor, SelectOption, SelectWidget, SplitRowPrefill,
15
    SplitsWidget, Widget, WidgetKind,
16
};
17

            
18
#[cfg(test)]
19
mod tests;
20

            
21
pub use validate::validate;
22

            
23
/// A single labelled input field.
24
#[derive(Debug)]
25
pub struct Field {
26
    pub label: &'static str,
27
    pub widget: Widget,
28
}
29

            
30
/// What to do when the form is submitted.
31
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32
pub enum FormKind {
33
    ConfigSet,
34
    ReportParams {
35
        kind: ReportKind,
36
    },
37
    CommodityCreate,
38
    CommodityConvert,
39
    AccountCreate,
40
    TransactionCreate,
41
    /// Edit an existing transaction; the transaction id is in `Form::entity_id`.
42
    TransactionEdit,
43
    /// Set (or rename via name="name") a tag on an account.
44
    AccountTag,
45
    /// Set a tag on a transaction.
46
    TransactionTag,
47
}
48

            
49
/// A generic multi-field input form.
50
#[derive(Debug)]
51
pub struct Form {
52
    pub fields: Vec<Field>,
53
    /// Index of the currently focused field. Always `< fields.len()`.
54
    pub focus: usize,
55
    pub kind: FormKind,
56
    /// Entity id for tag forms; `None` for all other kinds.
57
    pub entity_id: Option<String>,
58
}
59

            
60
/// The validated, ready-to-dispatch payload produced by [`validate`].
61
#[derive(Debug)]
62
pub enum FormSubmit {
63
    ConfigSet {
64
        key: String,
65
        value: String,
66
    },
67
    Report {
68
        kind: ReportKind,
69
        from: String,
70
        to: String,
71
        chart: String,
72
    },
73
    CommodityCreate {
74
        symbol: String,
75
        name: String,
76
    },
77
    CommodityConvert {
78
        /// Rational in `"N"` or `"N/D"` form, ready for the native.
79
        amount_num_denom: String,
80
        /// Raw typed amount string for display.
81
        amount_str: String,
82
        from: String,
83
        from_label: String,
84
        to: String,
85
        to_label: String,
86
    },
87
    AccountCreate {
88
        name: String,
89
        parent: Option<String>,
90
    },
91
    TransactionCreate {
92
        note: String,
93
        date: String,
94
        splits: Vec<LogicalSplitInput>,
95
    },
96
    TransactionEdit {
97
        id: String,
98
        note: String,
99
        date: String,
100
        splits: Vec<LogicalSplitInput>,
101
    },
102
    AccountTag {
103
        account_id: String,
104
        name: String,
105
        value: String,
106
    },
107
    TransactionTag {
108
        transaction_id: String,
109
        name: String,
110
        value: String,
111
    },
112
}
113

            
114
impl Form {
115
    /// Two-field form for the `config set` command.
116
    #[must_use]
117
15
    pub fn config_set(name: Editor, value: Editor) -> Self {
118
15
        Self {
119
15
            fields: vec![
120
15
                Field {
121
15
                    label: "name",
122
15
                    widget: Widget::Text(name),
123
15
                },
124
15
                Field {
125
15
                    label: "value",
126
15
                    widget: Widget::Text(value),
127
15
                },
128
15
            ],
129
15
            focus: 0,
130
15
            kind: FormKind::ConfigSet,
131
15
            entity_id: None,
132
15
        }
133
15
    }
134

            
135
    /// Three-field form for the `reports activity/breakdown` commands.
136
    #[must_use]
137
9
    pub fn report_params(from: Editor, to: Editor, chart: Editor, kind: ReportKind) -> Self {
138
9
        Self {
139
9
            fields: vec![
140
9
                Field {
141
9
                    label: "from",
142
9
                    widget: Widget::Text(from),
143
9
                },
144
9
                Field {
145
9
                    label: "to",
146
9
                    widget: Widget::Text(to),
147
9
                },
148
9
                Field {
149
9
                    label: "chart",
150
9
                    widget: Widget::Text(chart),
151
9
                },
152
9
            ],
153
9
            focus: 0,
154
9
            kind: FormKind::ReportParams { kind },
155
9
            entity_id: None,
156
9
        }
157
9
    }
158

            
159
    /// Two-field form for commodity create (symbol required, name required).
160
    #[must_use]
161
5
    pub fn commodity_create(mode: EditMode) -> Self {
162
5
        Self {
163
5
            fields: vec![
164
5
                Field {
165
5
                    label: "Symbol",
166
5
                    widget: Widget::Text(Editor::new(mode)),
167
5
                },
168
5
                Field {
169
5
                    label: "Name",
170
5
                    widget: Widget::Text(Editor::new(mode)),
171
5
                },
172
5
            ],
173
5
            focus: 0,
174
5
            kind: FormKind::CommodityCreate,
175
5
            entity_id: None,
176
5
        }
177
5
    }
178

            
179
    /// Three-field form for commodity conversion: amount, from-commodity, to-commodity.
180
    ///
181
    /// The two Select fields are empty on construction; call
182
    /// `fetch_commodity_convert_form_options` to populate them asynchronously.
183
    #[must_use]
184
11
    pub fn commodity_convert(mode: EditMode) -> Self {
185
11
        Self {
186
11
            fields: vec![
187
11
                Field {
188
11
                    label: "Amount",
189
11
                    widget: Widget::Amount(AmountWidget::new(mode)),
190
11
                },
191
11
                Field {
192
11
                    label: "From",
193
11
                    widget: Widget::Select(SelectWidget::new()),
194
11
                },
195
11
                Field {
196
11
                    label: "To",
197
11
                    widget: Widget::Select(SelectWidget::new()),
198
11
                },
199
11
            ],
200
11
            focus: 0,
201
11
            kind: FormKind::CommodityConvert,
202
11
            entity_id: None,
203
11
        }
204
11
    }
205

            
206
    /// Two-field form for account create (name required, parent Select optional).
207
    ///
208
    /// `parent_options` seeds the parent Select; include a leading `(none)` entry
209
    /// with empty id so "no parent" is the default.
210
    #[must_use]
211
7
    pub fn account_create(mode: EditMode, parent_options: Vec<SelectOption>) -> Self {
212
7
        let mut sw = SelectWidget::new();
213
7
        sw.set_options(parent_options);
214
7
        Self {
215
7
            fields: vec![
216
7
                Field {
217
7
                    label: "Name",
218
7
                    widget: Widget::Text(Editor::new(mode)),
219
7
                },
220
7
                Field {
221
7
                    label: "Parent",
222
7
                    widget: Widget::Select(sw),
223
7
                },
224
7
            ],
225
7
            focus: 0,
226
7
            kind: FormKind::AccountCreate,
227
7
            entity_id: None,
228
7
        }
229
7
    }
230

            
231
    /// Three-field form for transaction create: date, note, splits.
232
    #[must_use]
233
19
    pub fn transaction_create(mode: EditMode) -> Self {
234
19
        Self {
235
19
            fields: vec![
236
19
                Field {
237
19
                    label: "Date",
238
19
                    widget: Widget::Date(DateWidget::new(mode)),
239
19
                },
240
19
                Field {
241
19
                    label: "Note",
242
19
                    widget: Widget::Text(Editor::new(mode)),
243
19
                },
244
19
                Field {
245
19
                    label: "Splits",
246
19
                    widget: Widget::Splits(SplitsWidget::new(mode)),
247
19
                },
248
19
            ],
249
19
            focus: 0,
250
19
            kind: FormKind::TransactionCreate,
251
19
            entity_id: None,
252
19
        }
253
19
    }
254

            
255
    /// Three-field form for editing an existing transaction, pre-filled from `et`.
256
    ///
257
    /// Each split row's Selects start with a single-entry list holding the stored
258
    /// uuid as both id and label; after the async `fetch_transaction_form_options`
259
    /// reply arrives, `set_account_options` / `set_commodity_options` replace the
260
    /// lists while `set_options_preserving_id` keeps the stored selection.
261
    #[must_use]
262
3
    pub fn transaction_edit(mode: EditMode, et: &EditableTransaction) -> Self {
263
3
        let prefill_rows: Vec<SplitRowPrefill<'_>> = et
264
3
            .rows
265
3
            .iter()
266
3
            .map(|r| SplitRowPrefill {
267
3
                from: &r.from_account,
268
3
                to: &r.to_account,
269
3
                from_commodity: &r.from_commodity,
270
3
                to_commodity: &r.to_commodity,
271
3
                value: &r.value,
272
3
                to_amount: r.to_amount.as_deref(),
273
3
            })
274
3
            .collect();
275
3
        let mut sw = SplitsWidget::new(mode);
276
3
        sw.apply_prefill(&prefill_rows);
277
3
        Self {
278
3
            fields: vec![
279
3
                Field {
280
3
                    label: "Date",
281
3
                    widget: Widget::Date(DateWidget::with_value(mode, et.date.as_str())),
282
3
                },
283
3
                Field {
284
3
                    label: "Note",
285
3
                    widget: Widget::Text(Editor::with_buffer(mode, et.note.as_str())),
286
3
                },
287
3
                Field {
288
3
                    label: "Splits",
289
3
                    widget: Widget::Splits(sw),
290
3
                },
291
3
            ],
292
3
            focus: 0,
293
3
            kind: FormKind::TransactionEdit,
294
3
            entity_id: Some(et.id.clone()),
295
3
        }
296
3
    }
297

            
298
    /// Two-field form for `set-account-tag`: tag name (required) and value.
299
    ///
300
    /// The `account_id` is stored in `entity_id` and used at validation time.
301
    #[must_use]
302
3
    pub fn account_tag(mode: EditMode, account_id: String) -> Self {
303
3
        Self {
304
3
            fields: vec![
305
3
                Field {
306
3
                    label: "Tag name",
307
3
                    widget: Widget::Text(Editor::new(mode)),
308
3
                },
309
3
                Field {
310
3
                    label: "Value",
311
3
                    widget: Widget::Text(Editor::new(mode)),
312
3
                },
313
3
            ],
314
3
            focus: 0,
315
3
            kind: FormKind::AccountTag,
316
3
            entity_id: Some(account_id),
317
3
        }
318
3
    }
319

            
320
    /// Two-field form for `set-transaction-tag`: tag name (required) and value.
321
    ///
322
    /// The `transaction_id` is stored in `entity_id` and used at validation time.
323
    #[must_use]
324
2
    pub fn transaction_tag(mode: EditMode, transaction_id: String) -> Self {
325
2
        Self {
326
2
            fields: vec![
327
2
                Field {
328
2
                    label: "Tag name",
329
2
                    widget: Widget::Text(Editor::new(mode)),
330
2
                },
331
2
                Field {
332
2
                    label: "Value",
333
2
                    widget: Widget::Text(Editor::new(mode)),
334
2
                },
335
2
            ],
336
2
            focus: 0,
337
2
            kind: FormKind::TransactionTag,
338
2
            entity_id: Some(transaction_id),
339
2
        }
340
2
    }
341

            
342
    /// Advance or retreat focus by one step, wrapping around.
343
16
    pub fn cycle(&mut self, forward: bool) {
344
16
        if self.fields.is_empty() {
345
            return;
346
16
        }
347
16
        let len = self.fields.len();
348
16
        self.focus = if forward {
349
9
            (self.focus + 1) % len
350
        } else {
351
7
            (self.focus + len - 1) % len
352
        };
353
16
    }
354

            
355
    /// Mutable reference to the currently focused field, if any.
356
36
    pub fn focused_field_mut(&mut self) -> Option<&mut Field> {
357
36
        self.fields.get_mut(self.focus)
358
36
    }
359

            
360
    /// Mutable reference to the inner [`Editor`] of the focused field.
361
    ///
362
    /// Returns `None` when the form is empty or the focused widget is not
363
    /// `Text`, `Amount`, or `Date` (i.e. a `Select` or `Splits`).
364
6
    pub fn focused_editor_mut(&mut self) -> Option<&mut Editor> {
365
6
        self.focused_field_mut()
366
6
            .and_then(|f| f.widget.as_text_mut())
367
6
    }
368

            
369
    /// Widget kind of the focused field, or `None` when the form is empty.
370
    #[must_use]
371
21
    pub fn focused_widget_kind(&self) -> Option<WidgetKind> {
372
21
        self.fields.get(self.focus).map(|f| f.widget.kind())
373
21
    }
374

            
375
    /// When the focused field is a Splits widget, returns the kind of its
376
    /// focused sub-widget (for nested widget-first key routing).
377
    #[must_use]
378
21
    pub fn focused_splits_sub_kind(&self) -> Option<WidgetKind> {
379
21
        match self.fields.get(self.focus)?.widget {
380
14
            Widget::Splits(ref sw) => Some(sw.focused_subwidget_kind()),
381
7
            _ => None,
382
        }
383
21
    }
384

            
385
    /// When the focused field is a Splits widget, returns `(at_first_cell,
386
    /// at_last_cell)` so the key layer can yield Tab/BackTab to the outer
387
    /// field cycle at the editor's boundaries.
388
    #[must_use]
389
21
    pub fn focused_splits_boundary(&self) -> Option<(bool, bool)> {
390
21
        match self.fields.get(self.focus)?.widget {
391
14
            Widget::Splits(ref sw) => Some((sw.at_first_cell(), sw.at_last_cell())),
392
7
            _ => None,
393
        }
394
21
    }
395

            
396
    /// Buffer text of field `i`, or `""` when `i` is out of bounds.
397
76
    pub(super) fn field_buffer(&self, i: usize) -> &str {
398
76
        self.fields.get(i).map_or("", |f| f.widget.value())
399
76
    }
400
}