FOSSredder 0.5.0
Loading...
Searching...
No Matches
AnnualViewModel.cpp
Go to the documentation of this file.
1
5
7
8#include <algorithm>
9#include <cmath>
10
11#include <QDate>
12#include <QMetaType>
13#include <QSet>
14
15#include "ui/i18n/Text.h"
23
24namespace ui {
25
26namespace {
27
28constexpr int kYearMin = 1900;
29constexpr int kYearMax = 2500;
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 kDeduplicated = "deduplicated";
37constexpr auto kSimilar = "similar";
38constexpr auto kDivergent = "divergent";
39constexpr auto kWorkspaceOnly = "workspaceOnly";
40constexpr auto kMissingLive = "missingLive";
41
42QString qstr(const char* value) {
43 return QString::fromLatin1(value);
44}
45
46QString rowIdAt(const QVariantList& rows, int index,
47 const QString& idKey = QStringLiteral("id")) {
48 if (index < 0 || index >= rows.size()) {
49 return {};
50 }
51 return rows.at(index).toMap().value(idKey).toString();
52}
53
54int indexOfId(const QVariantList& rows, const QString& id,
55 const QString& idKey = QStringLiteral("id")) {
56 if (id.isEmpty()) {
57 return -1;
58 }
59 for (int i = 0; i < rows.size(); ++i) {
60 if (rows.at(i).toMap().value(idKey).toString() == id) {
61 return i;
62 }
63 }
64 return -1;
65}
66
67QVariantMap rowById(const QVariantList& rows, const QString& id,
68 const QString& idKey = QStringLiteral("id")) {
69 const int index = indexOfId(rows, id.trimmed(), idKey);
70 return index >= 0 ? rows.at(index).toMap() : QVariantMap{};
71}
72
73QString navigatedSelectionId(const QVariantList& rows, const QString& currentId,
74 int delta, int defaultIndex = 0,
75 const QString& idKey = QStringLiteral("id")) {
76 const int currentIndex = indexOfId(rows, currentId, idKey);
77 if (currentIndex < 0) {
78 if (rows.isEmpty()) {
79 return {};
80 }
81 if (delta > 0) {
82 return rowIdAt(rows, 0, idKey);
83 }
84 if (delta < 0) {
85 return rowIdAt(rows, rows.size() - 1, idKey);
86 }
87 return rowIdAt(rows, defaultIndex, idKey);
88 }
89 if (delta > 0) {
90 return currentIndex >= rows.size() - 1
91 ? QString()
92 : rowIdAt(rows, currentIndex + 1, idKey);
93 }
94 if (delta < 0) {
95 return currentIndex <= 0 ? QString()
96 : rowIdAt(rows, currentIndex - 1, idKey);
97 }
98 return rowIdAt(rows, currentIndex, idKey);
99}
100
101QString deleteNextSelectionId(const QVariantList& rows,
102 const QString& removedId, int defaultIndex = 0,
103 const QString& idKey = QStringLiteral("id")) {
104 if (rows.isEmpty()) {
105 return {};
106 }
107 const int removedIndex = indexOfId(rows, removedId, idKey);
108 const int nextIndex = removedIndex >= 0 ? removedIndex + 1 : defaultIndex;
109 const int wrapped = nextIndex % rows.size();
110 return rowIdAt(rows, wrapped < 0 ? wrapped + rows.size() : wrapped, idKey);
111}
112
113double numberFromVariant(const QVariant& value) {
114 bool ok = false;
115 const double out = value.toDouble(&ok);
116 return ok && std::isfinite(out) ? out : 0.0;
117}
118
119QString amountText(double value) {
120 return QString::number(value, 'f', 2);
121}
122
123QString amountText(const QVariant& value) {
124 return amountText(numberFromVariant(value));
125}
126
127QString nonEmptyString(const QVariantMap& map, const QString& key,
128 const QString& defaultValue = {}) {
129 const QString value = map.value(key).toString();
130 return value.isEmpty() ? defaultValue : value;
131}
132} // namespace
133
134AnnualViewModel::AnnualViewModel(QObject* parent) : QObject(parent) {
135 sectionExpanded_.insert(qstr(kDeduplicated), true);
136 sectionExpanded_.insert(qstr(kSimilar), true);
137 sectionExpanded_.insert(qstr(kDivergent), true);
138 sectionExpanded_.insert(qstr(kWorkspaceOnly), true);
139 sectionExpanded_.insert(qstr(kMissingLive), true);
140 verificationIssues_ = emptyIssues();
141 statusMetrics_ = emptyStatusMetrics();
142 year_ = defaultYear();
143}
144
146 WorkspaceCommands* commands,
147 WorkspaceSelection* selection,
148 WorkspaceSelectors* selectors) {
149 if (store_ == store && commands_ == commands && selection_ == selection &&
150 selectors_ == selectors) {
151 return;
152 }
153 bindWorkspaceRoles(store, commands, selection, selectors);
155 emitChanged();
156}
157
159 if (annualWorkflow_ == value) {
160 return;
161 }
162 annualWorkflow_ = value;
163 lastPreviewRevision_ = -1;
165 emitChanged();
166}
167
169 return !selectedAnnualId().isEmpty();
170}
171
172QVariantList AnnualViewModel::annualRows() const {
173 return selectors_ ? selectors_->annualRows() : QVariantList();
174}
175
177 return selection_ ? selection_->selectedAnnualId() : QString();
178}
179
180void AnnualViewModel::setName(const QString& value) {
181 if (name_ == value) {
182 return;
183 }
184 name_ = value;
185 emitChanged();
186}
187
189 const int next = std::clamp(value, kYearMin, kYearMax);
190 if (year_ == next) {
191 return;
192 }
193 year_ = next;
194 rebuildAnnualResultState();
195 emitChanged();
196}
197
199 return !annualRows().isEmpty();
200}
201
203 if (!isEdit()) {
204 return canSubmit();
205 }
206 QStringList current = analysisIds_;
207 QStringList saved = savedAnalysisIds_;
208 current.sort();
209 saved.sort();
210 return savedName_ != name_ || savedYear_ != year_ || saved != current ||
211 analysisMetadataDirty_;
212}
213
215 loadSelectedAnnual();
216 rebuildAnnualResultState();
217 emitChanged();
218}
219
221 if (isEdit() && selection_) {
222 selection_->selectAnnual({});
223 }
224 loadCreateState();
225 rebuildAnnualResultState();
226 emitChanged();
227}
228
230 contentIndex_ = contentIndex_ == 0 ? 1 : 0;
231 emitChanged();
232}
233
235 setYear(year_ + delta);
236}
237
238int AnnualViewModel::workspaceRevision() const {
239 return selectors_ ? selectors_->dataRevision() : -1;
240}
241
242void AnnualViewModel::bindWorkspaceRoles(WorkspaceStore* store,
243 WorkspaceCommands* commands,
244 WorkspaceSelection* selection,
245 WorkspaceSelectors* selectors) {
246 if (store_) {
247 disconnect(store_, nullptr, this, nullptr);
248 }
249 if (selection_) {
250 disconnect(selection_, nullptr, this, nullptr);
251 }
252 store_ = store;
253 commands_ = commands;
254 selection_ = selection;
255 selectors_ = selectors;
256 if (!store_ || !selection_) {
257 return;
258 }
259 connect(selection_, &WorkspaceSelection::selectedAnnualIdChanged, this,
261 connect(store_, &WorkspaceStore::dataRevisionChanged, this,
263}
264
265void AnnualViewModel::emitChanged() {
266 emit changed();
267}
268
269QStringList
270AnnualViewModel::normalizedAnalysisIds(const QVariant& values) const {
271 QStringList out;
272 QSet<QString> seen;
273 const QVariantList list = values.toList();
274 if (!list.isEmpty()) {
275 for (const QVariant& value : list) {
276 const QString id = analysisIdFromVariant(value);
277 if (id.isEmpty() || seen.contains(id)) {
278 continue;
279 }
280 seen.insert(id);
281 out.push_back(id);
282 }
283 return out;
284 }
285 const QString single = analysisIdFromVariant(values);
286 if (!single.isEmpty()) {
287 out.push_back(single);
288 }
289 return out;
290}
291
292QString AnnualViewModel::analysisIdFromVariant(const QVariant& value) const {
293 if (!value.isValid()) {
294 return {};
295 }
296 if (value.typeId() == QMetaType::QString) {
297 return value.toString().trimmed();
298 }
299 const QVariantMap map = value.toMap();
300 if (!map.isEmpty()) {
301 const QString id = map.value(QStringLiteral("id")).toString().trimmed();
302 return id.isEmpty()
303 ? map.value(QStringLiteral("objectId")).toString().trimmed()
304 : id;
305 }
306 return value.toString().trimmed();
307}
308
309QVariantMap AnnualViewModel::analysisRowById(const QString& id) const {
310 return rowById(normalizedAnalysisRows(), id);
311}
312
313QVariantMap AnnualViewModel::normalizeAnalysisRow(const QVariant& value) const {
314 const QVariantMap source = value.toMap();
315 const QString id = analysisIdFromVariant(value);
316 if (id.isEmpty()) {
317 return {};
318 }
319 const QString name = source.value(QStringLiteral("name")).toString();
320 const QString display = source.value(QStringLiteral("display")).toString();
321 const QString type =
322 nonEmptyString(source, QStringLiteral("type"), qstr(kTab));
323 QVariantMap out;
324 out.insert(QStringLiteral("id"), id);
325 out.insert(QStringLiteral("name"), name);
326 out.insert(QStringLiteral("display"),
327 !display.isEmpty() ? display : (!name.isEmpty() ? name : id));
328 out.insert(QStringLiteral("type"), type);
329 out.insert(QStringLiteral("config"),
330 source.value(QStringLiteral("config"), QStringLiteral("{}")));
331 out.insert(
332 QStringLiteral("filter"),
333 source.value(QStringLiteral("filter"),
334 source.value(QStringLiteral("filterSpec"), QString())));
335 out.insert(QStringLiteral("exportFormat"),
336 source.value(QStringLiteral("exportFormat"), QString()));
337 out.insert(
340 out.insert(QStringLiteral("snapshotTransactions"),
341 source.value(QStringLiteral("snapshotTransactions"),
342 QStringLiteral("[]")));
343 return augmentAnalysisRow(out);
344}
345
346QVariantList AnnualViewModel::normalizedAnalysisRows() const {
347 QVariantList rows;
348 if (!selectors_) {
349 return rows;
350 }
351 const QVariantList source = selectors_->analysisRows();
352 rows.reserve(source.size());
353 for (const QVariant& value : source) {
354 const QVariantMap row = normalizeAnalysisRow(value);
355 if (!row.isEmpty()) {
356 rows.push_back(row);
357 }
358 }
359 return rows;
360}
361
362QVariantList AnnualViewModel::buildAssignedAnalysisRows() const {
363 const QVariantList rows = normalizedAnalysisRows();
364 QVariantList out;
365 out.reserve(analysisIds_.size());
366 for (const QString& id : analysisIds_) {
367 const QVariantMap row = rowById(rows, id);
368 if (!row.isEmpty()) {
369 out.push_back(row);
370 }
371 }
372 return out;
373}
374
375QVariantList AnnualViewModel::buildAvailableAnalysisRows() const {
376 const QVariantList rows = normalizedAnalysisRows();
377 const QSet<QString> selected(analysisIds_.begin(), analysisIds_.end());
378 QVariantList out;
379 for (const QVariant& value : rows) {
380 const QString id = value.toMap().value(QStringLiteral("id")).toString();
381 if (!id.isEmpty() && !selected.contains(id)) {
382 out.push_back(value);
383 }
384 }
385 return out;
386}
387
388QVariantMap AnnualViewModel::augmentAnalysisRow(const QVariantMap& row) const {
389 QVariantMap out = row;
390 const QString type = row.value(QStringLiteral("type")).toString().toLower();
391 const QVariantList options = exportOptionsForType(type);
392 out.insert(QStringLiteral("typeLabel"),
393 type == qstr(kPlot) ? tr("Plot") : tr("Table"));
394 out.insert(QStringLiteral("exportFormatOptions"), options);
395 out.insert(
396 QStringLiteral("exportFormatIndex"),
397 exportFormatIndex(options,
398 row.value(QStringLiteral("exportFormat")).toString()));
399 return out;
400}
401
402QVariantList AnnualViewModel::exportOptionsForType(const QString& type) const {
403 if (type.trimmed().toLower() == qstr(kPlot)) {
404 return {QStringLiteral("PNG"), QStringLiteral("JPG")};
405 }
406 return {QStringLiteral("XLSX"), QStringLiteral("CSV")};
407}
408
409int AnnualViewModel::exportFormatIndex(const QVariantList& options,
410 const QString& exportFormat) const {
411 const QString normalized = exportFormat.trimmed().toUpper();
412 for (int i = 0; i < options.size(); ++i) {
413 if (options.at(i).toString().toUpper() == normalized) {
414 return i;
415 }
416 }
417 return 0;
418}
419
420QString AnnualViewModel::normalizedExportFormat(const QString& value,
421 const QString& type) const {
422 const QString normalized = value.trimmed().toLower();
423 const QString normalizedType = type.trimmed().toLower();
424 const QStringList allowed = normalizedType == qstr(kPlot)
425 ? QStringList{qstr(kPng), qstr(kJpg)}
426 : QStringList{qstr(kXlsx), qstr(kCsv)};
427 return allowed.contains(normalized) ? normalized : allowed.first();
428}
429
430QVariantList AnnualViewModel::analysisIds() const {
431 QVariantList out;
432 out.reserve(analysisIds_.size());
433 for (const QString& id : analysisIds_) {
434 out.push_back(id);
435 }
436 return out;
437}
438
440 if (index < 0 || index >= availableAnalysisRows_.size()) {
441 return;
442 }
443 const QString id = availableAnalysisRows_.at(index)
444 .toMap()
445 .value(QStringLiteral("id"))
446 .toString();
447 if (id.isEmpty() || analysisIds_.contains(id)) {
448 return;
449 }
450 QStringList next = analysisIds_;
451 next.push_back(id);
452 setAnalysisIds(next);
453}
454
455void AnnualViewModel::removeAnalysis(const QString& id) {
456 const QString trimmed = id.trimmed();
457 if (trimmed.isEmpty() || !analysisIds_.contains(trimmed)) {
458 return;
459 }
460 QStringList next = analysisIds_;
461 next.removeAll(trimmed);
462 setAnalysisIds(next);
463}
464
466 const QString& exportFormat) {
467 if (!commands_) {
468 return;
469 }
470 const QVariantMap row = analysisRowById(id.trimmed());
471 if (row.isEmpty()) {
472 return;
473 }
474 const QString type = nonEmptyString(row, QStringLiteral("type"), qstr(kTab));
475 const QString nextFormat = normalizedExportFormat(exportFormat, type);
476 commands_->updateAnalysisExportFormat(id.trimmed(), nextFormat);
477 analysisMetadataDirty_ = true;
478 refreshAnalysisSelectionRows();
479 rebuildAnnualResultState();
480 emitChanged();
481}
482
483void AnnualViewModel::refreshAnalysisSelectionRows() {
484 assignedAnalysisRows_ = buildAssignedAnalysisRows();
485 availableAnalysisRows_ = buildAvailableAnalysisRows();
486}
487
488void AnnualViewModel::setAnalysisIds(const QStringList& ids) {
489 QStringList next;
490 QSet<QString> seen;
491 for (const QString& id : ids) {
492 const QString trimmed = id.trimmed();
493 if (trimmed.isEmpty() || seen.contains(trimmed)) {
494 continue;
495 }
496 seen.insert(trimmed);
497 next.push_back(trimmed);
498 }
499 if (analysisIds_ == next) {
500 return;
501 }
502 analysisIds_ = next;
503 refreshAnalysisSelectionRows();
504 rebuildAnnualResultState();
505 emitChanged();
506}
507
509 if (!commands_ || !selection_ || !canSubmit()) {
510 return;
511 }
513 "AnnualViewModel::submitCreate", "Annual create submitted",
514 {{observability::context::kName, name_.toStdString()}});
515 const QString id = commands_->addAnnual(name_, year_, analysisIds_);
516 if (id.isEmpty()) {
517 return;
518 }
519 selection_->selectAnnual(id);
520 captureSavedState();
521 emitChanged();
522}
523
525 if (!commands_ || selectedAnnualId().isEmpty() || !canSubmit()) {
526 return;
527 }
529 "AnnualViewModel::submitUpdate", "Annual update submitted",
531 {observability::context::kName, name_.toStdString()}});
532 commands_->updateAnnual(selectedAnnualId(), name_, year_, analysisIds_);
533 captureSavedState();
534 emitChanged();
535}
536
538 if (!commands_ || !selection_ || selectedAnnualId().isEmpty()) {
539 return;
540 }
541 const QString removedId = selectedAnnualId();
543 "AnnualViewModel::deleteCurrent", "Annual delete submitted",
544 {{observability::context::kId, removedId.toStdString()}});
545 commands_->deleteAnnual(removedId);
546
547 const QString nextId = deleteNextSelectionId(annualRows(), removedId);
548 selection_->selectAnnual(nextId);
549 if (nextId.isEmpty()) {
550 loadCreateState();
551 }
552 emitChanged();
553}
554
556 if (!selection_) {
557 return;
558 }
559 const QVariantList rows = annualRows();
560 if (rows.isEmpty()) {
561 return;
562 }
563 selection_->selectAnnual(
564 navigatedSelectionId(rows, selectedAnnualId(), delta));
565}
566
567void AnnualViewModel::selectAnnual(const QString& id) {
568 if (selection_) {
569 selection_->selectAnnual(id.trimmed());
570 }
571}
572
573QVariantMap AnnualViewModel::annualRowById(const QString& id) const {
574 return rowById(annualRows(), id);
575}
576
577int AnnualViewModel::defaultYear() const {
578 return QDate::currentDate().year() - 1;
579}
580
581void AnnualViewModel::loadCreateState() {
582 name_.clear();
583 year_ = defaultYear();
584 analysisIds_.clear();
585 contentIndex_ = 0;
586 analysisMetadataDirty_ = false;
587 refreshAnalysisSelectionRows();
588 captureSavedState();
589}
590
591void AnnualViewModel::loadSelectedAnnual() {
592 if (!selectors_ || selectedAnnualId().isEmpty()) {
593 loadCreateState();
594 return;
595 }
596 const QVariantMap payload = annualRowById(selectedAnnualId());
597 if (payload.isEmpty()) {
598 loadCreateState();
599 return;
600 }
601 name_ = payload.value(QStringLiteral("name")).toString();
602 year_ = payload.value(QStringLiteral("year"), defaultYear()).toInt();
603 analysisIds_ =
604 normalizedAnalysisIds(payload.value(QStringLiteral("analysisIds")));
605 analysisMetadataDirty_ = false;
606 refreshAnalysisSelectionRows();
607 captureSavedState();
608}
609
610void AnnualViewModel::captureSavedState() {
611 savedName_ = name_;
612 savedYear_ = year_;
613 savedAnalysisIds_ = analysisIds_;
614 analysisMetadataDirty_ = false;
615}
616
618 return {
619 transactionSection(
620 qstr(kDeduplicated), tr("Included entries (exact matches)"),
621 annualTransactionGroups_.value(qstr(kDeduplicated)).toList()),
622 transactionSection(
623 qstr(kSimilar), tr("Included entries (possible variants)"),
624 annualTransactionGroups_.value(qstr(kSimilar)).toList()),
625 transactionSection(
626 qstr(kDivergent), tr("Included entries (unique)"),
627 annualTransactionGroups_.value(qstr(kDivergent)).toList()),
628 transactionSection(
629 qstr(kWorkspaceOnly),
630 tr("Missing live transactions from selected year"),
631 annualTransactionGroups_.value(qstr(kWorkspaceOnly)).toList()),
632 transactionSection(
633 qstr(kMissingLive), tr("Included deleted transactions"),
634 annualTransactionGroups_.value(qstr(kMissingLive)).toList()),
635 };
636}
637
639 return {
640 QVariantMap{{QStringLiteral("label"), tr("Included transactions")},
641 {QStringLiteral("value"), annualTransactions_.size()},
642 {QStringLiteral("tone"), QStringLiteral("primary")}},
643 QVariantMap{
644 {QStringLiteral("label"), tr("Duplicate entries")},
645 {QStringLiteral("value"),
646 verificationIssues_.value(QStringLiteral("duplicateCount")).toInt()},
647 {QStringLiteral("tone"), QStringLiteral("primary")}},
648 QVariantMap{{QStringLiteral("label"),
649 tr("Missing live transactions from selected year")},
650 {QStringLiteral("value"),
651 verificationIssues_.value(QStringLiteral("missingFromYear"))
652 .toInt()},
653 {QStringLiteral("tone"), QStringLiteral("primary")}},
654 QVariantMap{
655 {QStringLiteral("label"), tr("Included deleted transactions")},
656 {QStringLiteral("value"),
657 verificationIssues_.value(QStringLiteral("missingLive")).toInt()},
658 {QStringLiteral("tone"), QStringLiteral("primary")}},
659 QVariantMap{
660 {QStringLiteral("label"),
661 tr("Transactions from a different year than selected")},
662 {QStringLiteral("value"),
663 verificationIssues_.value(QStringLiteral("mixedInAnnual")).toInt()},
664 {QStringLiteral("tone"),
665 verificationIssues_.value(QStringLiteral("mixedInAnnual")).toInt() ==
666 0
667 ? QStringLiteral("success")
668 : QStringLiteral("danger")}},
669 QVariantMap{{QStringLiteral("label"), tr("Assigned analyses")},
670 {QStringLiteral("value"), analysisIds_.size()},
671 {QStringLiteral("tone"), QStringLiteral("primary")}},
672 };
673}
674
676 return tr("Neutral: %1, Unverified: %2, Verified: %3, Completed: %4")
677 .arg(statusMetrics_.value(QStringLiteral("neutral")).toInt())
678 .arg(statusMetrics_.value(QStringLiteral("unverified")).toInt())
679 .arg(statusMetrics_.value(QStringLiteral("verified")).toInt())
680 .arg(statusMetrics_.value(QStringLiteral("completed")).toInt());
681}
682
684 const QString trimmed = key.trimmed();
685 if (trimmed.isEmpty()) {
686 return;
687 }
688 sectionExpanded_.insert(trimmed, !isTransactionSectionExpanded(trimmed));
689 emitChanged();
690}
691
692bool AnnualViewModel::isTransactionSectionExpanded(const QString& key) const {
693 return sectionExpanded_.value(key.trimmed(), true).toBool();
694}
695
696void AnnualViewModel::rebuildAnnualResultState() {
697 const QString currentAnnualId = selectedAnnualId();
698 const int currentRevision = workspaceRevision();
699 if (lastPreviewAnnualId_ == currentAnnualId && lastPreviewYear_ == year_ &&
700 lastPreviewAnalysisIds_ == analysisIds_ &&
701 lastPreviewRevision_ == currentRevision) {
702 return;
703 }
704 lastPreviewAnnualId_ = currentAnnualId;
705 lastPreviewYear_ = year_;
706 lastPreviewAnalysisIds_ = analysisIds_;
707 lastPreviewRevision_ = currentRevision;
708 if (!annualWorkflow_) {
709 applyAnnualResult({});
710 return;
711 }
712 applyAnnualResult(annualWorkflow_->computeAnnualPreview(currentAnnualId,
713 analysisIds_, year_));
714}
715
716void AnnualViewModel::applyAnnualResult(const QVariantMap& result) {
717 const QVariantMap stats = result.value(QStringLiteral("stats")).toMap();
718 verificationIssues_ =
719 QVariantMap{{QStringLiteral("missingFromYear"),
720 stats.value(QStringLiteral("missingFromYear"), 0)},
721 {QStringLiteral("mixedInAnnual"),
722 stats.value(QStringLiteral("mixedYear"), 0)},
723 {QStringLiteral("duplicateCount"),
724 stats.value(QStringLiteral("duplicateCount"), 0)},
725 {QStringLiteral("missingLive"),
726 stats.value(QStringLiteral("missingLive"), 0)}};
727 statusMetrics_ = QVariantMap{
728 {QStringLiteral("neutral"), stats.value(QStringLiteral("neutral"), 0)},
729 {QStringLiteral("unverified"),
730 stats.value(QStringLiteral("unverified"), 0)},
731 {QStringLiteral("verified"), stats.value(QStringLiteral("verified"), 0)},
732 {QStringLiteral("completed"),
733 stats.value(QStringLiteral("completed"), 0)}};
734
735 annualTransactions_ = rowsFromResultBucket(
736 result.value(QStringLiteral("transactions")).toList());
737 annualTransactionGroups_ = QVariantMap{
738 {qstr(kDeduplicated),
739 rowsFromResultBucket(result.value(qstr(kDeduplicated)).toList())},
740 {qstr(kSimilar),
741 rowsFromResultBucket(result.value(qstr(kSimilar)).toList())},
742 {qstr(kDivergent),
743 rowsFromResultBucket(result.value(qstr(kDivergent)).toList())},
744 {qstr(kWorkspaceOnly),
745 rowsFromResultBucket(result.value(qstr(kWorkspaceOnly)).toList())},
746 {qstr(kMissingLive),
747 rowsFromResultBucket(result.value(qstr(kMissingLive)).toList())}};
748}
749
750QVariantMap AnnualViewModel::emptyIssues() const {
751 return {{QStringLiteral("missingFromYear"), 0},
752 {QStringLiteral("mixedInAnnual"), 0},
753 {QStringLiteral("duplicateCount"), 0},
754 {QStringLiteral("missingLive"), 0}};
755}
756
757QVariantMap AnnualViewModel::emptyStatusMetrics() const {
758 return {{QStringLiteral("neutral"), 0},
759 {QStringLiteral("unverified"), 0},
760 {QStringLiteral("verified"), 0},
761 {QStringLiteral("completed"), 0}};
762}
763
764QVariantList
765AnnualViewModel::rowsFromResultBucket(const QVariantList& rows) const {
766 QVariantList out;
767 out.reserve(rows.size());
768 for (const QVariant& value : rows) {
769 out.push_back(transactionRow(value.toMap()));
770 }
771 return out;
772}
773
774QVariantMap AnnualViewModel::transactionRow(const QVariantMap& source) const {
775 QVariantMap row = source;
776 const QVariantList names =
777 source.value(QStringLiteral("sourceAnalysisNames")).toList();
778 QStringList nameStrings;
779 nameStrings.reserve(names.size());
780 for (const QVariant& value : names) {
781 const QString text = value.toString();
782 if (!text.isEmpty()) {
783 nameStrings.push_back(text);
784 }
785 }
786 const int status = source.value(QStringLiteral("status")).toInt();
787 row.insert(QStringLiteral("contractType"),
788 source.value(QStringLiteral("contractType")).toString());
789 row.insert(QStringLiteral("amountText"),
790 amountText(source.value(QStringLiteral("amount"))));
791 row.insert(QStringLiteral("sourceNamesText"),
792 nameStrings.join(QStringLiteral(", ")));
793 row.insert(QStringLiteral("allocatableText"),
794 source.value(QStringLiteral("allocatable")).toBool()
795 ? tr("Allocatable")
796 : tr("Non-allocatable"));
797 row.insert(QStringLiteral("contractTypeLabel"),
798 nonEmptyString(source, QStringLiteral("contractType"),
799 tr("No type assigned")));
800 row.insert(QStringLiteral("statusText"),
801 nonEmptyString(source, QStringLiteral("statusText"),
803 row.insert(QStringLiteral("statusTone"),
804 status == 3 ? QStringLiteral("success")
805 : status == 2 ? QStringLiteral("info")
806 : status == 1 ? QStringLiteral("warning")
807 : QStringLiteral("primary"));
808 return row;
809}
810
811QVariantMap
812AnnualViewModel::transactionSection(const QString& key, const QString& title,
813 const QVariantList& rows) const {
814 return {{QStringLiteral("key"), key},
815 {QStringLiteral("title"), title},
816 {QStringLiteral("rows"), rows},
817 {QStringLiteral("expanded"), isTransactionSectionExpanded(key)},
818 {QStringLiteral("visible"), !rows.isEmpty()}};
819}
820
821} // namespace ui
Declares the QML API for the Annual view.
Declares the UI workflow for annual result computation.
Declarations for the UI PayloadKeys component.
Provides translated C++ UI strings for shell, workflow, adapter and view model code.
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 setYear(int value)
Updates the selected annual year.
void setAnnualWorkflow(AnnualWorkflow *value)
Sets the workflow used for annual computation.
Q_INVOKABLE void submitCreate()
Creates an annual through workspace.
QVariantList annualRows
Q_INVOKABLE void submitUpdate()
Updates the selected annual through workspace.
Q_INVOKABLE void setAnalysisExportFormat(const QString &id, const QString &exportFormat)
Updates export format for an assigned analysis.
Q_INVOKABLE void navigate(int delta)
Navigates between annual rows.
Q_INVOKABLE void stepYear(int delta)
Steps the selected year by a relative delta.
QVariantList analysisIds() const
Returns ids of analyses assigned to the annual.
Q_INVOKABLE void selectAnnual(const QString &id)
Selects an annual row.
QVariantList verificationRows
Q_INVOKABLE void addAvailableAnalysisAtIndex(int index)
Adds an available analysis by row index.
Q_INVOKABLE void removeAnalysis(const QString &id)
Removes an analysis from the annual.
Q_INVOKABLE void toggleTransactionSection(const QString &key)
Toggles expansion state for a transaction section.
AnnualViewModel(QObject *parent=nullptr)
Creates the annual view model.
void setName(const QString &value)
Updates the annual name field.
Q_INVOKABLE void refreshFromSelection()
Reloads create or edit state from the workspace selection.
Q_INVOKABLE void toggleContent()
Toggles between annual analysis assignment and transaction preview.
QVariantList transactionSections
void setWorkspaceRoles(WorkspaceStore *store, WorkspaceCommands *commands, WorkspaceSelection *selection, WorkspaceSelectors *selectors)
Sets workspace roles used for annual rows, selection and mutations.
Q_INVOKABLE void deleteCurrent()
Deletes the selected annual through workspace.
Q_INVOKABLE void resetCreateState()
Resets the create form to defaults.
Coordinates annual computation for UI states.
Maps UI intent to core workspace writer commands.
Tracks the currently selected workspace entities.
void selectedAnnualIdChanged()
Emitted when the selected annual id changed.
Projects workspace store data into QML-facing read models.
int dataRevision() const noexcept
Returns the current workspace data revision.
Owns the current UI-side workspace snapshot and revision lifecycle.
void dataRevisionChanged()
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
QVariantList options(const QString &neutral, const QString &unverified, const QString &verified, const QString &completed)
Builds transaction status options.
QString neutral()
Label for neutral transaction status.
Definition Text.h:454
Declares UI-side workspace command submission.