FOSSredder 0.5.0
Loading...
Searching...
No Matches
AnalysisViewModel.cpp
Go to the documentation of this file.
1
5
7
8#include <algorithm>
9#include <cmath>
10
11#include <QDate>
12#include <QSet>
13#include <QStringList>
14#include <QUrl>
15
18#include "ui/shell/Settings.h"
24
25namespace ui {
26
27namespace {
28
29constexpr int kPreviewDebounceMs = 40;
30constexpr auto kPlot = "plot";
31constexpr auto kTab = "tab";
32constexpr auto kXlsx = "xlsx";
33constexpr auto kCsv = "csv";
34constexpr auto kPng = "png";
35constexpr auto kJpg = "jpg";
36constexpr auto kTable = "table";
37constexpr auto kTabular = "tabular";
38constexpr auto kPie = "pie";
39constexpr auto kHistogram = "histogram";
40constexpr auto kAll = "all";
41constexpr auto kAllocatable = "allocatable";
42constexpr auto kNonAllocatable = "non-allocatable";
43constexpr auto kUnassigned = "unassigned";
44
45QString qstr(const char* value) {
46 return QString::fromLatin1(value);
47}
48
49QString rowIdAt(const QVariantList& rows, int index,
50 const QString& idKey = QStringLiteral("id")) {
51 if (index < 0 || index >= rows.size()) {
52 return {};
53 }
54 return rows.at(index).toMap().value(idKey).toString();
55}
56
57int indexOfId(const QVariantList& rows, const QString& id,
58 const QString& idKey = QStringLiteral("id")) {
59 if (id.isEmpty()) {
60 return -1;
61 }
62 for (int i = 0; i < rows.size(); ++i) {
63 if (rows.at(i).toMap().value(idKey).toString() == id) {
64 return i;
65 }
66 }
67 return -1;
68}
69
70QVariantMap rowById(const QVariantList& rows, const QString& id,
71 const QString& idKey = QStringLiteral("id")) {
72 const int index = indexOfId(rows, id.trimmed(), idKey);
73 return index >= 0 ? rows.at(index).toMap() : QVariantMap{};
74}
75
76QString navigatedSelectionId(const QVariantList& rows, const QString& currentId,
77 int delta, int defaultIndex = 0,
78 const QString& idKey = QStringLiteral("id")) {
79 const int currentIndex = indexOfId(rows, currentId, idKey);
80 if (currentIndex < 0) {
81 if (rows.isEmpty()) {
82 return {};
83 }
84 if (delta > 0) {
85 return rowIdAt(rows, 0, idKey);
86 }
87 if (delta < 0) {
88 return rowIdAt(rows, rows.size() - 1, idKey);
89 }
90 return rowIdAt(rows, defaultIndex, idKey);
91 }
92 if (delta > 0) {
93 return currentIndex >= rows.size() - 1
94 ? QString()
95 : rowIdAt(rows, currentIndex + 1, idKey);
96 }
97 if (delta < 0) {
98 return currentIndex <= 0 ? QString()
99 : rowIdAt(rows, currentIndex - 1, idKey);
100 }
101 return rowIdAt(rows, currentIndex, idKey);
102}
103
104QString deleteNextSelectionId(const QVariantList& rows,
105 const QString& removedId, int defaultIndex = 0,
106 const QString& idKey = QStringLiteral("id")) {
107 if (rows.isEmpty()) {
108 return {};
109 }
110 const int removedIndex = indexOfId(rows, removedId, idKey);
111 const int nextIndex = removedIndex >= 0 ? removedIndex + 1 : defaultIndex;
112 const int wrapped = nextIndex % rows.size();
113 return rowIdAt(rows, wrapped < 0 ? wrapped + rows.size() : wrapped, idKey);
114}
115
116double numberFromVariant(const QVariant& value) {
117 bool ok = false;
118 const double out = value.toDouble(&ok);
119 return ok && std::isfinite(out) ? out : 0.0;
120}
121
122QString amountText(double value) {
123 return QString::number(value, 'f', 2);
124}
125
126QString amountText(const QVariant& value) {
127 return amountText(numberFromVariant(value));
128}
129
130QString nonEmptyString(const QVariantMap& map, const QString& key,
131 const QString& defaultValue = {}) {
132 const QString value = map.value(key).toString();
133 return value.isEmpty() ? defaultValue : value;
134}
135
136QVariantList mapKeys(const QVariantMap& value) {
137 QVariantList keys;
138 keys.reserve(value.size());
139 for (auto it = value.cbegin(); it != value.cend(); ++it) {
140 if (!it.key().trimmed().isEmpty()) {
141 keys.push_back(it.key());
142 }
143 }
144 return keys;
145}
146
147QString normalizedType(const QString& value) {
148 const QString trimmed = value.trimmed().toLower();
149 return trimmed == qstr(kTabular) ? qstr(kTab) : trimmed;
150}
151
152QString plotTypeFromSubtypeIndex(int plotSubtypeIndex) {
153 return plotSubtypeIndex == 1 ? qstr(kHistogram) : qstr(kPie);
154}
155
156bool containsPropertyId(const QVariantList& rows, const QString& id) {
157 for (const QVariant& value : rows) {
158 if (value.toMap().value(QStringLiteral("id")).toString() == id) {
159 return true;
160 }
161 }
162 return false;
163}
164
165QString rowKey(const QVariant& value, const QString& idField) {
166 if (!idField.isEmpty()) {
167 return value.toMap().value(idField).toString();
168 }
169 const QVariantMap row = value.toMap();
170 if (row.contains(QStringLiteral("value"))) {
171 return row.value(QStringLiteral("value")).toString();
172 }
173 return value.toString();
174}
175
176bool containsRowValue(const QVariantList& rows, const QString& value) {
177 for (const QVariant& row : rows) {
178 if (rowKey(row, {}) == value) {
179 return true;
180 }
181 }
182 return false;
183}
184
185void appendContractTypeRow(QVariantList& rows, const QString& value,
186 const QString& label) {
187 if (value.isEmpty() || containsRowValue(rows, value)) {
188 return;
189 }
190 rows.push_back(QVariantMap{{QStringLiteral("value"), value},
191 {QStringLiteral("label"), label}});
192}
193
194} // namespace
195
196AnalysisViewModel::AnalysisViewModel(QObject* parent) : QObject(parent) {
197 previewDebounce_.setInterval(kPreviewDebounceMs);
198 previewDebounce_.setSingleShot(true);
199 connect(&previewDebounce_, &QTimer::timeout, this,
200 &AnalysisViewModel::refreshPreview);
201}
202
204 WorkspaceCommands* commands,
205 WorkspaceSelection* selection,
206 WorkspaceSelectors* selectors) {
207 if (store_ == store && commands_ == commands && selection_ == selection &&
208 selectors_ == selectors) {
209 return;
210 }
211 bindWorkspaceRoles(store, commands, selection, selectors);
213 emitChanged();
214}
215
217 if (analysisWorkflow_ == value) {
218 return;
219 }
220 analysisWorkflow_ = value;
222 emitChanged();
223}
224
226 if (settings_ == value) {
227 return;
228 }
229 settings_ = value;
231 emitChanged();
232}
233
235 return !selectedAnalysisId().isEmpty();
236}
237
239 return !analysisRows().isEmpty();
240}
241
243 refreshFilterRows();
244 loadSelectedAnalysis();
245 updateResultState();
246 emitChanged();
247}
248
249void AnalysisViewModel::emitChanged() {
250 emit changed();
251}
252
253void AnalysisViewModel::bindWorkspaceRoles(WorkspaceStore* store,
254 WorkspaceCommands* commands,
255 WorkspaceSelection* selection,
256 WorkspaceSelectors* selectors) {
257 if (store_) {
258 disconnect(store_, nullptr, this, nullptr);
259 }
260 if (selection_) {
261 disconnect(selection_, nullptr, this, nullptr);
262 }
263 store_ = store;
264 commands_ = commands;
265 selection_ = selection;
266 selectors_ = selectors;
267 if (!store_ || !selection_) {
268 return;
269 }
270 connect(selection_, &WorkspaceSelection::selectedAnalysisIdChanged, this,
272 connect(store_, &WorkspaceStore::dataRevisionChanged, this,
274}
275
277 if (!analysisWorkflow_ || !commands_ || !selection_ || !canSubmit()) {
278 return;
279 }
281 "AnalysisViewModel::submitCreate", "Analysis create submitted",
282 {{observability::context::kName, name_.toStdString()}});
283 refreshPreview();
284 refreshAdjustmentAmountsFromSelection();
285 const QString type = strategyType();
286 activeResultType_ = type;
287 const QString selectedExportFormat =
288 normalizedExportFormat(exportFormat_, uiType());
289 snapshotTransactions_ = previewTransactions_;
290 const QString newId = commands_->addAnalysis(
291 name_, type, currentAnalysisConfig(), currentAnalysisFilter(),
292 selectedExportFormat, includeAdjustments_, snapshotTransactions_,
293 adjustmentAmountsById_);
294 if (newId.isEmpty()) {
295 return;
296 }
297 selection_->selectAnalysis(newId);
298 selectedAdjustmentTxIds_ = mapKeys(adjustmentAmountsById_);
299 refreshAnalysisResult();
300 filterEditMode_ = false;
301 emitChanged();
302}
303
305 if (!analysisWorkflow_ || !commands_ || selectedAnalysisId().isEmpty() ||
306 !canSubmit()) {
307 return;
308 }
310 "AnalysisViewModel::submitUpdate", "Analysis update submitted",
312 {observability::context::kName, name_.toStdString()}});
313 const QString type = strategyType();
314 activeResultType_ = type;
315 commands_->updateAnalysis(
316 selectedAnalysisId(), name_, type, currentAnalysisConfig(),
317 currentAnalysisFilter(), normalizedExportFormat(exportFormat_, uiType()),
318 includeAdjustments_, snapshotTransactions_, adjustmentAmountsById_);
319 selectedAdjustmentTxIds_ = mapKeys(adjustmentAmountsById_);
320 refreshAnalysisResult();
321 filterEditMode_ = false;
322 emitChanged();
323}
324
326 const QString removedId = selectedAnalysisId();
327 if (removedId.isEmpty() || !commands_ || !selection_) {
328 return;
329 }
331 "AnalysisViewModel::deleteCurrent", "Analysis delete submitted",
332 {{observability::context::kId, removedId.toStdString()}});
333 const int currentIndex = indexOfId(analysisRows(), removedId);
334 commands_->deleteAnalysis(removedId);
335 selection_->selectAnalysis(
336 deleteNextSelectionId(analysisRows(), removedId, currentIndex));
337 emitChanged();
338}
339
341 if (!selection_ || analysisRows().isEmpty()) {
342 return;
343 }
344
345 const QVariantList rows = analysisRows();
346 selection_->selectAnalysis(
347 navigatedSelectionId(rows, selectedAnalysisId(), delta));
348}
349
350void AnalysisViewModel::selectAnalysis(const QString& id) {
351 if (selection_) {
352 selection_->selectAnalysis(id);
353 }
354}
355
357 return selection_ ? selection_->selectedAnalysisId() : QString();
358}
359
360QVariantList AnalysisViewModel::analysisRows() const {
361 return selectors_ ? selectors_->analysisRows() : QVariantList();
362}
363
364QVariantMap AnalysisViewModel::analysisRowById(const QString& id) const {
365 return rowById(analysisRows(), id);
366}
367
368QStringList AnalysisViewModel::stringList(const QVariantList& values) const {
369 QStringList out;
370 out.reserve(values.size());
371 for (const QVariant& value : values) {
372 const QString string = value.toString();
373 if (!string.isEmpty()) {
374 out.push_back(string);
375 }
376 }
377 return out;
378}
379
380QVariantList AnalysisViewModel::allPropertyIds() const {
381 QVariantList out;
382 for (const QVariant& value : propertyFilterRows_) {
383 const QString id = value.toMap().value(QStringLiteral("id")).toString();
384 if (!id.isEmpty()) {
385 out.push_back(id);
386 }
387 }
388 return out;
389}
390
391QVariantList AnalysisViewModel::allContractTypes() const {
392 QVariantList out;
393 for (const QVariant& value : contractTypeRows_) {
394 const QString type = rowKey(value, {});
395 if (!type.isEmpty()) {
396 out.push_back(type);
397 }
398 }
399 return out;
400}
401
402QVariantList
403AnalysisViewModel::pruneSelection(const QVariantList& values,
404 const QVariantList& availableRows,
405 const QString& idField) const {
406 QSet<QString> available;
407 for (const QVariant& value : availableRows) {
408 const QString key = rowKey(value, idField);
409 if (!key.isEmpty()) {
410 available.insert(key);
411 }
412 }
413
414 QVariantList out;
415 for (const QVariant& value : values) {
416 const QString key = value.toString();
417 if (!key.isEmpty() && available.contains(key)) {
418 out.push_back(key);
419 }
420 }
421 return out;
422}
423
424bool AnalysisViewModel::isAllSelected(const QVariantList& selectedIds,
425 const QVariantList& availableRows,
426 const QString& idField) const {
427 if (availableRows.isEmpty()) {
428 return true;
429 }
430 QSet<QString> selected;
431 for (const QVariant& value : selectedIds) {
432 const QString key = value.toString();
433 if (!key.isEmpty()) {
434 selected.insert(key);
435 }
436 }
437 if (selected.size() != availableRows.size()) {
438 return false;
439 }
440 for (const QVariant& value : availableRows) {
441 const QString key = rowKey(value, idField);
442 if (key.isEmpty() || !selected.contains(key)) {
443 return false;
444 }
445 }
446 return true;
447}
448
449void AnalysisViewModel::refreshFilterRows() {
450 const bool propertyWasAll = isAllSelected(
451 selectedPropertyIds_, propertyFilterRows_, QStringLiteral("id"));
452 const bool contractTypesWereAll =
453 isAllSelected(selectedContractTypes_, contractTypeRows_);
454
455 propertyFilterRows_ =
456 selectors_ ? selectors_->propertyRows() : QVariantList();
457 if (!containsPropertyId(propertyFilterRows_, qstr(kUnassigned))) {
458 propertyFilterRows_.push_back(QVariantMap{
459 {QStringLiteral("id"), qstr(kUnassigned)},
460 {QStringLiteral("name"), tr("Unassigned")},
461 });
462 }
463 selectedPropertyIds_ =
464 propertyWasAll ? allPropertyIds()
465 : pruneSelection(selectedPropertyIds_, propertyFilterRows_,
466 QStringLiteral("id"));
467
468 QVariantList types;
469 if (analysisWorkflow_) {
470 for (const QString& type : analysisWorkflow_->contractTypes()) {
471 appendContractTypeRow(types, type, type);
472 }
473 }
474 appendContractTypeRow(types, qstr(kUnassigned), tr("Unassigned"));
475 contractTypeRows_ = types;
476 selectedContractTypes_ =
477 contractTypesWereAll
478 ? allContractTypes()
479 : pruneSelection(selectedContractTypes_, contractTypeRows_);
480}
481
482QString AnalysisViewModel::currentDateField() const {
483 return dateFieldIndex_ == 1 ? QStringLiteral("valuta")
484 : QStringLiteral("bookingDate");
485}
486
487QString AnalysisViewModel::currentDateMode() const {
488 return dateModeIndex_ == 0 ? QStringLiteral("year") : QStringLiteral("range");
489}
490
491QString AnalysisViewModel::defaultAnalysisDateMode() const {
492 if (settings_) {
493 const QString mode = settings_->analysisDefaultDateMode().toLower();
494 return mode == QStringLiteral("range") ? QStringLiteral("range")
495 : QStringLiteral("year");
496 }
497 return QStringLiteral("year");
498}
499
500QString AnalysisViewModel::defaultAnalysisYear() const {
501 if (settings_ && settings_->analysisDefaultYear() > 0) {
502 return QString::number(settings_->analysisDefaultYear());
503 }
504 return QString::number(QDate::currentDate().year() - 1);
505}
506
507QStringList AnalysisViewModel::effectiveSelectedPropertyIds() const {
508 if (propertyFilterRows_.isEmpty() ||
509 isAllSelected(selectedPropertyIds_, propertyFilterRows_,
510 QStringLiteral("id"))) {
511 return {};
512 }
513 return selectedPropertyIds_.isEmpty() ? QStringList{}
514 : stringList(selectedPropertyIds_);
515}
516
517QStringList AnalysisViewModel::effectiveSelectedContractTypes() const {
518 if (contractTypeRows_.isEmpty() ||
519 isAllSelected(selectedContractTypes_, contractTypeRows_)) {
520 return {};
521 }
522 return selectedContractTypes_.isEmpty() ? QStringList{}
523 : stringList(selectedContractTypes_);
524}
525
526QString AnalysisViewModel::currentFilterSpec() const {
527 if (!analysisWorkflow_) {
528 return {};
529 }
530 const QString mode = allocatableMode_.toLower();
531 return analysisWorkflow_->analysisFilterSpec(
532 currentDateField(), currentDateMode(), yearValue_, dateFromValue_,
533 dateToValue_, effectiveSelectedPropertyIds(),
534 effectiveSelectedContractTypes(),
535 mode == qstr(kAllocatable) || mode == qstr(kNonAllocatable) ? mode
536 : QString());
537}
538
539QVariantMap AnalysisViewModel::currentAnalysisConfig() const {
540 return QVariantMap{
541 {QStringLiteral("type"), strategyType()},
542 {QStringLiteral("plotType"), plotTypeFromSubtypeIndex(plotSubtypeIndex_)},
543 {QStringLiteral("plotMeasure"), QStringLiteral("totalAmount")},
544 {QStringLiteral("propertyIds"), effectiveSelectedPropertyIds()},
545 {QStringLiteral("contractTypes"), effectiveSelectedContractTypes()},
546 {QStringLiteral("taxPercent"), 0.0},
547 };
548}
549
550QVariantMap AnalysisViewModel::currentAnalysisFilter() const {
551 const QString mode = allocatableMode_.toLower();
552 return QVariantMap{
553 {QStringLiteral("dateField"), currentDateField()},
554 {QStringLiteral("dateMode"), currentDateMode()},
555 {QStringLiteral("year"), yearValue_},
556 {QStringLiteral("dateFrom"), dateFromValue_},
557 {QStringLiteral("dateTo"), dateToValue_},
558 {QStringLiteral("propertyIds"), effectiveSelectedPropertyIds()},
559 {QStringLiteral("propertyIdsNone"),
560 selectedPropertyIds_.contains(qstr(kUnassigned))},
561 {QStringLiteral("contractTypes"), effectiveSelectedContractTypes()},
562 {QStringLiteral("contractTypesNone"),
563 selectedContractTypes_.contains(qstr(kUnassigned))},
564 {QStringLiteral("allocatableMode"),
565 mode == qstr(kAllocatable) || mode == qstr(kNonAllocatable) ? mode
566 : QString()},
567 };
568}
569
570void AnalysisViewModel::resetAdjustments() {
571 selectedAdjustmentTxIds_.clear();
572 adjustmentAmountsById_.clear();
573 adjustmentName_.clear();
574 adjustmentPercentText_.clear();
575}
576
578 if (filterEditMode_ == value) {
579 return;
580 }
581 filterEditMode_ = value;
582 emitChanged();
583}
584
586 const int next = value == 1 ? 1 : 0;
587 if (dateFieldIndex_ == next) {
588 return;
589 }
590 dateFieldIndex_ = next;
591 requestPreviewRefresh();
592 emitChanged();
593}
594
596 const int next = value == 1 ? 1 : 0;
597 if (dateModeIndex_ == next) {
598 return;
599 }
600 dateModeIndex_ = next;
601 requestPreviewRefresh();
602 emitChanged();
603}
604
605void AnalysisViewModel::setYearValue(const QString& value) {
606 if (yearValue_ == value) {
607 return;
608 }
609 yearValue_ = value;
610 requestPreviewRefresh();
611 emitChanged();
612}
613
614void AnalysisViewModel::setDateFromValue(const QString& value) {
615 if (dateFromValue_ == value) {
616 return;
617 }
618 dateFromValue_ = value;
619 requestPreviewRefresh();
620 emitChanged();
621}
622
623void AnalysisViewModel::setDateToValue(const QString& value) {
624 if (dateToValue_ == value) {
625 return;
626 }
627 dateToValue_ = value;
628 requestPreviewRefresh();
629 emitChanged();
630}
631
632void AnalysisViewModel::setSelectedPropertyIds(const QVariantList& value) {
633 if (selectedPropertyIds_ == value) {
634 return;
635 }
636 selectedPropertyIds_ = value;
637 requestPreviewRefresh();
638 emitChanged();
639}
640
641void AnalysisViewModel::setSelectedContractTypes(const QVariantList& value) {
642 if (selectedContractTypes_ == value) {
643 return;
644 }
645 selectedContractTypes_ = value;
646 requestPreviewRefresh();
647 emitChanged();
648}
649
650void AnalysisViewModel::setAllocatableMode(const QString& value) {
651 const QString lower = value.trimmed().toLower();
652 const QString next =
653 lower == qstr(kAllocatable) || lower == qstr(kNonAllocatable)
654 ? lower
655 : qstr(kAll);
656 if (allocatableMode_ == next) {
657 return;
658 }
659 allocatableMode_ = next;
660 requestPreviewRefresh();
661 emitChanged();
662}
663
664void AnalysisViewModel::setPropertySelected(const QString& id, bool selected) {
665 const QString trimmed = id.trimmed();
666 if (trimmed.isEmpty()) {
667 return;
668 }
669 QVariantList next = selectedPropertyIds_;
670 if (selected && !next.contains(trimmed)) {
671 next.push_back(trimmed);
672 }
673 if (!selected) {
674 next.removeAll(trimmed);
675 }
677}
678
682
686
688 bool selected) {
689 const QString trimmed = type.trimmed();
690 if (trimmed.isEmpty()) {
691 return;
692 }
693 QVariantList next = selectedContractTypes_;
694 if (selected && !next.contains(trimmed)) {
695 next.push_back(trimmed);
696 }
697 if (!selected) {
698 next.removeAll(trimmed);
699 }
701}
702
706
710
712 bool selected) {
713 const QString trimmed = id.trimmed();
714 if (trimmed.isEmpty()) {
715 return;
716 }
717 QVariantList next = selectedAdjustmentTxIds_;
718 if (selected && !next.contains(trimmed)) {
719 next.push_back(trimmed);
720 }
721 if (!selected) {
722 next.removeAll(trimmed);
723 }
725}
726
728 if (index == 1) {
729 setAllocatableMode(qstr(kAllocatable));
730 return;
731 }
732 if (index == 2) {
733 setAllocatableMode(qstr(kNonAllocatable));
734 return;
735 }
736 setAllocatableMode(qstr(kAll));
737}
738
740 filterContentIndex_ = filterContentIndex_ == 0 ? 1 : 0;
741 emitChanged();
742}
743
745 dateFieldIndex_ = 0;
746 dateModeIndex_ = 0;
747 yearValue_ = defaultAnalysisYear();
748 dateFromValue_.clear();
749 dateToValue_.clear();
750 selectedPropertyIds_ = allPropertyIds();
751 selectedContractTypes_ = allContractTypes();
752 allocatableMode_ = qstr(kAll);
753 resetAdjustments();
754 refreshPreview();
755 emitChanged();
756}
757
758void AnalysisViewModel::setName(const QString& value) {
759 if (name_ == value) {
760 return;
761 }
762 name_ = value;
763 emitChanged();
764}
765
767 const int next = value == 1 ? 1 : 0;
768 if (mainTypeIndex_ == next) {
769 return;
770 }
771 mainTypeIndex_ = next;
772 activeResultType_ = strategyType();
773 exportFormat_ = normalizedExportFormat(exportFormat_, uiType());
774 emitChanged();
775}
776
778 const int next = value == 1 ? 1 : 0;
779 if (plotSubtypeIndex_ == next) {
780 return;
781 }
782 plotSubtypeIndex_ = next;
783 emitChanged();
784}
785
787 return {QVariantMap{{QStringLiteral("value"), qstr(kPie)},
788 {QStringLiteral("label"), tr("Pie chart")}},
789 QVariantMap{{QStringLiteral("value"), qstr(kHistogram)},
790 {QStringLiteral("label"), tr("Histogram")}}};
791}
792
794 if (uiType() == qstr(kTable)) {
795 return {QVariantMap{{QStringLiteral("value"), qstr(kXlsx)},
796 {QStringLiteral("label"), tr("XLSX")}},
797 QVariantMap{{QStringLiteral("value"), qstr(kCsv)},
798 {QStringLiteral("label"), tr("CSV")}}};
799 }
800 return {QVariantMap{{QStringLiteral("value"), qstr(kPng)},
801 {QStringLiteral("label"), tr("PNG")}},
802 QVariantMap{{QStringLiteral("value"), qstr(kJpg)},
803 {QStringLiteral("label"), tr("JPG")}}};
804}
805
806void AnalysisViewModel::setExportFormat(const QString& value) {
807 const QString next = normalizedExportFormat(value, uiType());
808 if (exportFormat_ == next) {
809 return;
810 }
811 exportFormat_ = next;
812 emitChanged();
813}
814
816 const QVariantList options = exportFormatOptions();
817 const QString current = normalizedExportFormat(exportFormat_, uiType());
818 for (int index = 0; index < options.size(); ++index) {
819 if (options.at(index).toMap().value(QStringLiteral("value")).toString() ==
820 current) {
821 return index;
822 }
823 }
824 return 0;
825}
826
828 const QVariantList options = exportFormatOptions();
829 if (options.isEmpty()) {
830 return;
831 }
832 const int index = std::clamp(value, 0, static_cast<int>(options.size()) - 1);
833 setExportFormat(
834 options.at(index).toMap().value(QStringLiteral("value")).toString());
835}
836
838 if (includeAdjustments_ == value) {
839 return;
840 }
841 includeAdjustments_ = value;
842 refreshAnalysisResult();
843 updateResultState();
844 emitChanged();
845}
846
847void AnalysisViewModel::setSelectedAdjustmentTxIds(const QVariantList& value) {
848 if (selectedAdjustmentTxIds_ == value) {
849 return;
850 }
851 selectedAdjustmentTxIds_ = value;
852 emitChanged();
853}
854
855void AnalysisViewModel::setAdjustmentName(const QString& value) {
856 if (adjustmentName_ == value) {
857 return;
858 }
859 adjustmentName_ = value;
860 emitChanged();
861}
862
864 if (adjustmentPercentText_ == value) {
865 return;
866 }
867 adjustmentPercentText_ = value;
868 emitChanged();
869}
870
872 return !name_.trimmed().isEmpty() && !selectedPropertyIds_.isEmpty() &&
873 !selectedContractTypes_.isEmpty();
874}
875
876QString AnalysisViewModel::uiType() const {
877 return mainTypeIndex_ == 1 ? qstr(kTable) : qstr(kPlot);
878}
879
880QString AnalysisViewModel::strategyType() const {
881 return uiType() == qstr(kTable) ? qstr(kTab) : qstr(kPlot);
882}
883
884QString
885AnalysisViewModel::normalizedExportFormat(const QString& value,
886 const QString& targetUiType) const {
887 const QString normalized = value.trimmed().toLower();
888 const QStringList allowed = targetUiType == qstr(kTable)
889 ? QStringList{qstr(kXlsx), qstr(kCsv)}
890 : QStringList{qstr(kPng), qstr(kJpg)};
891 return allowed.contains(normalized) ? normalized : allowed.first();
892}
893
894void AnalysisViewModel::loadSelectedAnalysis() {
895 if (!selectors_ || selectedAnalysisId().isEmpty()) {
896 loadCreateState();
897 return;
898 }
899
900 const QVariantMap row = analysisRowById(selectedAnalysisId());
901 if (row.isEmpty()) {
902 refreshPreview();
903 return;
904 }
905
906 name_ = row.value(QStringLiteral("name")).toString();
907 const QString type =
908 normalizedType(row.value(QStringLiteral("type"), qstr(kPlot)).toString());
909 mainTypeIndex_ = type == qstr(kTab) ? 1 : 0;
910 activeResultType_ = type.isEmpty() ? qstr(kPlot) : type;
911
912 const QVariantMap config =
913 row.value(payload::keys::analysis::kConfig).toMap();
914 plotSubtypeIndex_ =
915 config.value(QStringLiteral("plotType")).toString() == qstr(kHistogram)
916 ? 1
917 : 0;
918
919 const QVariantMap parsed =
920 row.value(payload::keys::analysis::kFilter).toMap();
921 dateFieldIndex_ = parsed.value(QStringLiteral("dateField")).toString() ==
922 QStringLiteral("valuta")
923 ? 1
924 : 0;
925 dateModeIndex_ = parsed.value(QStringLiteral("dateMode")).toString() ==
926 QStringLiteral("year")
927 ? 0
928 : 1;
929 yearValue_ = parsed.value(QStringLiteral("year")).toString();
930 dateFromValue_ = parsed.value(QStringLiteral("dateFrom")).toString();
931 dateToValue_ = parsed.value(QStringLiteral("dateTo")).toString();
932 selectedPropertyIds_ =
933 parsed.value(QStringLiteral("propertyIdsNone")).toBool()
934 ? QVariantList{qstr(kUnassigned)}
935 : (parsed.value(QStringLiteral("propertyIds")).toList().isEmpty()
936 ? allPropertyIds()
937 : parsed.value(QStringLiteral("propertyIds")).toList());
938 selectedContractTypes_ =
939 parsed.value(QStringLiteral("contractTypesNone")).toBool()
940 ? QVariantList{qstr(kUnassigned)}
941 : (parsed.value(QStringLiteral("contractTypes")).toList().isEmpty()
942 ? allContractTypes()
943 : parsed.value(QStringLiteral("contractTypes")).toList());
944 allocatableMode_ =
945 nonEmptyString(parsed, QStringLiteral("allocatableMode"), qstr(kAll));
946
947 adjustmentAmountsById_ =
948 row.value(payload::keys::analysis::kAdjustments).toMap();
949 selectedAdjustmentTxIds_ = mapKeys(adjustmentAmountsById_);
950 exportFormat_ = normalizedExportFormat(
951 row.value(payload::keys::analysis::kExportFormat).toString(), uiType());
952 includeAdjustments_ =
954 .toBool();
955 snapshotTransactions_ =
957 filterEditMode_ = false;
958 refreshAnalysisResult();
959 refreshPreview();
960}
961
962void AnalysisViewModel::loadCreateState() {
963 name_.clear();
964 mainTypeIndex_ = 0;
965 activeResultType_ = qstr(kPlot);
966 plotSubtypeIndex_ = 0;
967 dateFieldIndex_ = 0;
968 dateModeIndex_ = defaultAnalysisDateMode() == QStringLiteral("year") ? 0 : 1;
969 yearValue_ = defaultAnalysisYear();
970 dateFromValue_.clear();
971 dateToValue_.clear();
972 selectedPropertyIds_ = allPropertyIds();
973 selectedContractTypes_ = allContractTypes();
974 allocatableMode_ = qstr(kAll);
975 filterEditMode_ = true;
976 filterContentIndex_ = 0;
977 exportFormat_.clear();
978 includeAdjustments_ = true;
979 snapshotTransactions_.clear();
980 currentAnalysisResult_.clear();
981 resetAdjustments();
982 refreshPreview();
983}
984
986 QVariantList rows;
987 rows.reserve(previewTransactions_.size());
988 const QString calculationLabel = adjustmentName_.trimmed().isEmpty()
989 ? tr("adjustment")
990 : adjustmentName_.trimmed();
991
992 for (const QVariant& value : previewTransactions_) {
993 const QVariantMap source = value.toMap();
994 const QString txId = source.value(QStringLiteral("id")).toString();
995 const double baseAmount =
996 numberFromVariant(source.value(QStringLiteral("amount")));
997 const bool hasAdjusted = includeAdjustments_ && !txId.isEmpty() &&
998 adjustmentAmountsById_.contains(txId);
999 const double adjustedAmount =
1000 hasAdjusted ? numberFromVariant(adjustmentAmountsById_.value(txId))
1001 : baseAmount;
1002 QString renderedAmount = amountText(baseAmount);
1003 if (hasAdjusted) {
1004 renderedAmount =
1005 QStringLiteral("%1 (%2 %3 %4)")
1006 .arg(amountText(baseAmount), amountText(adjustedAmount),
1007 tr("plus"), calculationLabel);
1008 }
1009
1010 rows.push_back(QVariantMap{
1011 {QStringLiteral("id"), txId},
1012 {QStringLiteral("statementName"),
1013 source.value(QStringLiteral("statementName")).toString()},
1014 {QStringLiteral("transactionName"),
1015 source.value(QStringLiteral("transactionName")).toString()},
1016 {QStringLiteral("date"),
1017 source.value(QStringLiteral("date")).toString()},
1018 {QStringLiteral("valuta"),
1019 source.value(QStringLiteral("valuta")).toString()},
1020 {QStringLiteral("actorName"),
1021 source.value(QStringLiteral("actorName")).toString()},
1022 {QStringLiteral("contractName"),
1023 source.value(QStringLiteral("contractName")).toString()},
1024 {QStringLiteral("contractType"),
1025 nonEmptyString(source, QStringLiteral("contractType"),
1026 tr("Unassigned"))},
1027 {QStringLiteral("propertiesLabel"),
1028 source.value(QStringLiteral("propertiesLabel")).toString()},
1029 {QStringLiteral("amountText"), renderedAmount}});
1030 }
1031 return rows;
1032}
1033
1035 const int count =
1036 previewMetrics_.value(QStringLiteral("statementCount")).toInt();
1037 return tr("Statements: %1").arg(count);
1038}
1039
1041 const int count =
1042 previewMetrics_.value(QStringLiteral("transactionCount")).toInt();
1043 return tr("Transactions: %1").arg(count);
1044}
1045
1047 const double amount =
1048 numberFromVariant(previewMetrics_.value(QStringLiteral("amountSum")));
1049 return tr("Amount sum: %1").arg(amountText(amount));
1050}
1051
1052void AnalysisViewModel::refreshPreview() {
1053 if (!analysisWorkflow_) {
1054 return;
1055 }
1056 const QVariantMap preview =
1057 analysisWorkflow_->previewTransactions(currentFilterSpec());
1058 previewTransactions_ = preview.value(QStringLiteral("transactions")).toList();
1059 previewMetrics_ = preview.value(QStringLiteral("metrics")).toMap();
1060 emitChanged();
1061}
1062
1063void AnalysisViewModel::requestPreviewRefresh() {
1064 previewDebounce_.start();
1065}
1066
1067QString AnalysisViewModel::currentResultType() const {
1068 const QVariantMap row = analysisRowById(selectedAnalysisId());
1069 const QString type = normalizedType(
1070 nonEmptyString(row, QStringLiteral("type"), activeResultType_));
1071 return type.isEmpty() ? activeResultType_ : type;
1072}
1073
1075 return currentResultType() == qstr(kTab);
1076}
1077
1078QString AnalysisViewModel::renderedPreviewSourceFromResult(
1079 const QVariantMap& analysisResult) const {
1080 const QVariantList artifacts =
1081 analysisResult.value(QStringLiteral("artifacts")).toList();
1082 if (artifacts.isEmpty()) {
1083 return {};
1084 }
1085
1086 QString renderedSource;
1087 const QString rawPath = artifacts.first().toString().trimmed();
1088 if (rawPath.startsWith(QStringLiteral("file:")) ||
1089 rawPath.startsWith(QStringLiteral("qrc:")) ||
1090 rawPath.startsWith(QStringLiteral("http:")) ||
1091 rawPath.startsWith(QStringLiteral("https:")) ||
1092 rawPath.startsWith(QStringLiteral("data:"))) {
1093 renderedSource = rawPath;
1094 } else if (!rawPath.isEmpty()) {
1095 renderedSource = QUrl::fromLocalFile(rawPath).toString();
1096 }
1097 if (renderedSource.isEmpty() ||
1098 renderedSource.startsWith(QStringLiteral("data:")) ||
1099 renderedSource.startsWith(QStringLiteral("qrc:"))) {
1100 return renderedSource;
1101 }
1102
1103 QUrl previewUrl(renderedSource);
1104 previewUrl.setQuery(QStringLiteral("v=%1").arg(renderedPreviewRevision_));
1105 return previewUrl.toString();
1106}
1107
1109 refreshAdjustmentAmountsFromSelection();
1110 refreshAnalysisResult();
1111 updateResultState();
1112 emitChanged();
1113}
1114
1115void AnalysisViewModel::refreshAdjustmentAmountsFromSelection() {
1116 if (!analysisWorkflow_ || selectedAdjustmentTxIds_.isEmpty()) {
1117 return;
1118 }
1119 adjustmentAmountsById_ =
1121 previewTransactions_, stringList(selectedAdjustmentTxIds_),
1122 adjustmentPercentText_);
1123}
1124
1125void AnalysisViewModel::refreshAnalysisResult() {
1126 if (!selectors_ || !analysisWorkflow_ || selectedAnalysisId().isEmpty()) {
1127 return;
1128 }
1129 const QVariantMap result = analysisWorkflow_->computeAnalysisPreview(
1130 selectedAnalysisId(), currentFilterSpec(), includeAdjustments_,
1131 adjustmentAmountsById_);
1132 currentAnalysisResult_ = result;
1133}
1134
1135void AnalysisViewModel::updateResultState() {
1136 renderedPreviewSource_.clear();
1137 tableContractTypes_.clear();
1138 tablePropertyRows_.clear();
1139 tableGrandTotal_ = 0.0;
1140
1141 const QVariantMap result = currentAnalysisResult_;
1142 if (!analysisWorkflow_ || result.isEmpty()) {
1143 return;
1144 }
1145
1146 ++renderedPreviewRevision_;
1147 renderedPreviewSource_ = renderedPreviewSourceFromResult(result);
1148 const QVariantMap table = result.value(QStringLiteral("tableState")).toMap();
1149 tableContractTypes_ = table.value(QStringLiteral("contractTypes")).toList();
1150 tablePropertyRows_ = table.value(QStringLiteral("propertyRows")).toList();
1151 tableGrandTotal_ =
1152 numberFromVariant(table.value(QStringLiteral("grandTotal")));
1153}
1154
1155} // namespace ui
Declares the QML API for the Analysis view.
Declares the UI workflow for analysis creation and computation.
Declarations for the UI PayloadKeys component.
Declares the centralized UI settings view model exposed to QML.
Declarations for the UI Trace component.
Declares the workspace selection state synchronized with the store.
Declares read-only workspace selectors for QML payloads.
Declares the UI workspace snapshot store.
void setYearValue(const QString &value)
Updates the year filter field.
Q_INVOKABLE void selectAnalysis(const QString &id)
Selects an analysis row.
void setAdjustmentName(const QString &value)
Updates the adjustment name field.
QVariantList exportFormatOptions
void setDateFromValue(const QString &value)
Updates the start date filter field.
Q_INVOKABLE void selectAllContractTypes()
Selects all available contract types.
void setIncludeAdjustments(bool value)
Updates whether adjustment amounts are included.
void setDateModeIndex(int value)
Updates the selected date mode index.
void setAdjustmentPercentText(const QString &value)
Updates the adjustment percent field.
void setSelectedPropertyIds(const QVariantList &value)
Updates selected property ids for filtering.
void setSelectedContractTypes(const QVariantList &value)
Updates selected contract type keys for filtering.
Q_INVOKABLE void selectAllProperties()
Selects all available properties.
void setAllocatableMode(const QString &value)
Updates the selected allocatable filter mode.
Q_INVOKABLE void selectNoProperties()
Clears the property selection.
Q_INVOKABLE void navigate(int delta)
Navigates between analyses.
void setFilterEditMode(bool value)
Updates whether filter editing is shown in create mode.
void setName(const QString &value)
Updates the analysis name field.
Q_INVOKABLE void submitCreate()
Creates an analysis through workspace.
Q_INVOKABLE void setAllocatableModeIndex(int index)
Updates allocatable mode from a segmented control index.
void setSelectedAdjustmentTxIds(const QVariantList &value)
Updates transaction ids selected for adjustment.
void setPlotSubtypeIndex(int value)
Updates the selected plot subtype index.
void setDateFieldIndex(int value)
Updates the selected date field index.
void setSettings(Settings *value)
Sets the settings source used for analysis defaults.
Q_INVOKABLE void applySelectedAdjustment()
Applies the entered adjustment to selected preview transactions.
Q_INVOKABLE void setAdjustmentTransactionSelected(const QString &id, bool selected)
Updates whether a preview transaction is selected for adjustment.
void setExportFormatIndex(int value)
Updates the selected export format index.
QString previewTransactionCountText READ previewTransactionCountText
Q_INVOKABLE void setPropertySelected(const QString &id, bool selected)
Updates whether a property is selected for filtering.
AnalysisViewModel(QObject *parent=nullptr)
Creates the analysis view model.
void setAnalysisWorkflow(AnalysisWorkflow *value)
Sets the workflow used for analysis computation and rendering.
void setDateToValue(const QString &value)
Updates the end date filter field.
Q_INVOKABLE void submitUpdate()
Updates the selected analysis through workspace.
QVariantList previewTransactionRows
Q_INVOKABLE void clearFilters()
Resets all filter fields and refreshes the preview.
Q_INVOKABLE void setContractTypeSelected(const QString &type, bool selected)
Updates whether a contract type is selected for filtering.
Q_INVOKABLE void selectNoContractTypes()
Clears the contract type selection.
Q_INVOKABLE void deleteCurrent()
Deletes the selected analysis through workspace.
Q_INVOKABLE void refreshFromSelection()
Reloads create or edit state from the workspace selection.
void setMainTypeIndex(int value)
Updates the selected main analysis type index.
void setWorkspaceRoles(WorkspaceStore *store, WorkspaceCommands *commands, WorkspaceSelection *selection, WorkspaceSelectors *selectors)
Sets workspace roles used for analysis CRUD, rows and selection.
Q_INVOKABLE void toggleFilterContent()
Toggles between filter fields and transaction preview.
Coordinates analysis preview and computation use cases for UI state.
QVariantMap previewTransactions(const QString &filterSpec) const
Builds a transaction preview for the current filter.
QVariantMap analysisAdjustmentAmountsFromPercentText(const QVariantList &transactions, const QStringList &selectedTransactionIds, const QString &percentText) const
Builds adjustment amounts from selected transactions and percent text.
Stores and exposes globally persisted UI settings for settings-related views.
Definition Settings.h:19
Maps UI intent to core workspace writer commands.
Tracks the currently selected workspace entities.
void selectedAnalysisIdChanged()
Emitted when the selected analysis id changed.
QString selectedAnalysisId() const
Returns the selected analysis id.
Projects workspace store data into QML-facing read models.
QVariantList analysisRows() const
Returns analysis rows for QML views.
Owns the current UI-side workspace snapshot and revision lifecycle.
void dataRevisionChanged()
constexpr std::string_view kAllocatable
constexpr std::string_view kUnassigned
constexpr auto kName
Definition Trace.h:33
constexpr auto kId
Definition Trace.h:30
void traceViewModel(const char *origin, std::string message, core::errors::ErrorContext context={})
Emits a view model trace event.
Definition Trace.h:97
Declares UI-side workspace command submission.