1use ravenrs_report::wizard::{ColumnWords, FieldInfo, FieldKind, WizardOptions, table_report_with};
18use ravenrs_report::{DataSource, DataSourceKind, Document, ReportEngine};
19use ravenrs_report_export::{ExportFormat, ExportedFile};
20use ravenrs_widgets::{GridExportFormat, GridTable, GridTableKind, Value};
21use serde_json::{Map, Value as Json};
22
23fn field(column: usize) -> String {
25 format!("c{column}")
26}
27
28fn json_of(value: &Value) -> Json {
29 match value {
30 Value::Text(text) => Json::String(text.to_string()),
31 Value::Number(number) => serde_json::Number::from_f64(*number).map_or(Json::Null, Json::Number),
32 Value::Bool(on) => Json::Bool(*on),
33 Value::Date(date) => {
34 Json::String(format!("{:04}-{:02}-{:02}", date.year(), date.month(), date.day()))
35 }
36 _ => Json::Null,
37 }
38}
39
40fn number_pattern(decimals: usize, prefix: &str, suffix: &str) -> String {
42 let digits = if decimals == 0 { "#,##0".to_owned() } else { format!("#,##0.{}", "0".repeat(decimals)) };
43 format!("{prefix}{digits}{suffix}")
44}
45
46pub fn document_of(table: &GridTable) -> Document {
48 let rows: Vec<Json> = table
49 .rows
50 .iter()
51 .map(|row| {
52 let cells = row.iter().enumerate().map(|(c, value)| (field(c), json_of(value)));
53 Json::Object(cells.collect::<Map<_, _>>())
54 })
55 .collect();
56 let mut columns = Vec::with_capacity(table.columns.len());
57 let mut words = Vec::with_capacity(table.columns.len());
58 let mut totals = Vec::new();
59 for (c, column) in table.columns.iter().enumerate() {
60 let (kind, pattern) = match &column.kind {
61 GridTableKind::Number { decimals, prefix, suffix } => {
62 (FieldKind::Number, Some(number_pattern(*decimals, prefix, suffix)))
63 }
64 GridTableKind::Date => (FieldKind::Date, None),
65 GridTableKind::Boolean => (FieldKind::Boolean, None),
66 _ => (FieldKind::Text, None),
67 };
68 columns.push(FieldInfo { path: field(c), kind });
69 words.push(ColumnWords { caption: Some(column.title.clone()), format: pattern });
70 if column.total {
71 totals.push(field(c));
72 }
73 }
74 let options = WizardOptions {
75 title: table.title.clone(),
76 culture: table.culture.clone(),
77 columns,
78 group_by: table.group_by.map(field),
79 totals,
80 ..WizardOptions::default()
81 };
82 let data = Json::Array(rows);
83 let source = DataSource { name: "rows".into(), kind: DataSourceKind::Json { data: data.clone() } };
84 let report = table_report_with(source, &options, &words);
85 ReportEngine::new().render(&report, &data)
86}
87
88pub fn export(
90 table: &GridTable,
91 format: GridExportFormat,
92 suffix: Option<&str>,
93) -> Result<ExportedFile, String> {
94 let document = document_of(table);
95 let format = match format {
96 GridExportFormat::Xlsx => ExportFormat::Xlsx,
97 GridExportFormat::Pdf => ExportFormat::Pdf,
98 _ => ExportFormat::Csv,
99 };
100 ravenrs_report_export::export(&document, format, suffix)
101}
102
103#[cfg(test)]
104#[allow(clippy::expect_used)]
105mod tests {
106 use ravenrs_widgets::grid::{GridQuery, to_table};
107 use ravenrs_widgets::{Date, GridColumn, Summary};
108
109 use super::*;
110
111 fn sample() -> GridTable {
112 let columns = [
113 GridColumn::text("Conta. Principal"),
114 GridColumn::currency("Valor", "R$ ", 2).summary(Summary::Sum),
115 GridColumn::date("Data"),
116 GridColumn::text("Oculta").hidden(),
117 ];
118 let date = Date::new(2026, 11, 2).expect("a date");
119 let rows = vec![
120 vec!["Ana".into(), 1234.5.into(), Value::Date(date), "x".into()],
121 vec!["Bia".into(), 10.0.into(), Value::Date(date), "y".into()],
122 ];
123 let query = GridQuery {
124 hidden: columns.iter().map(|c| c.title() == "Oculta").collect(),
125 ..GridQuery::default()
126 };
127 to_table("Contas", "pt-BR", &columns, &rows, &query, &[1, 0])
128 }
129
130 #[test]
131 fn the_table_is_what_the_grid_shows() {
132 let table = sample();
133 assert_eq!(table.columns.len(), 3, "the hidden column stays out");
134 assert_eq!(table.rows[0][0], Value::from("Bia"), "in the grid's order");
135 assert!(table.columns[1].total);
136 assert_eq!(table.columns[2].kind, GridTableKind::Date);
137 }
138
139 #[test]
140 fn numbers_keep_their_text_around_them() {
141 assert_eq!(number_pattern(2, "R$ ", ""), "R$ #,##0.00");
142 assert_eq!(number_pattern(0, "", " un"), "#,##0 un");
143 }
144
145 #[test]
146 fn every_format_makes_a_file() {
147 let table = sample();
148 let pdf = export(&table, GridExportFormat::Pdf, None).expect("a pdf");
149 assert_eq!(pdf.name, "Contas.pdf");
150 assert!(pdf.bytes.starts_with(b"%PDF"));
151 let xlsx = export(&table, GridExportFormat::Xlsx, None).expect("a workbook");
152 assert!(xlsx.bytes.starts_with(b"PK"), "a workbook is a zip");
153 let csv = export(&table, GridExportFormat::Csv, None).expect("a csv");
154 let text = String::from_utf8(csv.bytes).expect("text");
155 assert!(text.contains("CONTA. PRINCIPAL"), "a title with a dot is a caption: {text}");
156 assert!(text.contains("Bia"));
157 assert!(text.contains("R$ 1.234,50"), "the column's format, in pt-BR: {text}");
158 }
159}