Skip to main content

ravenrs_report/
samples.rs

1//! Ready-made reports with sample data: the designer's templates, the
2//! benchmarks' workloads and living documentation of the report model.
3
4use serde_json::{Value as Json, json};
5
6use crate::chart::ChartKind;
7use crate::model::{
8    Band, Chart, ChartSeries, Control, ControlKind, DataSource, DataSourceKind, DetailReport, Group, Label,
9    LinkLabel, PageSettings, ParameterKind, Report, Style, Table, TableRow, TextAlign, VerticalAlign,
10};
11
12// Raven Light tokens: reports are printed on white paper.
13const ACCENT: &str = "#3f57db";
14const INK: &str = "#12151c";
15const MUTED: &str = "#5a6375";
16const LINE: &str = "#dadde5";
17const STRIPE: &str = "#f4f5f8";
18
19/// A small deterministic random generator, so samples are the same everywhere.
20struct Rng(u64);
21
22impl Rng {
23    fn next(&mut self) -> u64 {
24        self.0 ^= self.0 << 13;
25        self.0 ^= self.0 >> 7;
26        self.0 ^= self.0 << 17;
27        self.0
28    }
29
30    fn pick<'a>(&mut self, items: &[&'a str]) -> &'a str {
31        items[(self.next() % items.len() as u64) as usize]
32    }
33
34    fn range(&mut self, lo: u64, hi: u64) -> u64 {
35        lo + self.next() % (hi - lo + 1)
36    }
37}
38
39const FIRST: [&str; 16] = [
40    "Ana", "Bruno", "Carla", "Diego", "Elisa", "Felipe", "Gabriela", "Henrique", "Isabela", "João",
41    "Larissa", "Marcos", "Natália", "Otávio", "Paula", "Rafael",
42];
43const LAST: [&str; 12] = [
44    "Silva", "Souza", "Oliveira", "Lima", "Pereira", "Costa", "Almeida", "Ribeiro", "Carvalho", "Gomes",
45    "Martins", "Rocha",
46];
47const CITIES: [(&str, &str); 10] = [
48    ("São Paulo", "Sudeste"),
49    ("Rio de Janeiro", "Sudeste"),
50    ("Belo Horizonte", "Sudeste"),
51    ("Curitiba", "Sul"),
52    ("Porto Alegre", "Sul"),
53    ("Florianópolis", "Sul"),
54    ("Recife", "Nordeste"),
55    ("Salvador", "Nordeste"),
56    ("Fortaleza", "Nordeste"),
57    ("Manaus", "Norte"),
58];
59const PRODUCTS: [(&str, f64); 10] = [
60    ("Notebook Pro 14", 7_499.0),
61    ("Monitor 27\" 4K", 2_299.0),
62    ("Teclado mecânico", 489.9),
63    ("Mouse sem fio", 159.9),
64    ("Headset", 349.0),
65    ("Webcam Full HD", 279.0),
66    ("Dock USB-C", 899.0),
67    ("SSD 1 TB", 529.0),
68    ("Cadeira ergonômica", 1_890.0),
69    ("Mesa elevatória", 2_790.0),
70];
71
72/// `count` sales rows: date, region, city, customer, product, quantity, price.
73pub fn sales_data(count: usize) -> Json {
74    let mut rng = Rng(0x5eed_1234_abcd_9876);
75    let rows: Vec<Json> = (0..count)
76        .map(|i| {
77            let (city, region) = CITIES[(rng.next() % CITIES.len() as u64) as usize];
78            let (product, price) = PRODUCTS[(rng.next() % PRODUCTS.len() as u64) as usize];
79            let month = rng.range(1, 12);
80            let day = rng.range(1, 28);
81            json!({
82                "Id": 10_000 + i,
83                "Date": format!("2026-{month:02}-{day:02}"),
84                "Region": region,
85                "City": city,
86                "Customer": format!("{} {}", rng.pick(&FIRST), rng.pick(&LAST)),
87                "Product": product,
88                "Quantity": rng.range(1, 12),
89                "UnitPrice": price,
90                "Paid": !rng.next().is_multiple_of(5),
91            })
92        })
93        .collect();
94    Json::Array(rows)
95}
96
97/// An invoice with its items, as a single master row.
98pub fn invoice_data() -> Json {
99    json!({
100        "Number": "NF-2026-00417",
101        "IssueDate": "2026-11-01",
102        "DueDate": "2026-11-16",
103        "Seller": { "Name": "Raven Tecnologia Ltda.", "Document": "12.345.678/0001-90", "City": "Recife - PE", "Email": "financeiro@raven.dev" },
104        "Customer": { "Name": "Kestrel Saúde S.A.", "Document": "98.765.432/0001-10", "Address": "Av. Boa Viagem, 1200 - Recife - PE", "Email": "compras@kestrel.com.br" },
105        "Discount": 350.0,
106        "Notes": "Pagamento via PIX ou boleto. Após o vencimento, multa de 2% e juros de 1% ao mês.",
107        "Items": [
108            { "Sku": "NB-PRO14", "Description": "Notebook Pro 14, 32 GB, 1 TB", "Quantity": 4, "UnitPrice": 7499.0 },
109            { "Sku": "MN-27K", "Description": "Monitor 27\" 4K com ajuste de altura", "Quantity": 4, "UnitPrice": 2299.0 },
110            { "Sku": "DK-USBC", "Description": "Dock USB-C com 2 saídas de vídeo e carregamento de 100 W", "Quantity": 4, "UnitPrice": 899.0 },
111            { "Sku": "KB-MEC", "Description": "Teclado mecânico ABNT2", "Quantity": 4, "UnitPrice": 489.9 },
112            { "Sku": "SV-INST", "Description": "Instalação e configuração no local", "Quantity": 1, "UnitPrice": 1200.0 }
113        ]
114    })
115}
116
117fn title() -> Style {
118    Style::new().font_size(20.0).bold().color(INK)
119}
120
121fn caption() -> Style {
122    Style::new().font_size(7.5).bold().color(MUTED)
123}
124
125fn header_cell() -> Style {
126    Style::new().font_size(8.0).bold().color("#ffffff").background(ACCENT).padding(4.0)
127}
128
129fn cell() -> Style {
130    Style::new().font_size(9.0).color(INK).padding(4.0)
131}
132
133fn money(style: Style) -> Style {
134    style.align(TextAlign::Right)
135}
136
137fn page_footer(culture_pt: bool) -> Band {
138    let page = if culture_pt { "Página {0} de {1}" } else { "Page {0} of {1}" };
139    Band::new(22.0)
140        .control(Control::line().at(0.0, 0.0, 510.0, 2.0).style(Style::new().color(LINE)))
141        .control(
142            Control::field("FormatString('{0:dd/MM/yyyy HH:mm}', Now())")
143                .at(0.0, 4.0, 200.0, 16.0)
144                .style(Style::new().font_size(7.5).color(MUTED)),
145        )
146        .control(
147            Control::field(format!("FormatString('{page}', PageNumber(), PageCount())"))
148                .at(310.0, 4.0, 200.0, 16.0)
149                .style(Style::new().font_size(7.5).color(MUTED).align(TextAlign::Right)),
150        )
151}
152
153/// Sales grouped by region and city, with subtotals, a grand total, zebra
154/// rows and a highlighting rule. `rows` sample rows are embedded.
155pub fn sales_by_region(rows: usize) -> Report {
156    let w = PageSettings::a4().content_width();
157    let cols = [70.0, 150.0, 140.0, 40.0, 50.0, w - 450.0];
158    let header = TableRow::new(20.0)
159        .text(cols[0], "DATA")
160        .text(cols[1], "CLIENTE")
161        .text(cols[2], "PRODUTO")
162        .text(cols[3], "QTD")
163        .cell_style(Style::new().align(TextAlign::Right))
164        .text(cols[4], "PAGO")
165        .cell_style(Style::new().align(TextAlign::Center))
166        .text(cols[5], "TOTAL")
167        .cell_style(Style::new().align(TextAlign::Right))
168        .style_name("th");
169    let detail = TableRow::new(16.0)
170        .field(cols[0], "[Date]", Some("dd/MM/yyyy"))
171        .field(cols[1], "[Customer]", None)
172        .field(cols[2], "[Product]", None)
173        .field(cols[3], "[Quantity]", None)
174        .cell_style(Style::new().align(TextAlign::Right))
175        .field(cols[4], "Iif([Paid], 'Sim', 'Não')", None)
176        .cell_style(Style::new().align(TextAlign::Center))
177        .field(cols[5], "[Total]", Some("C2"))
178        .cell_style(Style::new().align(TextAlign::Right))
179        .style_name("td");
180    Report::new("Vendas por região")
181        .culture("pt-BR")
182        .data_source(DataSource {
183            name: "Vendas".into(),
184            kind: DataSourceKind::Json { data: sales_data(rows) },
185        })
186        .calculated("Total", "[Quantity] * [UnitPrice]")
187        .parameter("MinTotal", ParameterKind::Number, json!(0))
188        .parameter_label("Valor mínimo")
189        .filter("[Total] >= ?MinTotal")
190        .sort_by("[Date]", false)
191        .style("th", header_cell())
192        .style("td", cell())
193        .style("odd", Style::new().background("#ffffff"))
194        .style("even", Style::new().background(STRIPE))
195        .report_header(
196            Band::new(64.0)
197                .control(Control::label("Vendas por região").at(0.0, 0.0, 360.0, 30.0).style(title()))
198                .control(
199                    Control::field("FormatString('{0:N0} pedidos · {1:C2}', Count(), Sum([Total]))")
200                        .at(0.0, 30.0, 360.0, 18.0)
201                        .style(Style::new().font_size(10.0).color(MUTED)),
202                )
203                .control(
204                    Control::label("RAVEN TECNOLOGIA")
205                        .at(w - 160.0, 4.0, 160.0, 14.0)
206                        .style(caption().align(TextAlign::Right).color(ACCENT)),
207                ),
208        )
209        .page_footer(page_footer(true))
210        .group(
211            Group::new("[Region]")
212                .header(
213                    Band::new(30.0)
214                        .control(
215                            Control::rect(3.0)
216                                .at(0.0, 10.0, 4.0, 16.0)
217                                .style(Style::new().background(ACCENT)),
218                        )
219                        .control(Control::bookmark("[Region]", 0).at(0.0, 8.0, 80.0, 12.0))
220                        .control(
221                            Control::field("'Região ' + [Region]")
222                                .at(10.0, 8.0, 300.0, 20.0)
223                                .style(Style::new().font_size(13.0).bold().color(INK)),
224                        )
225                        .keep_with_next(),
226                )
227                .footer(
228                    Band::new(26.0)
229                        .control(
230                            Control::field("'Total ' + [Region]")
231                                .at(0.0, 4.0, 300.0, 18.0)
232                                .style(Style::new().bold().color(INK)),
233                        )
234                        .control(
235                            Control::field("Sum([Total])")
236                                .format("C2")
237                                .at(w - 160.0, 4.0, 160.0, 18.0)
238                                .style(
239                                    Style::new()
240                                        .bold()
241                                        .color(INK)
242                                        .align(TextAlign::Right)
243                                        .border("top", 1.0, INK),
244                                ),
245                        ),
246                )
247                .repeat_header(),
248        )
249        .group(
250            Group::new("[City]")
251                .header(
252                    Band::new(44.0)
253                        .control(
254                            Control::field("[City]")
255                                .at(0.0, 2.0, 300.0, 18.0)
256                                .style(Style::new().bold().color(MUTED)),
257                        )
258                        .control(Control::table(Table::new().row(header)).at(0.0, 22.0, w, 20.0))
259                        .keep_with_next(),
260                )
261                .footer(
262                    Band::new(24.0).control(
263                        Control::field(
264                            "FormatString('{0} pedidos em {1}: {2:C2}', Count(), [City], Sum([Total]))",
265                        )
266                        .at(0.0, 2.0, w, 18.0)
267                        .style(Style::new().font_size(8.5).color(MUTED).align(TextAlign::Right)),
268                    ),
269                )
270                .repeat_header(),
271        )
272        .detail(
273            Band::new(16.0)
274                .control(
275                    Control::table(Table::new().row(detail))
276                        .at(0.0, 0.0, w, 16.0)
277                        .rule("[Total] >= 20000", Style::new().color("#c93434").bold()),
278                )
279                .zebra("odd", "even"),
280        )
281        .report_footer(
282            Band::new(40.0).control(
283                Control::field("FormatString('Total geral: {0:C2}', Sum([Total]))")
284                    .at(w - 260.0, 12.0, 260.0, 22.0)
285                    .style(
286                        Style::new()
287                            .font_size(12.0)
288                            .bold()
289                            .color("#ffffff")
290                            .background(INK)
291                            .padding(5.0)
292                            .align(TextAlign::Right),
293                    ),
294            ),
295        )
296}
297
298/// An invoice: seller and customer blocks, an item table from a nested
299/// collection (master-detail), totals, notes.
300pub fn invoice() -> Report {
301    let w = PageSettings::a4().content_width();
302    let cols = [70.0, w - 300.0, 50.0, 90.0, 90.0];
303    let party = |label: &str, field: &str, x: f32| {
304        Control::table(
305            Table::new()
306                .row(TableRow::new(14.0).text(190.0, label).style_name("caption"))
307                .row(
308                    TableRow::new(18.0)
309                        .field(190.0, &format!("[{field}.Name]"), None)
310                        .cell_style(Style::new().bold().font_size(11.0).color(INK).padding(2.0)),
311                )
312                .row(
313                    TableRow::new(14.0)
314                        .field(190.0, &format!("'CNPJ ' + [{field}.Document]"), None)
315                        .style_name("small"),
316                )
317                .row(
318                    TableRow::new(14.0)
319                        .field(190.0, &format!("IsNull([{field}.Address], [{field}.City])"), None)
320                        .style_name("small"),
321                )
322                .row(TableRow::new(14.0).field(190.0, &format!("[{field}.Email]"), None).style_name("small")),
323        )
324        .at(x, 70.0, 190.0, 74.0)
325    };
326    let total_line = |label: &str, expr: &str, y: f32, strong: bool| {
327        let style = if strong {
328            Style::new().font_size(12.0).bold().color("#ffffff").background(ACCENT).padding(5.0)
329        } else {
330            Style::new().color(INK).padding(3.0)
331        };
332        [
333            Control::label(label)
334                .at(w - 260.0, y, 130.0, if strong { 26.0 } else { 18.0 })
335                .style(style.clone()),
336            Control::field(expr)
337                .format("C2")
338                .at(w - 130.0, y, 130.0, if strong { 26.0 } else { 18.0 })
339                .style(money(style)),
340        ]
341    };
342    let [s1, s2] = total_line("Subtotal", "[Items][].Sum([Quantity] * [UnitPrice])", 8.0, false);
343    let [d1, d2] = total_line("Desconto", "-[Discount]", 26.0, false);
344    let [t1, t2] =
345        total_line("Total a pagar", "[Items][].Sum([Quantity] * [UnitPrice]) - [Discount]", 48.0, true);
346    Report::new("Fatura")
347        .culture("pt-BR")
348        .data_source(DataSource {
349            name: "Fatura".into(),
350            kind: DataSourceKind::Json { data: invoice_data() },
351        })
352        .style("caption", caption())
353        .style("small", Style::new().font_size(8.5).color(MUTED).padding(2.0))
354        .style("th", header_cell())
355        .style("td", cell().border("bottom", 0.5, LINE))
356        .report_header(
357            Band::new(160.0)
358                .control(Control::rect(6.0).at(0.0, 0.0, 34.0, 34.0).style(Style::new().background(ACCENT)))
359                .control(
360                    Control::label("R").at(0.0, 0.0, 34.0, 34.0).style(
361                        Style::new()
362                            .font_size(18.0)
363                            .bold()
364                            .color("#ffffff")
365                            .align(TextAlign::Center)
366                            .valign(VerticalAlign::Middle),
367                    ),
368                )
369                .control(
370                    Control::field("[Seller.Name]")
371                        .at(44.0, 0.0, 260.0, 18.0)
372                        .style(Style::new().bold().font_size(11.0).color(INK)),
373                )
374                .control(
375                    Control {
376                        kind: ControlKind::Link(LinkLabel {
377                            label: Label {
378                                expression: Some("[Seller.Email]".to_owned()),
379                                ..Label::default()
380                            },
381                            url_expression: Some("'mailto:' + [Seller.Email]".to_owned()),
382                            ..LinkLabel::default()
383                        }),
384                        ..Control::default()
385                    }
386                    .at(44.0, 18.0, 260.0, 14.0)
387                    .style(Style::new().font_size(8.5).color(MUTED)),
388                )
389                .control(
390                    Control::label("FATURA")
391                        .at(w - 200.0, 0.0, 200.0, 26.0)
392                        .style(title().align(TextAlign::Right)),
393                )
394                .control(
395                    Control::field("[Number]")
396                        .at(w - 200.0, 26.0, 200.0, 14.0)
397                        .style(Style::new().color(MUTED).align(TextAlign::Right)),
398                )
399                .control(Control::line().at(0.0, 52.0, w, 2.0).style(Style::new().color(LINE)))
400                .control(party("EMITENTE", "Seller", 0.0))
401                .control(party("CLIENTE", "Customer", 200.0))
402                .control(
403                    Control::table(
404                        Table::new()
405                            .row(TableRow::new(14.0).text(110.0, "EMISSÃO").style_name("caption"))
406                            .row(
407                                TableRow::new(18.0)
408                                    .field(110.0, "[IssueDate]", Some("dd/MM/yyyy"))
409                                    .style_name("td"),
410                            )
411                            .row(TableRow::new(14.0).text(110.0, "VENCIMENTO").style_name("caption"))
412                            .row(
413                                TableRow::new(18.0)
414                                    .field(110.0, "[DueDate]", Some("dd/MM/yyyy"))
415                                    .cell_style(cell().bold().color(ACCENT)),
416                            ),
417                    )
418                    .at(w - 110.0, 70.0, 110.0, 64.0),
419                ),
420        )
421        .detail_report(
422            DetailReport::new("Items")
423                .header(
424                    Band::new(22.0).control(
425                        Control::table(
426                            Table::new().row(
427                                TableRow::new(22.0)
428                                    .text(cols[0], "CÓDIGO")
429                                    .text(cols[1], "DESCRIÇÃO")
430                                    .text(cols[2], "QTD")
431                                    .cell_style(Style::new().align(TextAlign::Right))
432                                    .text(cols[3], "PREÇO UNIT.")
433                                    .cell_style(Style::new().align(TextAlign::Right))
434                                    .text(cols[4], "TOTAL")
435                                    .cell_style(Style::new().align(TextAlign::Right))
436                                    .style_name("th"),
437                            ),
438                        )
439                        .at(0.0, 0.0, w, 22.0),
440                    ),
441                )
442                .detail(
443                    Band::new(20.0).control(
444                        Control::table(
445                            Table::new().row(
446                                TableRow::new(20.0)
447                                    .field(cols[0], "[Sku]", None)
448                                    .field(cols[1], "[Description]", None)
449                                    .field(cols[2], "[Quantity]", None)
450                                    .cell_style(Style::new().align(TextAlign::Right))
451                                    .field(cols[3], "[UnitPrice]", Some("C2"))
452                                    .cell_style(Style::new().align(TextAlign::Right))
453                                    .field(cols[4], "[Quantity] * [UnitPrice]", Some("C2"))
454                                    .cell_style(Style::new().align(TextAlign::Right))
455                                    .style_name("td"),
456                            ),
457                        )
458                        .at(0.0, 0.0, w, 20.0),
459                    ),
460                ),
461        )
462        .report_footer(
463            Band::new(150.0)
464                .control(s1)
465                .control(s2)
466                .control(d1)
467                .control(d2)
468                .control(t1)
469                .control(t2)
470                .control(Control::label("OBSERVAÇÕES").at(0.0, 90.0, 300.0, 14.0).style(caption()))
471                .control(
472                    Control::field("[Notes]")
473                        .at(0.0, 104.0, w, 30.0)
474                        .style(Style::new().font_size(8.5).color(MUTED).valign(VerticalAlign::Top)),
475                ),
476        )
477        .page_footer(page_footer(true))
478}
479
480/// A sales dashboard: one page of charts over the same orders.
481pub fn sales_dashboard() -> Report {
482    let w = PageSettings::a4().content_width();
483    let half = (w - 16.0) / 2.0;
484    let total = "[Quantity] * [UnitPrice]";
485    let monthly = Chart {
486        kind: ChartKind::Area,
487        title: "Vendas por mês".to_owned(),
488        argument: Some("GetMonth([Date])".to_owned()),
489        series: vec![ChartSeries::new("Total", total)],
490        sorted: true,
491        format: Some("N0".to_owned()),
492        ..Chart::default()
493    };
494    let chart = |chart: Chart, x: f32, y: f32, width: f32, height: f32| {
495        Control { kind: ControlKind::Chart(chart), ..Control::default() }
496            .at(x, y, width, height)
497            .style(Style::new().font_size(9.0).color(INK))
498    };
499    Report::new("Painel de vendas")
500        .culture("pt-BR")
501        .data_source(DataSource {
502            name: "Vendas".into(),
503            kind: DataSourceKind::Json { data: sales_data(300) },
504        })
505        .report_header(
506            Band::new(620.0)
507                .control(
508                    Control::label("Painel de vendas")
509                        .at(0.0, 0.0, w, 30.0)
510                        .style(Style::new().font_size(20.0).bold().color(INK)),
511                )
512                .control(
513                    Control::field(
514                        "FormatString('{0} pedidos · {1:C2}', Count(), Sum([Quantity] * [UnitPrice]))",
515                    )
516                    .at(0.0, 30.0, w, 16.0)
517                    .style(Style::new().font_size(10.0).color(MUTED)),
518                )
519                .control(chart(monthly, 0.0, 60.0, w, 200.0))
520                .control(chart(
521                    Chart {
522                        kind: ChartKind::Bar,
523                        title: "Por região".to_owned(),
524                        argument: Some("[Region]".to_owned()),
525                        series: vec![ChartSeries::new("Total", total)],
526                        sorted: true,
527                        ..Chart::default()
528                    },
529                    0.0,
530                    276.0,
531                    half,
532                    230.0,
533                ))
534                .control(chart(
535                    Chart {
536                        kind: ChartKind::Doughnut,
537                        title: "Por produto".to_owned(),
538                        argument: Some("[Product]".to_owned()),
539                        series: vec![ChartSeries::new("Total", total)],
540                        ..Chart::default()
541                    },
542                    half + 16.0,
543                    276.0,
544                    half,
545                    230.0,
546                ))
547                .control(Control::bookmark("'Painel'", 0).at(0.0, 0.0, 80.0, 12.0)),
548        )
549}
550
551/// A customer list grouped by the first letter, the simplest kind of report.
552pub fn customer_list() -> Report {
553    let w = PageSettings::a4().content_width();
554    Report::new("Clientes")
555        .culture("pt-BR")
556        .data_source(DataSource {
557            name: "Vendas".into(),
558            kind: DataSourceKind::Json { data: sales_data(60) },
559        })
560        .sort_by("[Customer]", false)
561        .report_header(
562            Band::new(40.0).control(Control::label("Clientes").at(0.0, 0.0, 300.0, 30.0).style(title())),
563        )
564        .group(
565            Group::new("Substring([Customer], 0, 1)").header(
566                Band::new(26.0)
567                    .control(
568                        Control::field("Substring([Customer], 0, 1)")
569                            .at(0.0, 4.0, 30.0, 20.0)
570                            .style(Style::new().font_size(14.0).bold().color(ACCENT)),
571                    )
572                    .keep_with_next(),
573            ),
574        )
575        .detail(
576            Band::new(18.0)
577                .control(Control::field("[Customer]").at(20.0, 0.0, 200.0, 18.0).style(cell()))
578                .control(
579                    Control::field("[City] + ' · ' + [Region]")
580                        .at(220.0, 0.0, 200.0, 18.0)
581                        .style(cell().color(MUTED)),
582                )
583                .control(
584                    Control::check_box("[Paid]", "Em dia")
585                        .at(w - 80.0, 0.0, 80.0, 18.0)
586                        .style(Style::new().font_size(8.5).color(MUTED)),
587                ),
588        )
589        .page_footer(page_footer(true))
590}