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";
40constexpr auto kAll =
"all";
45QString qstr(
const char* value) {
46 return QString::fromLatin1(value);
49QString rowIdAt(
const QVariantList& rows,
int index,
50 const QString& idKey = QStringLiteral(
"id")) {
51 if (index < 0 || index >= rows.size()) {
54 return rows.at(index).toMap().value(idKey).toString();
57int indexOfId(
const QVariantList& rows,
const QString&
id,
58 const QString& idKey = QStringLiteral(
"id")) {
62 for (
int i = 0; i < rows.size(); ++i) {
63 if (rows.at(i).toMap().value(idKey).toString() ==
id) {
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{};
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) {
85 return rowIdAt(rows, 0, idKey);
88 return rowIdAt(rows, rows.size() - 1, idKey);
90 return rowIdAt(rows, defaultIndex, idKey);
93 return currentIndex >= rows.size() - 1
95 : rowIdAt(rows, currentIndex + 1, idKey);
98 return currentIndex <= 0 ? QString()
99 : rowIdAt(rows, currentIndex - 1, idKey);
101 return rowIdAt(rows, currentIndex, idKey);
104QString deleteNextSelectionId(
const QVariantList& rows,
105 const QString& removedId,
int defaultIndex = 0,
106 const QString& idKey = QStringLiteral(
"id")) {
107 if (rows.isEmpty()) {
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);
116double numberFromVariant(
const QVariant& value) {
118 const double out = value.toDouble(&ok);
119 return ok && std::isfinite(out) ? out : 0.0;
122QString amountText(
double value) {
123 return QString::number(value,
'f', 2);
126QString amountText(
const QVariant& value) {
127 return amountText(numberFromVariant(value));
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;
136QVariantList mapKeys(
const QVariantMap& value) {
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());
147QString normalizedType(
const QString& value) {
148 const QString trimmed = value.trimmed().toLower();
149 return trimmed == qstr(kTabular) ? qstr(kTab) : trimmed;
152QString plotTypeFromSubtypeIndex(
int plotSubtypeIndex) {
153 return plotSubtypeIndex == 1 ? qstr(kHistogram) : qstr(
kPie);
156bool containsPropertyId(
const QVariantList& rows,
const QString&
id) {
157 for (
const QVariant& value : rows) {
158 if (value.toMap().value(QStringLiteral(
"id")).toString() ==
id) {
165QString rowKey(
const QVariant& value,
const QString& idField) {
166 if (!idField.isEmpty()) {
167 return value.toMap().value(idField).toString();
169 const QVariantMap row = value.toMap();
170 if (row.contains(QStringLiteral(
"value"))) {
171 return row.value(QStringLiteral(
"value")).toString();
173 return value.toString();
176bool containsRowValue(
const QVariantList& rows,
const QString& value) {
177 for (
const QVariant& row : rows) {
178 if (rowKey(row, {}) == value) {
185void appendContractTypeRow(QVariantList& rows,
const QString& value,
186 const QString& label) {
187 if (value.isEmpty() || containsRowValue(rows, value)) {
190 rows.push_back(QVariantMap{{QStringLiteral(
"value"), value},
191 {QStringLiteral(
"label"), label}});
197 previewDebounce_.setInterval(kPreviewDebounceMs);
198 previewDebounce_.setSingleShot(
true);
199 connect(&previewDebounce_, &QTimer::timeout,
this,
200 &AnalysisViewModel::refreshPreview);
207 if (store_ == store && commands_ == commands && selection_ == selection &&
208 selectors_ == selectors) {
211 bindWorkspaceRoles(store, commands, selection, selectors);
217 if (analysisWorkflow_ == value) {
220 analysisWorkflow_ = value;
226 if (settings_ == value) {
244 loadSelectedAnalysis();
249void AnalysisViewModel::emitChanged() {
253void AnalysisViewModel::bindWorkspaceRoles(WorkspaceStore* store,
254 WorkspaceCommands* commands,
255 WorkspaceSelection* selection,
256 WorkspaceSelectors* selectors) {
258 disconnect(store_,
nullptr,
this,
nullptr);
261 disconnect(selection_,
nullptr,
this,
nullptr);
264 commands_ = commands;
265 selection_ = selection;
266 selectors_ = selectors;
267 if (!store_ || !selection_) {
277 if (!analysisWorkflow_ || !commands_ || !selection_ || !
canSubmit()) {
281 "AnalysisViewModel::submitCreate",
"Analysis create submitted",
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()) {
297 selection_->selectAnalysis(newId);
298 selectedAdjustmentTxIds_ = mapKeys(adjustmentAmountsById_);
299 refreshAnalysisResult();
300 filterEditMode_ =
false;
310 "AnalysisViewModel::submitUpdate",
"Analysis update submitted",
313 const QString type = strategyType();
314 activeResultType_ = type;
315 commands_->updateAnalysis(
317 currentAnalysisFilter(), normalizedExportFormat(exportFormat_, uiType()),
318 includeAdjustments_, snapshotTransactions_, adjustmentAmountsById_);
319 selectedAdjustmentTxIds_ = mapKeys(adjustmentAmountsById_);
320 refreshAnalysisResult();
321 filterEditMode_ =
false;
327 if (removedId.isEmpty() || !commands_ || !selection_) {
331 "AnalysisViewModel::deleteCurrent",
"Analysis delete submitted",
333 const int currentIndex = indexOfId(
analysisRows(), removedId);
334 commands_->deleteAnalysis(removedId);
335 selection_->selectAnalysis(
336 deleteNextSelectionId(
analysisRows(), removedId, currentIndex));
346 selection_->selectAnalysis(
352 selection_->selectAnalysis(
id);
361 return selectors_ ? selectors_->
analysisRows() : QVariantList();
364QVariantMap AnalysisViewModel::analysisRowById(
const QString&
id)
const {
368QStringList AnalysisViewModel::stringList(
const QVariantList& values)
const {
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);
380QVariantList AnalysisViewModel::allPropertyIds()
const {
382 for (
const QVariant& value : propertyFilterRows_) {
383 const QString
id = value.toMap().value(QStringLiteral(
"id")).toString();
391QVariantList AnalysisViewModel::allContractTypes()
const {
393 for (
const QVariant& value : contractTypeRows_) {
394 const QString type = rowKey(value, {});
395 if (!type.isEmpty()) {
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);
415 for (
const QVariant& value : values) {
416 const QString key = value.toString();
417 if (!key.isEmpty() && available.contains(key)) {
424bool AnalysisViewModel::isAllSelected(
const QVariantList& selectedIds,
425 const QVariantList& availableRows,
426 const QString& idField)
const {
427 if (availableRows.isEmpty()) {
430 QSet<QString> selected;
431 for (
const QVariant& value : selectedIds) {
432 const QString key = value.toString();
433 if (!key.isEmpty()) {
434 selected.insert(key);
437 if (selected.size() != availableRows.size()) {
440 for (
const QVariant& value : availableRows) {
441 const QString key = rowKey(value, idField);
442 if (key.isEmpty() || !selected.contains(key)) {
449void AnalysisViewModel::refreshFilterRows() {
450 const bool propertyWasAll = isAllSelected(
451 selectedPropertyIds_, propertyFilterRows_, QStringLiteral(
"id"));
452 const bool contractTypesWereAll =
453 isAllSelected(selectedContractTypes_, contractTypeRows_);
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")},
463 selectedPropertyIds_ =
464 propertyWasAll ? allPropertyIds()
465 : pruneSelection(selectedPropertyIds_, propertyFilterRows_,
466 QStringLiteral(
"id"));
469 if (analysisWorkflow_) {
470 for (
const QString& type : analysisWorkflow_->contractTypes()) {
471 appendContractTypeRow(types, type, type);
474 appendContractTypeRow(types, qstr(kUnassigned), tr(
"Unassigned"));
475 contractTypeRows_ = types;
476 selectedContractTypes_ =
479 : pruneSelection(selectedContractTypes_, contractTypeRows_);
482QString AnalysisViewModel::currentDateField()
const {
483 return dateFieldIndex_ == 1 ? QStringLiteral(
"valuta")
484 : QStringLiteral(
"bookingDate");
487QString AnalysisViewModel::currentDateMode()
const {
488 return dateModeIndex_ == 0 ? QStringLiteral(
"year") : QStringLiteral(
"range");
491QString AnalysisViewModel::defaultAnalysisDateMode()
const {
493 const QString mode = settings_->analysisDefaultDateMode().toLower();
494 return mode == QStringLiteral(
"range") ? QStringLiteral(
"range")
495 : QStringLiteral(
"year");
497 return QStringLiteral(
"year");
500QString AnalysisViewModel::defaultAnalysisYear()
const {
501 if (settings_ && settings_->analysisDefaultYear() > 0) {
502 return QString::number(settings_->analysisDefaultYear());
504 return QString::number(QDate::currentDate().year() - 1);
507QStringList AnalysisViewModel::effectiveSelectedPropertyIds()
const {
508 if (propertyFilterRows_.isEmpty() ||
509 isAllSelected(selectedPropertyIds_, propertyFilterRows_,
510 QStringLiteral(
"id"))) {
513 return selectedPropertyIds_.isEmpty() ? QStringList{}
514 : stringList(selectedPropertyIds_);
517QStringList AnalysisViewModel::effectiveSelectedContractTypes()
const {
518 if (contractTypeRows_.isEmpty() ||
519 isAllSelected(selectedContractTypes_, contractTypeRows_)) {
522 return selectedContractTypes_.isEmpty() ? QStringList{}
523 : stringList(selectedContractTypes_);
526QString AnalysisViewModel::currentFilterSpec()
const {
527 if (!analysisWorkflow_) {
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
539QVariantMap AnalysisViewModel::currentAnalysisConfig()
const {
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},
550QVariantMap AnalysisViewModel::currentAnalysisFilter()
const {
551 const QString mode = allocatableMode_.toLower();
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
570void AnalysisViewModel::resetAdjustments() {
571 selectedAdjustmentTxIds_.clear();
572 adjustmentAmountsById_.clear();
573 adjustmentName_.clear();
574 adjustmentPercentText_.clear();
578 if (filterEditMode_ == value) {
581 filterEditMode_ = value;
586 const int next = value == 1 ? 1 : 0;
587 if (dateFieldIndex_ == next) {
590 dateFieldIndex_ = next;
591 requestPreviewRefresh();
596 const int next = value == 1 ? 1 : 0;
597 if (dateModeIndex_ == next) {
600 dateModeIndex_ = next;
601 requestPreviewRefresh();
606 if (yearValue_ == value) {
610 requestPreviewRefresh();
615 if (dateFromValue_ == value) {
618 dateFromValue_ = value;
619 requestPreviewRefresh();
624 if (dateToValue_ == value) {
627 dateToValue_ = value;
628 requestPreviewRefresh();
633 if (selectedPropertyIds_ == value) {
636 selectedPropertyIds_ = value;
637 requestPreviewRefresh();
642 if (selectedContractTypes_ == value) {
645 selectedContractTypes_ = value;
646 requestPreviewRefresh();
651 const QString lower = value.trimmed().toLower();
653 lower == qstr(kAllocatable) || lower == qstr(kNonAllocatable)
656 if (allocatableMode_ == next) {
659 allocatableMode_ = next;
660 requestPreviewRefresh();
665 const QString trimmed =
id.trimmed();
666 if (trimmed.isEmpty()) {
669 QVariantList next = selectedPropertyIds_;
670 if (selected && !next.contains(trimmed)) {
671 next.push_back(trimmed);
674 next.removeAll(trimmed);
689 const QString trimmed = type.trimmed();
690 if (trimmed.isEmpty()) {
693 QVariantList next = selectedContractTypes_;
694 if (selected && !next.contains(trimmed)) {
695 next.push_back(trimmed);
698 next.removeAll(trimmed);
713 const QString trimmed =
id.trimmed();
714 if (trimmed.isEmpty()) {
717 QVariantList next = selectedAdjustmentTxIds_;
718 if (selected && !next.contains(trimmed)) {
719 next.push_back(trimmed);
722 next.removeAll(trimmed);
740 filterContentIndex_ = filterContentIndex_ == 0 ? 1 : 0;
747 yearValue_ = defaultAnalysisYear();
748 dateFromValue_.clear();
749 dateToValue_.clear();
750 selectedPropertyIds_ = allPropertyIds();
751 selectedContractTypes_ = allContractTypes();
752 allocatableMode_ = qstr(kAll);
759 if (name_ == value) {
767 const int next = value == 1 ? 1 : 0;
768 if (mainTypeIndex_ == next) {
771 mainTypeIndex_ = next;
772 activeResultType_ = strategyType();
773 exportFormat_ = normalizedExportFormat(exportFormat_, uiType());
778 const int next = value == 1 ? 1 : 0;
779 if (plotSubtypeIndex_ == next) {
782 plotSubtypeIndex_ = next;
787 return {QVariantMap{{QStringLiteral(
"value"), qstr(kPie)},
788 {QStringLiteral(
"label"), tr(
"Pie chart")}},
789 QVariantMap{{QStringLiteral(
"value"), qstr(kHistogram)},
790 {QStringLiteral(
"label"), tr(
"Histogram")}}};
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")}}};
800 return {QVariantMap{{QStringLiteral(
"value"), qstr(kPng)},
801 {QStringLiteral(
"label"), tr(
"PNG")}},
802 QVariantMap{{QStringLiteral(
"value"), qstr(kJpg)},
803 {QStringLiteral(
"label"), tr(
"JPG")}}};
806void AnalysisViewModel::setExportFormat(
const QString& value) {
807 const QString next = normalizedExportFormat(value, uiType());
808 if (exportFormat_ == next) {
811 exportFormat_ = next;
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() ==
829 if (options.isEmpty()) {
832 const int index = std::clamp(value, 0,
static_cast<int>(options.size()) - 1);
834 options.at(index).toMap().value(QStringLiteral(
"value")).toString());
838 if (includeAdjustments_ == value) {
841 includeAdjustments_ = value;
842 refreshAnalysisResult();
848 if (selectedAdjustmentTxIds_ == value) {
851 selectedAdjustmentTxIds_ = value;
856 if (adjustmentName_ == value) {
859 adjustmentName_ = value;
864 if (adjustmentPercentText_ == value) {
867 adjustmentPercentText_ = value;
872 return !name_.trimmed().isEmpty() && !selectedPropertyIds_.isEmpty() &&
873 !selectedContractTypes_.isEmpty();
876QString AnalysisViewModel::uiType()
const {
877 return mainTypeIndex_ == 1 ? qstr(kTable) : qstr(kPlot);
880QString AnalysisViewModel::strategyType()
const {
881 return uiType() == qstr(kTable) ? qstr(kTab) : qstr(kPlot);
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();
894void AnalysisViewModel::loadSelectedAnalysis() {
906 name_ = row.value(QStringLiteral(
"name")).toString();
908 normalizedType(row.value(QStringLiteral(
"type"), qstr(kPlot)).toString());
909 mainTypeIndex_ = type == qstr(kTab) ? 1 : 0;
910 activeResultType_ = type.isEmpty() ? qstr(kPlot) : type;
912 const QVariantMap config =
915 config.value(QStringLiteral(
"plotType")).toString() == qstr(kHistogram)
919 const QVariantMap parsed =
921 dateFieldIndex_ = parsed.value(QStringLiteral(
"dateField")).toString() ==
922 QStringLiteral(
"valuta")
925 dateModeIndex_ = parsed.value(QStringLiteral(
"dateMode")).toString() ==
926 QStringLiteral(
"year")
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()
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()
943 : parsed.value(QStringLiteral(
"contractTypes")).toList());
945 nonEmptyString(parsed, QStringLiteral(
"allocatableMode"), qstr(kAll));
947 adjustmentAmountsById_ =
949 selectedAdjustmentTxIds_ = mapKeys(adjustmentAmountsById_);
950 exportFormat_ = normalizedExportFormat(
952 includeAdjustments_ =
955 snapshotTransactions_ =
957 filterEditMode_ =
false;
958 refreshAnalysisResult();
962void AnalysisViewModel::loadCreateState() {
965 activeResultType_ = qstr(kPlot);
966 plotSubtypeIndex_ = 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();
987 rows.reserve(previewTransactions_.size());
988 const QString calculationLabel = adjustmentName_.trimmed().isEmpty()
990 : adjustmentName_.trimmed();
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))
1002 QString renderedAmount = amountText(baseAmount);
1005 QStringLiteral(
"%1 (%2 %3 %4)")
1006 .arg(amountText(baseAmount), amountText(adjustedAmount),
1007 tr(
"plus"), calculationLabel);
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"),
1027 {QStringLiteral(
"propertiesLabel"),
1028 source.value(QStringLiteral(
"propertiesLabel")).toString()},
1029 {QStringLiteral(
"amountText"), renderedAmount}});
1036 previewMetrics_.value(QStringLiteral(
"statementCount")).toInt();
1037 return tr(
"Statements: %1").arg(count);
1042 previewMetrics_.value(QStringLiteral(
"transactionCount")).toInt();
1043 return tr(
"Transactions: %1").arg(count);
1047 const double amount =
1048 numberFromVariant(previewMetrics_.value(QStringLiteral(
"amountSum")));
1049 return tr(
"Amount sum: %1").arg(amountText(amount));
1052void AnalysisViewModel::refreshPreview() {
1053 if (!analysisWorkflow_) {
1056 const QVariantMap preview =
1058 previewTransactions_ = preview.value(QStringLiteral(
"transactions")).toList();
1059 previewMetrics_ = preview.value(QStringLiteral(
"metrics")).toMap();
1063void AnalysisViewModel::requestPreviewRefresh() {
1064 previewDebounce_.start();
1067QString AnalysisViewModel::currentResultType()
const {
1069 const QString type = normalizedType(
1070 nonEmptyString(row, QStringLiteral(
"type"), activeResultType_));
1071 return type.isEmpty() ? activeResultType_ : type;
1075 return currentResultType() == qstr(kTab);
1078QString AnalysisViewModel::renderedPreviewSourceFromResult(
1079 const QVariantMap& analysisResult)
const {
1080 const QVariantList artifacts =
1081 analysisResult.value(QStringLiteral(
"artifacts")).toList();
1082 if (artifacts.isEmpty()) {
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();
1097 if (renderedSource.isEmpty() ||
1098 renderedSource.startsWith(QStringLiteral(
"data:")) ||
1099 renderedSource.startsWith(QStringLiteral(
"qrc:"))) {
1100 return renderedSource;
1103 QUrl previewUrl(renderedSource);
1104 previewUrl.setQuery(QStringLiteral(
"v=%1").arg(renderedPreviewRevision_));
1105 return previewUrl.toString();
1109 refreshAdjustmentAmountsFromSelection();
1110 refreshAnalysisResult();
1111 updateResultState();
1115void AnalysisViewModel::refreshAdjustmentAmountsFromSelection() {
1116 if (!analysisWorkflow_ || selectedAdjustmentTxIds_.isEmpty()) {
1119 adjustmentAmountsById_ =
1121 previewTransactions_, stringList(selectedAdjustmentTxIds_),
1122 adjustmentPercentText_);
1125void AnalysisViewModel::refreshAnalysisResult() {
1129 const QVariantMap result = analysisWorkflow_->computeAnalysisPreview(
1131 adjustmentAmountsById_);
1132 currentAnalysisResult_ = result;
1135void AnalysisViewModel::updateResultState() {
1136 renderedPreviewSource_.clear();
1137 tableContractTypes_.clear();
1138 tablePropertyRows_.clear();
1139 tableGrandTotal_ = 0.0;
1141 const QVariantMap result = currentAnalysisResult_;
1142 if (!analysisWorkflow_ || result.isEmpty()) {
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();
1152 numberFromVariant(table.value(QStringLiteral(
"grandTotal")));
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
QString previewStatementCountText
QVariantList plotTypeOptions
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.
QString previewAmountSumText
bool currentResultIsTable
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.
QVariantList analysisRows
QString selectedAnalysisId
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.
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 std::string_view kHistogram
constexpr std::string_view kPie
void traceViewModel(const char *origin, std::string message, core::errors::ErrorContext context={})
Emits a view model trace event.
const auto kSnapshotTransactions
const auto kIncludeCalcAdjustments
const auto kNonAllocatable
Declares UI-side workspace command submission.