1use std::cell::RefCell;
5use std::collections::BTreeMap;
6use std::rc::Rc;
7
8use ravenrs_app::{
9 Action, ActionCategory, EntityInfo, FieldInfo, FieldKind, Message, MessageKind, Module, Object,
10 ObjectSpace, Operation, Query, Selection, TargetScope, Value, ViewController, ViewKind,
11};
12use ravenrs_app_i18n::{Language, Texts};
13use ravenrs_app_ui::Session;
14use ravenrs_reactive::State;
15use ravenrs_report::wizard::{self, ColumnWords, WizardOptions};
16use ravenrs_report::{DataSource, DataSourceKind, Document, RenderOptions, Report, ReportEngine};
17use ravenrs_report_designer::Designer;
18use ravenrs_report_export::ExportFormat;
19use ravenrs_report_viewer::{DocumentViewer, ParameterPanel, ParameterValues, ViewerState};
20use ravenrs_widgets::{
21 Alert, AnyView, BuildCx, Button, ButtonSize, ButtonStyle, Date, NodeId, Tone, VStack, View, ViewExt,
22};
23
24pub const REPORT_DEFINITION: &str = "ReportDefinition";
26const SHOW_IN_REPORT: &str = "ShowInReport";
27const SHOW: &str = "ShowReport";
28const DESIGN: &str = "DesignReport";
29const SAMPLE_ROWS: usize = 200;
31const VIEWER_HEIGHT: f32 = 720.0;
32
33pub const PT_BR: &str = ravenrs_app_i18n::model::REPORTS_PT_BR;
35
36#[derive(Clone, Debug)]
38#[non_exhaustive]
39pub struct ReportWords {
40 pub show: String,
42 pub design: String,
44 pub show_in_report: String,
46 pub save: String,
48 pub close: String,
50 pub saved: String,
52 pub none_for_list: String,
54 pub unknown_entity: String,
56 pub broken_layout: String,
58 pub pick_report: String,
60 pub open_panel: String,
62}
63
64impl Default for ReportWords {
65 fn default() -> Self {
66 Self::from_texts(&Texts::new(Language::English))
67 }
68}
69
70impl ReportWords {
71 pub fn of(language: Language) -> Self {
73 Self::from_texts(&Texts::new(language))
74 }
75
76 pub fn from_texts(texts: &Texts) -> Self {
78 Self {
79 show: texts.get("report.show"),
80 design: texts.get("report.design"),
81 show_in_report: texts.get("report.show_in_report"),
82 save: texts.get("report.save"),
83 close: texts.get("report.close"),
84 saved: texts.get("report.saved"),
85 none_for_list: texts.get("report.none_for_list"),
86 unknown_entity: texts.get("report.unknown_entity"),
87 broken_layout: texts.get("report.broken_layout"),
88 pick_report: texts.get("report.pick_report"),
89 open_panel: texts.get("report.open_panel"),
90 }
91 }
92
93 pub fn pt_br() -> Self {
95 Self::from_texts(&Texts::new(Language::BrazilianPortuguese))
96 }
97}
98
99pub fn report_entity() -> EntityInfo {
102 EntityInfo::new(REPORT_DEFINITION)
103 .caption("Reports")
104 .navigation("Reports")
105 .icon("file-text")
106 .field(FieldInfo::new("id", FieldKind::Guid).key())
107 .field(FieldInfo::new("name", FieldKind::Text).default_property())
108 .field(FieldInfo::new("target_entity", FieldKind::Text).caption("Entity"))
109 .field(FieldInfo::new("description", FieldKind::LongText))
110 .field(FieldInfo::new("layout", FieldKind::LongText).in_list(false).in_detail(false))
111 .rule(ravenrs_app::Rule::required("name"))
112 .rule(ravenrs_app::Rule::required("target_entity"))
113}
114
115fn json_of(
117 field: &FieldInfo,
118 value: &Value,
119 names: &BTreeMap<String, BTreeMap<String, String>>,
120) -> serde_json::Value {
121 use serde_json::{Value as Json, json};
122 match (&field.kind, value) {
123 (_, Value::Null) => Json::Null,
124 (FieldKind::Reference(target), value) => {
125 let key = ravenrs_app::key_text(value).unwrap_or_default();
126 let name = names.get(target).and_then(|all| all.get(&key)).cloned();
127 json!(name.unwrap_or(key))
128 }
129 (FieldKind::Money, Value::Number(cents)) => json!(cents / 100.0),
130 (FieldKind::Enum(options), Value::Text(code)) => {
131 json!(options.iter().find(|o| o.code == *code).map_or(code.as_str(), |o| o.caption.as_str()))
132 }
133 (FieldKind::Image | FieldKind::File, _) => Json::Null,
134 (FieldKind::Date, Value::Date(when)) => {
135 let date = Date::from_days(when.days);
136 json!(format!("{:04}-{:02}-{:02}", date.year(), date.month(), date.day()))
137 }
138 (_, Value::Date(when)) => {
139 let date = Date::from_days(when.days);
140 let (hour, minute) = (when.seconds / 3600, when.seconds % 3600 / 60);
141 json!(format!(
142 "{:04}-{:02}-{:02}T{hour:02}:{minute:02}:00",
143 date.year(),
144 date.month(),
145 date.day()
146 ))
147 }
148 (_, Value::Number(number)) => json!(number),
149 (_, Value::Bool(on)) => json!(on),
150 (_, Value::Text(text)) => json!(text),
151 _ => Json::Null,
152 }
153}
154
155fn plain_fields(entity: &EntityInfo) -> impl Iterator<Item = &FieldInfo> {
156 entity.fields.iter().filter(|field| {
157 !field.secret && !matches!(field.kind, FieldKind::Collection(_) | FieldKind::Image | FieldKind::File)
158 })
159}
160
161fn names_for(session: &Session, entity: &EntityInfo) -> BTreeMap<String, BTreeMap<String, String>> {
163 let mut all = BTreeMap::new();
164 for field in &entity.fields {
165 let FieldKind::Reference(target) = &field.kind else { continue };
166 let Some(other) = session.entity(target) else { continue };
167 let (Some(key), Some(name)) = (other.key_field(), other.default_property()) else { continue };
168 let found = session.frame().space().borrow().query(target, &Query::all()).unwrap_or_default();
169 let names = found
170 .iter()
171 .filter_map(|object| {
172 let key = ravenrs_app::key_text(&object.get(&key.name))?;
173 match object.get(&name.name) {
174 Value::Text(text) => Some((key, text)),
175 _ => None,
176 }
177 })
178 .collect();
179 all.insert(target.clone(), names);
180 }
181 all
182}
183
184pub(crate) fn rows_of(session: &Session, entity: &EntityInfo, objects: &[Object]) -> serde_json::Value {
186 let names = names_for(session, entity);
187 serde_json::Value::Array(
188 objects
189 .iter()
190 .map(|object| {
191 let mut row = serde_json::Map::new();
192 for field in plain_fields(entity) {
193 row.insert(field.name.clone(), json_of(field, &object.get(&field.name), &names));
194 }
195 serde_json::Value::Object(row)
196 })
197 .collect(),
198 )
199}
200
201fn with_rows(mut report: Report, entity: &EntityInfo, rows: serde_json::Value) -> Report {
204 report.data_sources =
205 vec![DataSource { name: entity.name.clone(), kind: DataSourceKind::Json { data: rows } }];
206 report.data_source = Some(entity.name.clone());
207 report.data_member = None;
208 report
209}
210
211fn first_layout(session: &Session, name: &str, entity: &EntityInfo, rows: serde_json::Value) -> Report {
213 let words = session.words();
214 let columns: Vec<_> = plain_fields(entity).filter(|field| field.in_list).collect();
215 let options = WizardOptions {
216 title: name.to_owned(),
217 culture: if words.decimal == ',' { "pt-BR".into() } else { "en-US".into() },
218 columns: columns
219 .iter()
220 .map(|field| wizard::FieldInfo {
221 path: field.name.clone(),
222 kind: match field.kind {
223 FieldKind::Integer | FieldKind::Decimal(_) | FieldKind::Money => {
224 wizard::FieldKind::Number
225 }
226 FieldKind::Boolean => wizard::FieldKind::Boolean,
227 FieldKind::Date | FieldKind::DateTime => wizard::FieldKind::Date,
228 _ => wizard::FieldKind::Text,
229 },
230 })
231 .collect(),
232 totals: columns
233 .iter()
234 .filter(|field| matches!(field.kind, FieldKind::Money))
235 .map(|field| field.name.clone())
236 .collect(),
237 ..WizardOptions::default()
238 };
239 let column_words: Vec<ColumnWords> = columns
240 .iter()
241 .map(|field| ColumnWords {
242 caption: Some(field.caption.clone()),
243 format: matches!(field.kind, FieldKind::Money).then(|| format!("{}#,##0.00", words.money_prefix)),
244 })
245 .collect();
246 let source = DataSource { name: entity.name.clone(), kind: DataSourceKind::Json { data: rows } };
247 wizard::table_report_with(source, &options, &column_words)
248}
249
250fn layout_to_keep(mut report: Report) -> String {
252 for source in &mut report.data_sources {
253 source.kind = DataSourceKind::Json { data: serde_json::Value::Null };
254 }
255 report.to_json()
256}
257
258fn report_of(
260 session: &Session,
261 record: &Object,
262 words: &ReportWords,
263) -> Result<(Report, EntityInfo), String> {
264 let text = |name: &str| match record.get(name) {
265 Value::Text(text) => text,
266 _ => String::new(),
267 };
268 let target = text("target_entity");
269 let entity = session.entity(&target).ok_or_else(|| format!("{} {target}", words.unknown_entity))?;
270 let layout = text("layout");
271 if layout.trim().is_empty() {
272 return Ok((
273 first_layout(session, &text("name"), &entity, serde_json::Value::Array(Vec::new())),
274 entity,
275 ));
276 }
277 let report = Report::from_json(&layout).map_err(|error| format!("{} {error}", words.broken_layout))?;
278 Ok((report, entity))
279}
280
281fn every_record(session: &Session, entity: &str, limit: Option<usize>) -> Vec<Object> {
282 let query = match limit {
283 Some(limit) => Query::all().page(0, limit),
284 None => Query::all(),
285 };
286 session.frame().space().borrow().query(entity, &query).unwrap_or_default()
287}
288
289fn show_report(
291 session: &Session,
292 report: Report,
293 entity: &EntityInfo,
294 objects: &[Object],
295 words: &ReportWords,
296) {
297 let rows = rows_of(session, entity, objects);
298 let shown = ReportView::new(session.clone(), with_rows(report, entity, rows), words.clone());
299 session.show_full(move || Box::new(shown.clone()) as AnyView);
300}
301
302fn design_report(session: &Session, record: &Object, words: &ReportWords) -> Result<(), String> {
304 let (report, entity) = report_of(session, record, words)?;
305 let sample = every_record(session, &entity.name, Some(SAMPLE_ROWS));
306 let rows = rows_of(session, &entity, &sample);
307 let name = match record.get("name") {
308 Value::Text(name) => name,
309 _ => String::new(),
310 };
311 let report = if record.get("layout") == Value::Null {
312 first_layout(session, &name, &entity, rows)
313 } else {
314 with_rows(report, &entity, rows)
315 };
316 let (saving, closing, told) = (session.clone(), session.clone(), words.saved.clone());
317 let (key, entity_name) = (record.get("id"), REPORT_DEFINITION);
318 let designer = Rc::new(Designer::embedded(
319 report,
320 words.save.clone(),
321 words.close.clone(),
322 move |report| {
323 let space = saving.frame().space();
324 let mut space = space.borrow_mut();
325 let result = (|| {
326 let mut stored = space
327 .find(entity_name, &key)?
328 .ok_or_else(|| ravenrs_app::SpaceError::Conflict(told.clone()))?;
329 stored.set("layout", layout_to_keep(report));
330 space.save(stored)?;
331 space.commit()
332 })();
333 if let Err(error) = result {
334 space.rollback();
335 drop(space);
336 saving.show_message(Message::new(error.to_string(), MessageKind::Error));
337 } else {
338 drop(space);
339 saving.show_message(Message::new(told.clone(), MessageKind::Success));
340 saving.records_changed();
341 }
342 },
343 move || closing.close_full(),
344 ));
345 session.show_full(move || Box::new(SharedDesigner(designer.clone())) as AnyView);
346 Ok(())
347}
348
349struct SharedDesigner(Rc<Designer>);
350
351impl View for SharedDesigner {
352 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
353 self.0.build(cx)
354 }
355}
356
357type Rendered = (String, Rc<Document>);
359
360#[derive(Clone)]
362struct ReportView {
363 session: Session,
364 report: Rc<Report>,
365 words: ReportWords,
366 applied: State<ParameterValues>,
367 state: Rc<ViewerState>,
368 engine: Rc<RefCell<ReportEngine>>,
369 rendered: Rc<RefCell<Option<Rendered>>>,
370}
371
372impl ReportView {
373 fn new(session: Session, report: Report, words: ReportWords) -> Self {
374 Self {
375 session,
376 report: Rc::new(report),
377 words,
378 applied: State::new(ParameterValues::new()),
379 state: Rc::new(ViewerState::new()),
380 engine: Rc::new(RefCell::new(ReportEngine::new())),
381 rendered: Rc::default(),
382 }
383 }
384
385 fn document(&self) -> Rc<Document> {
386 let parameters = self.applied.get();
387 let mut sorted: Vec<_> = parameters.iter().collect();
388 sorted.sort_by_key(|(name, _)| *name);
389 let key = format!("{sorted:?}");
390 if let Some((known, document)) = self.rendered.borrow().as_ref()
391 && *known == key
392 {
393 return document.clone();
394 }
395 let data = self.report.embedded_data().cloned().unwrap_or(serde_json::Value::Null);
396 let options = RenderOptions { parameters: parameters.clone(), ..RenderOptions::default() };
397 let document = Rc::new(self.engine.borrow_mut().render_with(&self.report, &data, &options));
398 *self.rendered.borrow_mut() = Some((key, document.clone()));
399 document
400 }
401}
402
403impl View for ReportView {
404 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
405 let document = self.document();
406 let picker = cx.file_picker();
407 let (session, closing) = (self.session.clone(), self.session.clone());
408 let trailing_document = document.clone();
409 let close = self.words.close.clone();
410 let viewer = DocumentViewer::new(document, &self.state).toolbar_trailing(move || {
411 let export = |label: &str, format: ExportFormat| -> AnyView {
412 let (session, picker, document) =
413 (session.clone(), picker.clone(), trailing_document.clone());
414 Box::new(
415 Button::new(label.to_owned())
416 .style(ButtonStyle::Secondary)
417 .size(ButtonSize::Small)
418 .on_click(move || match ravenrs_report_export::export(&document, format, None) {
419 Ok(file) => session.hand_file(file.name, file.bytes, &picker),
420 Err(error) => {
421 session.show_message(Message::new(error.to_string(), MessageKind::Error));
422 }
423 }),
424 )
425 };
426 let closing = closing.clone();
427 vec![
428 export("PDF", ExportFormat::Pdf),
429 export("XLSX", ExportFormat::Xlsx),
430 Box::new(
431 Button::new(close.clone())
432 .style(ButtonStyle::Ghost)
433 .size(ButtonSize::Small)
434 .on_click(move || closing.close_full()),
435 ) as AnyView,
436 ]
437 });
438 let panel = (!self.report.parameters.is_empty()).then(|| {
439 let today = Date::from_days(ravenrs_app::local_now().days);
440 ParameterPanel::new(self.report.parameters.clone(), self.applied.clone(), today)
441 });
442 let content = VStack::new((panel, viewer.height(VIEWER_HEIGHT)))
443 .spacing(8.0)
444 .align(ravenrs_layout::Align::Stretch)
445 .grow(1.0);
446 cx.child(&content)
447 }
448}
449
450struct StoredReports {
451 words: ReportWords,
452}
453
454impl ViewController for StoredReports {
455 fn id(&self) -> &str {
456 "StoredReports"
457 }
458
459 fn actions(&self) -> Vec<Action> {
460 let (show_words, design_words) = (self.words.clone(), self.words.clone());
461 vec![
462 Action::simple(SHOW, self.words.show.clone())
463 .category(ActionCategory::View)
464 .target_entity(REPORT_DEFINITION)
465 .selection(Selection::RequireSingle)
466 .icon("eye")
467 .on_execute(move |cx| {
468 let session = Session::of(cx).ok_or("this action needs the screens")?;
469 let record = cx.selected.first().ok_or("this action needs a report")?;
470 let (report, entity) = report_of(&session, record, &show_words)?;
471 let objects = every_record(&session, &entity.name, None);
472 show_report(&session, report, &entity, &objects, &show_words);
473 Ok(())
474 }),
475 Action::simple(DESIGN, self.words.design.clone())
476 .category(ActionCategory::Edit)
477 .target_entity(REPORT_DEFINITION)
478 .selection(Selection::RequireSingle)
479 .requires(Operation::Write)
480 .icon("pencil")
481 .on_execute(move |cx| {
482 let session = Session::of(cx).ok_or("this action needs the screens")?;
483 let record = cx.selected.first().ok_or("this action needs a report")?;
484 design_report(&session, record, &design_words)
485 }),
486 ]
487 }
488}
489
490struct ShowInReport {
491 words: ReportWords,
492}
493
494impl ViewController for ShowInReport {
495 fn id(&self) -> &str {
496 "ShowInReport"
497 }
498
499 fn scope(&self) -> TargetScope {
500 TargetScope::any().view_kind(ViewKind::List)
501 }
502
503 fn activated(&mut self, frame: &ravenrs_app::Frame) {
504 let own = [REPORT_DEFINITION, crate::ANALYTICS_DASHBOARD, crate::ANALYTICS_TILE];
506 let shows =
507 frame.view().and_then(|view| view.entity).is_none_or(|name| !own.contains(&name.as_str()));
508 if let Some(action) = frame.action(SHOW_IN_REPORT) {
509 action.set_active("own records", shows);
510 }
511 }
512
513 fn actions(&self) -> Vec<Action> {
514 let words = self.words.clone();
515 vec![
516 Action::simple(SHOW_IN_REPORT, self.words.show_in_report.clone())
517 .category(ActionCategory::Export)
518 .icon("file-text")
519 .on_execute(move |cx| {
520 let session = Session::of(cx).ok_or("this action needs the screens")?;
521 let view = cx.view.as_ref().ok_or("this action needs a list")?;
522 let entity = view
523 .entity
524 .as_deref()
525 .and_then(|name| session.entity(name))
526 .ok_or("this action needs a list")?;
527 let objects =
529 if cx.selected.is_empty() { session.list_records(view) } else { cx.selected.clone() };
530 let reports: Vec<Object> = every_record(&session, REPORT_DEFINITION, None)
531 .into_iter()
532 .filter(|report| report.get("target_entity") == Value::Text(entity.name.clone()))
533 .collect();
534 match reports.as_slice() {
535 [] => session
536 .show_message(Message::new(words.none_for_list.clone(), MessageKind::Warning)),
537 [one] => {
538 let (report, _) = report_of(&session, one, &words)?;
539 show_report(&session, report, &entity, &objects, &words);
540 }
541 many => {
542 let (session_for_list, words_for_list, many) =
543 (session.clone(), words.clone(), many.to_vec());
544 session.present(words.pick_report.clone(), move || {
545 let buttons: Vec<AnyView> = many
546 .iter()
547 .map(|record| {
548 let name = match record.get("name") {
549 Value::Text(name) => name,
550 _ => String::new(),
551 };
552 let (session, words, record, entity, objects) = (
553 session_for_list.clone(),
554 words_for_list.clone(),
555 record.clone(),
556 entity.clone(),
557 objects.clone(),
558 );
559 Box::new(Button::new(name).on_click(move || {
560 session.close_presented();
561 match report_of(&session, &record, &words) {
562 Ok((report, _)) => {
563 show_report(&session, report, &entity, &objects, &words);
564 }
565 Err(why) => {
566 session
567 .show_message(Message::new(why, MessageKind::Error));
568 }
569 }
570 })) as AnyView
571 })
572 .collect();
573 Box::new(VStack::new(buttons).spacing(8.0)) as AnyView
574 });
575 }
576 }
577 Ok(())
578 }),
579 ]
580 }
581}
582
583#[derive(Debug, Default)]
586pub struct StoredReportsModule {
587 words: ReportWords,
588 predefined: Vec<(String, String, String)>,
589}
590
591impl StoredReportsModule {
592 pub fn new(words: ReportWords) -> Self {
594 Self { words, predefined: Vec::new() }
595 }
596
597 pub fn predefined(mut self, name: impl Into<String>, entity: impl Into<String>, report: Report) -> Self {
599 self.predefined.push((name.into(), entity.into(), layout_to_keep(report)));
600 self
601 }
602}
603
604impl Module for StoredReportsModule {
605 fn name(&self) -> &str {
606 "StoredReports"
607 }
608
609 fn entities(&self) -> Vec<EntityInfo> {
610 vec![report_entity()]
611 }
612
613 fn view_controllers(&self) -> Vec<Box<dyn ViewController>> {
614 vec![
615 Box::new(StoredReports { words: self.words.clone() }),
616 Box::new(ShowInReport { words: self.words.clone() }),
617 ]
618 }
619
620 fn initial_records(&self, space: &mut dyn ObjectSpace) -> Result<(), String> {
621 let wrong = |error: ravenrs_app::SpaceError| error.to_string();
622 let existing = space.query(REPORT_DEFINITION, &Query::all()).map_err(wrong)?;
623 for (name, entity, layout) in &self.predefined {
624 if existing.iter().any(|record| record.get("name") == Value::Text(name.clone())) {
625 continue;
626 }
627 let mut record = space.new_object(REPORT_DEFINITION).map_err(wrong)?;
628 record.set("name", name.clone());
629 record.set("target_entity", entity.clone());
630 record.set("layout", layout.clone());
631 space.save(record).map_err(wrong)?;
632 }
633 Ok(())
634 }
635}
636
637pub(crate) fn problem(title: &str, why: String) -> AnyView {
639 Box::new(Alert::new(title.to_owned(), why).tone(Tone::Danger))
640}