FOSSredder 0.5.0
Loading...
Searching...
No Matches
ImportWorkflow.cpp
Go to the documentation of this file.
1
5
7
9
10#include <algorithm>
11#include <map>
12#include <stdexcept>
13#include <utility>
14#include <vector>
15
17#include "ui/i18n/Text.h"
20#include "ui/shell/Defaults.h"
24#include <QMetaObject>
25#include <QUuid>
26
27namespace ui::importing {
28
30public:
32
33 bool isRunning() const noexcept {
34 return isRunning_;
35 }
36 bool isPaused() const noexcept {
37 return paused_;
38 }
39 double progress() const noexcept {
40 return progress_;
41 }
42 const QString& phase() const noexcept {
43 return phase_;
44 }
45 const QString& error() const noexcept {
46 return error_;
47 }
48 const QString& selectedFile() const noexcept {
49 return selectedFile_;
50 }
51 const QStringList& queuedFiles() const noexcept {
52 return queuedFiles_;
53 }
54 bool hasDraft() const noexcept {
55 return hasDraft_;
56 }
58 return hasDraft_ ? &draft_ : nullptr;
59 }
61 return hasDraft_ ? &draft_ : nullptr;
62 }
64 catalogSnapshot() const noexcept {
65 return catalogSnapshot_;
66 }
67 int currentTransactionIndex() const noexcept {
68 return currentTransactionIndex_;
69 }
70 void setCurrentTransactionIndex(int index);
71 int artifactCount() const noexcept {
72 return artifactCount_;
73 }
74 bool cancelRequested() const noexcept {
75 return canceled_;
76 }
77
78 bool setSelectedFile(const QString& path);
79 bool addFiles(const QStringList& paths);
80 bool resetStatus();
81 bool clearDraft();
82
83 QString currentImportFile() const;
85 bool takeNextQueuedFile(QString& nextFile);
86
87 void beginImport(const QString& path);
88 void rejectStart(const QString& errorMessage);
89 void beginCancel(bool clearQueue);
90 bool setPaused(bool paused);
91 void recordCanceled();
92 void recordFailed(const QString& errorMessage);
93 void recordFinished();
94 bool
97 const std::map<std::string, std::vector<uint8_t>>& artifacts,
101 const QString& draftId, int currentTransactionIndex);
102 void updateProgress(double progress, const QString& phase);
103
104private:
105 void clearDraftState();
106 void clearTransientImportState();
107 void resetCancellationState();
108
109 bool isRunning_ = false;
110 double progress_ = 0.0;
111 QString phase_;
112 QString error_;
113 QString selectedFile_;
114 QStringList queuedFiles_;
116 bool hasDraft_ = false;
117 int currentTransactionIndex_ = 0;
119 int artifactCount_ = 0;
120 bool canceled_ = false;
121 bool paused_ = false;
122 bool cancelClearsQueue_ = false;
123 QString currentImportFile_;
124};
125
126void ImportWorkflowState::clearDraftState() {
127 draft_ = {};
128 catalogSnapshot_ = {};
129 hasDraft_ = false;
130 currentTransactionIndex_ = 0;
131}
132
133bool ImportWorkflowState::setSelectedFile(const QString& path) {
134 if (selectedFile_ == path) {
135 return false;
136 }
137 selectedFile_ = path;
138 return true;
139}
140
141bool ImportWorkflowState::addFiles(const QStringList& paths) {
142 QStringList cleaned;
143 cleaned.reserve(paths.size());
144 for (const auto& path : paths) {
145 const auto trimmed = path.trimmed();
146 if (trimmed.isEmpty()) {
147 continue;
148 }
149 cleaned.push_back(trimmed);
150 }
151 if (cleaned.isEmpty()) {
152 return false;
153 }
154
155 bool changed = false;
156 const bool hasSelection = !selectedFile_.trimmed().isEmpty();
157 int startIndex = 0;
158 if (!hasSelection) {
159 changed = setSelectedFile(cleaned.front()) || changed;
160 startIndex = 1;
161 }
162
163 for (int i = startIndex; i < cleaned.size(); ++i) {
164 if (cleaned[i] == selectedFile_ || queuedFiles_.contains(cleaned[i])) {
165 continue;
166 }
167 queuedFiles_.push_back(cleaned[i]);
168 changed = true;
169 }
170
171 return changed;
172}
173
175 if (isRunning_) {
176 return false;
177 }
178
179 phase_.clear();
180 error_.clear();
182 canceled_ = false;
183 cancelClearsQueue_ = false;
184 paused_ = false;
185 selectedFile_.clear();
186 currentImportFile_.clear();
187 queuedFiles_.clear();
188 artifactCount_ = 0;
189 return true;
190}
191
193 clearDraftState();
194
195 artifactCount_ = 0;
196 if (!isRunning_ && queuedFiles_.isEmpty()) {
197 selectedFile_.clear();
198 currentImportFile_.clear();
199 }
200 return !isRunning_ && !queuedFiles_.isEmpty();
201}
202
204 if (!hasDraft_ || draft_.transactions.empty()) {
205 currentTransactionIndex_ = 0;
206 return;
207 }
208 const int maxIndex = static_cast<int>(draft_.transactions.size()) - 1;
209 currentTransactionIndex_ = std::clamp(index, 0, maxIndex);
210}
211
213 error_.clear();
215 clearTransientImportState();
216 resetCancellationState();
217 selectedFile_.clear();
218 currentImportFile_.clear();
219}
220
222 return currentImportFile_.isEmpty() ? selectedFile_ : currentImportFile_;
223}
224
226 const QString trimmed = selectedFile_.trimmed();
227 if (trimmed.isEmpty()) {
228 return {};
229 }
230 selectedFile_.clear();
231 return trimmed;
232}
233
235 if (isRunning_ || queuedFiles_.isEmpty()) {
236 return false;
237 }
238 nextFile = queuedFiles_.takeFirst();
239 return true;
240}
241
242void ImportWorkflowState::clearTransientImportState() {
243 isRunning_ = false;
245}
246
247void ImportWorkflowState::resetCancellationState() {
248 canceled_ = false;
249 cancelClearsQueue_ = false;
250 paused_ = false;
251}
252
253void ImportWorkflowState::beginImport(const QString& path) {
254 selectedFile_ = path;
255 currentImportFile_ = path;
256
257 artifactCount_ = 0;
258 error_.clear();
261 isRunning_ = true;
262 resetCancellationState();
263}
264
265void ImportWorkflowState::rejectStart(const QString& errorMessage) {
266 error_ = errorMessage;
267 queuedFiles_.clear();
268 currentImportFile_.clear();
269 phase_.clear();
270 clearTransientImportState();
271 resetCancellationState();
272}
273
274void ImportWorkflowState::beginCancel(bool clearQueue) {
275 if (!isRunning_) {
276 return;
277 }
278 canceled_ = true;
279 paused_ = false;
280 cancelClearsQueue_ = clearQueue;
281 if (clearQueue) {
282 queuedFiles_.clear();
283 }
285}
286
288 if (!isRunning_ || canceled_) {
289 return false;
290 }
291 if (paused_ == paused) {
292 return false;
293 }
294 paused_ = paused;
295 phase_ = paused_ ? ui::text::importing::phasePaused()
297 return true;
298}
299
301 error_.clear();
302 if (cancelClearsQueue_) {
303 queuedFiles_.clear();
304 }
306 clearTransientImportState();
307 resetCancellationState();
308 selectedFile_.clear();
309 currentImportFile_.clear();
310}
311
312void ImportWorkflowState::recordFailed(const QString& errorMessage) {
313 error_ = errorMessage;
314 queuedFiles_.clear();
316 clearTransientImportState();
317 resetCancellationState();
318 currentImportFile_.clear();
319}
320
324 const std::map<std::string, std::vector<uint8_t>>& artifacts,
326 if (draft.id.empty() && draft.name.empty()) {
327 return false;
328 }
329
330 artifactCount_ = static_cast<int>(artifacts.size());
331
332 clearDraftState();
333 draft_ = draft;
334 catalogSnapshot_ = state;
335 hasDraft_ = true;
338 return true;
339}
340
344 const QString& draftId, int currentTransactionIndex) {
345 if (draft.id.empty() && draft.name.empty()) {
346 return false;
347 }
348
349 clearDraftState();
350 draft_ = draft;
351 if (!draftId.isEmpty()) {
352 draft_.id = ui::strings::toStdString(draftId);
353 }
354 catalogSnapshot_ = state;
355 hasDraft_ = true;
357 return hasDraft_;
358}
359
361 const QString& phase) {
362 if (!isRunning_ || canceled_ || paused_) {
363 return;
364 }
365
366 progress_ = progress;
367 if (phase.isEmpty()) {
368 return;
369 }
370 phase_ = phase;
371}
372
373} // namespace ui::importing
374
375namespace ui {
376
377namespace {
378
379QString newLogId() {
380 return QUuid::createUuid().toString(QUuid::WithoutBraces);
381}
382
383double clampedProgress(double progress) {
386 }
389 }
390 return progress;
391}
392
393} // namespace
394
396 std::shared_ptr<ui::adapters::ImportAdapter> importAdapter,
397 std::shared_ptr<core::ports::diagnostics::IErrorReporter> errorReporter,
398 StateSnapshotProvider stateSnapshotProvider, WorkspaceCommands* commands,
399 WorkspaceSelectors* selectors, QObject* parent)
400 : QObject(parent),
401 state_(std::make_unique<importing::ImportWorkflowState>()),
402 importAdapter_(std::move(importAdapter)),
403 stateSnapshotProvider_(std::move(stateSnapshotProvider)),
404 commands_(commands), selectors_(selectors),
405 errorReporter_(std::move(errorReporter)) {
406 if (!errorReporter_) {
407 throw std::invalid_argument("ImportWorkflow requires an error reporter");
408 }
409}
410
412
414 WorkspaceSelectors* selectors) {
415 commands_ = commands;
416 selectors_ = selectors;
417 emit stateChanged();
418}
419
420bool ImportWorkflow::isRunning() const noexcept {
421 return state_->isRunning();
422}
423
424bool ImportWorkflow::isPaused() const noexcept {
425 return state_->isPaused();
426}
427
428double ImportWorkflow::progress() const noexcept {
429 return state_->progress();
430}
431
432QString ImportWorkflow::phase() const {
433 return state_->phase();
434}
435
436QString ImportWorkflow::error() const {
437 return state_->error();
438}
439
441 return state_->selectedFile();
442}
443
444int ImportWorkflow::queuedCount() const noexcept {
445 return static_cast<int>(state_->queuedFiles().size());
446}
447
448QStringList ImportWorkflow::queuedFiles() const {
449 return state_->queuedFiles();
450}
451
452bool ImportWorkflow::hasDraft() const noexcept {
453 return state_->hasDraft();
454}
455
457 return state_->currentTransactionIndex();
458}
459
460bool ImportWorkflow::hasActiveImportHandle() const noexcept {
461 return hasActiveImportHandle_ && !activeImportHandle_.importId.empty();
462}
463
464void ImportWorkflow::clearActiveImportSubscription() {
465 if (importAdapter_ && hasActiveImportHandle()) {
466 importAdapter_->unsubscribe(activeImportHandle_);
467 }
468 activeImportHandle_ = {};
469 hasActiveImportHandle_ = false;
470}
471
472void ImportWorkflow::saveImportLog(const QString& logId, const QString& status,
473 const QString& message, bool draftAttached,
474 const QString& draftId,
475 const QString& statementId,
476 const QString& importFile) {
477 if (!commands_) {
478 return;
479 }
480 const QString sourceFile =
481 importFile.isEmpty() ? state_->currentImportFile() : importFile;
482 commands_->upsertImportLog(logId, status, message, draftAttached, draftId,
483 statementId, sourceFile);
484}
485
486void ImportWorkflow::clearStoredDraft(const QString& draftId) {
487 if (!commands_) {
488 return;
489 }
490 try {
491 commands_->clearStatementDraft(draftId);
492 } catch (...) {
494 std::current_exception());
495 }
496}
497
499 if (!commands_ || !state_->hasDraft()) {
500 return;
501 }
502 const QString draftId = currentDraftId();
504 saveImportLog(draftId, text::importing::statusDraft(),
505 text::importing::messageDraftPaused(), true, draftId);
506 clearDraft();
507}
508
510 if (!commands_ || !state_->hasDraft()) {
511 return;
512 }
513 const QString draftId = currentDraftId();
515 clearStoredDraft(draftId);
516 saveImportLog(draftId, text::importing::statusDraftDiscarded(),
518 state_->selectedFile());
519 clearDraft();
520}
521
523 if (!commands_ || !state_->hasDraft()) {
524 return;
525 }
526 const QString draftId = currentDraftId();
529 "Statement draft finalize submitted",
530 {{"draftId", draftId.toStdString()}});
531
532 QString statementId;
533 try {
534 statementId = finalizeCurrentStatementDraft();
535 } catch (...) {
537 std::current_exception());
538 }
539
540 if (statementId.isEmpty()) {
541 saveImportLog(draftId, text::importing::statusFinalizeFailed(),
543 return;
544 }
545
546 clearStoredDraft(draftId);
547 saveImportLog(draftId, text::importing::statusFinalized(),
548 text::importing::messageFinalized(), false, {}, statementId);
549 clearDraft();
550}
551
553 bool draftAttached,
554 const QString& draftId) {
555 if (draftAttached) {
556 clearStoredDraft(draftId);
557 clearDraft();
558 }
559 if (commands_ && !logId.isEmpty()) {
560 commands_->deleteImportLog(logId);
561 }
562}
563
564void ImportWorkflow::addFiles(const QStringList& paths) {
565 if (state_->addFiles(paths)) {
566 emit stateChanged();
567 }
568}
569
570void ImportWorkflow::setSelectedFile(const QString& path) {
571 if (state_->setSelectedFile(path)) {
572 emit stateChanged();
573 }
574}
575
577 if (state_->resetStatus()) {
578 emit stateChanged();
579 }
580}
581
583 rememberCurrentDraftTransactionIndex();
584 const bool shouldStartNext = state_->clearDraft();
585 activeDraftId_.clear();
586 emit stateChanged();
587 if (shouldStartNext) {
588 startNextQueuedImport();
589 }
590}
591
592void ImportWorkflow::rejectImportStart(const QString& errorMessage,
593 const char* traceMessage) {
594 state_->rejectStart(errorMessage);
595 observability::reportFlow(errorReporter_.get(),
599 traceMessage);
600 emit stateChanged();
601 emit importFailed(state_->error());
602}
603
604void ImportWorkflow::startNextQueuedImport() {
605 QString next;
606 if (!state_->takeNextQueuedFile(next)) {
607 return;
608 }
609 emit stateChanged();
610 startImportForFile(next);
611}
612
614 if (state_->isRunning() || state_->hasDraft()) {
615 return;
616 }
617
618 const auto selected = state_->takeSelectedFileForStart();
619 if (!selected.isEmpty()) {
620 emit stateChanged();
621 startImportForFile(selected);
622 return;
623 }
624
625 startNextQueuedImport();
626}
627
628void ImportWorkflow::startImportForFile(const QString& path) {
629 if (state_->isRunning()) {
630 return;
631 }
632 if (!importAdapter_) {
634 "Import start rejected: workflow unavailable");
635 return;
636 }
637 if (path.trimmed().isEmpty()) {
638 rejectImportStart(text::workflowErrors::noFileSelected(),
639 "Import start rejected: no file selected");
640 return;
641 }
642
643 state_->beginImport(path);
644 activeImportLogId_ = newLogId();
645 activeImportTerminalHandled_ = false;
646 hasPendingTerminalEvent_ = false;
647 activeImportHandle_ = {};
648 hasActiveImportHandle_ = false;
650 pendingTerminalMessage_.clear();
651 saveImportLog(activeImportLogId_, text::importing::statusRunning(),
653 emit stateChanged();
654
659 std::to_string(state_->queuedFiles().size())}});
661 errorReporter_.get(),
665 {{observability::context::kFile, strings::toStdString(path)},
666 {observability::context::kQueuedCount,
667 std::to_string(state_->queuedFiles().size())}});
668
669 core::ports::importing::ImportRequest request;
670 request.sourcePath = strings::toEncodedPath(path);
671 activeImportHandle_ = importAdapter_->startStatementImport(
672 request,
673 [this](const core::ports::importing::StatementImportEvent& event) {
674 handleImportEvent(event);
675 });
676 hasActiveImportHandle_ = !activeImportHandle_.importId.empty();
677
678 if (!hasActiveImportHandle_) {
679 handleImportFailed(text::workflowErrors::importFailed(),
680 "Import failed: unable to start job");
681 }
682}
683
684void ImportWorkflow::updateProgress(double progress, const QString& phase) {
685 state_->updateProgress(progress, phase);
686 emit stateChanged();
687}
688
689void ImportWorkflow::reportException(const char* origin,
690 std::exception_ptr exception) const {
691 if (!errorReporter_) {
692 return;
693 }
694 errorReporter_->reportException(core::errors::ErrorSeverity::Error, origin,
695 exception);
696}
697
698void ImportWorkflow::requestImportCancellation(bool clearQueue,
699 const char* origin,
700 const char* traceMessage) {
701 if (!state_->isRunning()) {
702 return;
703 }
704
705 const auto queuedBeforeCancel =
706 clearQueue ? state_->queuedFiles() : QStringList{};
707 state_->beginCancel(clearQueue);
708
709 if (clearQueue && commands_) {
710 for (const auto& queuedPath : queuedBeforeCancel) {
711 commands_->upsertImportLog(newLogId(), text::importing::statusCanceled(),
712 text::importing::messageCanceledBeforeStart(),
713 false, {}, {}, queuedPath);
714 }
715 }
716
717 if (importAdapter_ && hasActiveImportHandle()) {
718 importAdapter_->cancel(activeImportHandle_);
719 }
720 observability::reportFlow(
721 errorReporter_.get(),
723 observability::codes::FlowImportCanceled, origin, traceMessage,
724 {{observability::context::kFile,
725 strings::toStdString(state_->currentImportFile())},
726 {observability::context::kQueuedCount,
727 std::to_string(state_->queuedFiles().size())}});
728 emit stateChanged();
729}
730
732 requestImportCancellation(false,
734 "Import cancellation requested");
735}
736
738 requestImportCancellation(
740 "Cancel-all requested for import queue");
741}
742
743void ImportWorkflow::setImportPaused(bool paused) {
744 if (state_->isPaused() == paused) {
745 return;
746 }
747 if (!state_->setPaused(paused)) {
748 return;
749 }
750 if (importAdapter_ && hasActiveImportHandle()) {
751 if (state_->isPaused()) {
752 importAdapter_->pause(activeImportHandle_);
753 } else {
754 importAdapter_->resume(activeImportHandle_);
755 }
756 }
757 if (!activeImportLogId_.isEmpty()) {
758 saveImportLog(activeImportLogId_,
759 state_->isPaused() ? text::importing::statusPaused()
760 : text::importing::statusRunning(),
761 state_->isPaused() ? text::importing::messageImportPaused()
762 : text::importing::messageImportResumed());
763 }
764 emit stateChanged();
765
766 if (!state_->isPaused() && hasPendingTerminalEvent_) {
767 const auto terminalState = pendingTerminalState_;
768 const auto message = pendingTerminalMessage_;
769 hasPendingTerminalEvent_ = false;
770 pendingTerminalState_ =
772 pendingTerminalMessage_.clear();
773 onJobTerminal(terminalState, message);
774 }
775}
776
778 setImportPaused(true);
779}
780
782 setImportPaused(false);
783}
784
785void ImportWorkflow::handleImportEvent(
787 const double progress = clampedProgress(event.progress);
788 const QString phase = QString::fromStdString(event.message);
789 const auto eventState = event.state;
790 const QString message = QString::fromStdString(event.message);
791
792 QMetaObject::invokeMethod(
793 this,
794 [this, progress, phase, eventState, message]() {
795 if (eventState ==
797 eventState ==
799 eventState ==
801 onJobTerminal(eventState, message);
802 return;
803 }
804 updateProgress(progress, phase);
805 },
806 Qt::QueuedConnection);
807}
808
809void ImportWorkflow::handleImportCanceled() {
810 state_->recordCanceled();
811 if (!activeImportTerminalHandled_) {
812 saveImportLog(activeImportLogId_, text::importing::statusCanceled(),
813 text::importing::phaseCanceled());
814 activeImportTerminalHandled_ = true;
815 }
816 observability::reportFlow(
817 errorReporter_.get(),
819 observability::codes::FlowImportCanceled,
820 observability::origins::workflow::import::kTerminal, "Import canceled",
821 {{observability::context::kStatus,
822 strings::toStdString(text::importing::statusCanceled())}});
823 emit stateChanged();
824 emit importCanceled();
825}
826
827void ImportWorkflow::handleImportFailed(const QString& errorMessage,
828 const char* traceMessage) {
829 const QString traceDetail =
830 traceMessage ? QString::fromUtf8(traceMessage) : QString();
831 const QString visibleError =
832 errorMessage == text::workflowErrors::importFailed() &&
833 traceDetail.startsWith(QStringLiteral("Import failed:"))
834 ? traceDetail
835 : errorMessage;
836 state_->recordFailed(visibleError);
837 if (!activeImportTerminalHandled_) {
838 saveImportLog(activeImportLogId_, text::importing::statusFailed(),
839 errorMessage);
840 activeImportTerminalHandled_ = true;
841 }
842 observability::reportFlow(errorReporter_.get(),
844 observability::codes::FlowImportFailed,
845 observability::origins::workflow::import::kTerminal,
846 traceMessage,
847 {{observability::context::kError,
848 strings::toStdString(state_->error())}});
849 emit stateChanged();
850 emit importFailed(state_->error());
851}
852
853bool ImportWorkflow::populateDraftFromResult() {
854 if (!importAdapter_ || !hasActiveImportHandle()) {
855 handleImportFailed(text::workflowErrors::importFailed(),
856 "Import failed: import runner unavailable");
857 return false;
858 }
859
860 const auto imported = importAdapter_->importResult(activeImportHandle_);
861 if (!imported.hasStatement) {
862 handleImportFailed(text::workflowErrors::importFailed(),
863 "Import failed: missing statement result");
864 return false;
865 }
866
867 const auto snapshot = stateSnapshot();
868 const QString draftId = activeImportLogId_;
869 const bool hadVisibleDraft = state_->hasDraft();
870
871 const auto preparedDraft = importAdapter_->buildStatementDraft(
872 strings::toStdString(state_->currentImportFile()), imported.statement,
873 snapshot, imported.transactions, strings::toStdString(draftId));
874 if (preparedDraft.id.empty() && preparedDraft.transactions.empty()) {
875 handleImportFailed(text::workflowErrors::importFailed(),
876 "Import failed: unable to create draft state");
877 return false;
878 }
879
880 const auto draftSnapshot =
881 importAdapter_->buildStatementDraftSnapshot(preparedDraft, snapshot);
882 if (!commands_ || draftSnapshot.id.empty()) {
883 handleImportFailed(text::workflowErrors::importFailed(),
884 "Import failed: unable to save draft state");
885 return false;
886 }
887 commands_->saveStatementDraft(draftSnapshot);
888
889 if (hadVisibleDraft) {
890 state_->recordFinished();
891 } else {
892 if (!state_->populateDraft(preparedDraft, snapshot, imported.artifacts,
893 0)) {
894 handleImportFailed(text::workflowErrors::importFailed(),
895 "Import failed: unable to create statement draft");
896 return false;
897 }
898 }
899
900 saveImportLog(activeImportLogId_, text::importing::statusDraft(),
901 text::importing::messageDraftReady(), true, draftId);
902 if (!hadVisibleDraft) {
903 activeDraftId_ = draftId;
904 }
905 activeImportTerminalHandled_ = true;
906
907 observability::reportFlow(
908 errorReporter_.get(),
910 observability::codes::FlowImportFinished,
911 observability::origins::workflow::import::kTerminal, "Import finished",
912 {{observability::context::kStatus,
913 strings::toStdString(text::importing::statusSuccess())},
914 {observability::context::kArtifactCount,
915 std::to_string(state_->artifactCount())}});
916 emit stateChanged();
917 emit importFinished();
918 return true;
919}
920
921void ImportWorkflow::onJobTerminal(
923 const QString& message) {
924 if ((!state_->isRunning() && !state_->cancelRequested()) ||
925 activeImportTerminalHandled_) {
926 return;
927 }
928
929 if (state_->isPaused() &&
931 hasPendingTerminalEvent_ = true;
932 pendingTerminalState_ = state;
933 pendingTerminalMessage_ = message;
934 return;
935 }
936
938 state_->cancelRequested()) {
939 clearActiveImportSubscription();
940 handleImportCanceled();
941 return;
942 }
943
945 clearActiveImportSubscription();
946 handleImportFailed(message.isEmpty() ? text::workflowErrors::importFailed()
947 : message,
948 "Import failed");
949 return;
950 }
951
952 const bool populated = populateDraftFromResult();
953 clearActiveImportSubscription();
954 if (!populated) {
955 return;
956 }
957
958 startNextQueuedImport();
959}
960
961QString ImportWorkflow::resolveDraftContextId() const {
962 if (!activeDraftId_.isEmpty()) {
963 return activeDraftId_;
964 }
965 return currentDraftId();
966}
967
968void ImportWorkflow::rememberCurrentDraftTransactionIndex() {
969 const QString draftId = currentDraftId();
970 if (draftId.isEmpty()) {
971 return;
972 }
973 draftTransactionIndexByDraftId_.insert(draftId,
974 state_->currentTransactionIndex());
975}
976
977int ImportWorkflow::rememberedDraftTransactionIndex(
978 const QString& draftId) const {
979 return draftTransactionIndexByDraftId_.value(draftId, 0);
980}
981
982bool ImportWorkflow::openStoredDraft(const QString& draftId) {
983 if (!stateSnapshotProvider_ || !selectors_) {
984 return false;
985 }
986 rememberCurrentDraftTransactionIndex();
987 const QString currentDraftId = this->currentDraftId();
988
989 QString requestedDraftId = !draftId.isEmpty() ? draftId : activeDraftId_;
990 if (requestedDraftId.isEmpty()) {
991 for (const auto& id : selectors_->attachedImportDraftIds()) {
992 requestedDraftId = id;
993 break;
994 }
995 }
996 if (!requestedDraftId.isEmpty()) {
997 if (!currentDraftId.isEmpty() && requestedDraftId != currentDraftId) {
999 }
1000 activeDraftId_ = requestedDraftId;
1001 }
1002
1003 const bool restored = restoreDraftFromState(stateSnapshot());
1004 if (restored) {
1005 emit stateChanged();
1006 }
1007 return restored;
1008}
1009
1010bool ImportWorkflow::restoreDraftFromState(
1012 if (snapshot.statementDrafts.empty() || !importAdapter_) {
1013 return false;
1014 }
1015
1016 const core::ports::workspace::StatementDraftSnapshot* storedDraft = nullptr;
1017 const QString requestedDraftId = resolveDraftContextId();
1018 if (!requestedDraftId.isEmpty()) {
1019 for (const auto& draft : snapshot.statementDrafts) {
1020 if (QString::fromStdString(draft.id) == requestedDraftId) {
1021 storedDraft = &draft;
1022 break;
1023 }
1024 }
1025 }
1026 if (!storedDraft) {
1027 storedDraft = &snapshot.statementDrafts.front();
1028 }
1029 if (!storedDraft) {
1030 return false;
1031 }
1032
1033 const QString restoredDraftId = QString::fromStdString(storedDraft->id);
1034 if (!restoredDraftId.isEmpty()) {
1035 activeDraftId_ = restoredDraftId;
1036 }
1037
1038 auto draft = importAdapter_->restoreStatementDraft(*storedDraft);
1039 return state_->restoreDraft(draft, snapshot, restoredDraftId,
1040 rememberedDraftTransactionIndex(restoredDraftId));
1041}
1042
1044ImportWorkflow::currentTransactionDraft() {
1045 auto* draft = state_->draft();
1046 if (!draft) {
1047 return nullptr;
1048 }
1049 const int index = state_->currentTransactionIndex();
1050 if (index < 0 ||
1051 static_cast<std::size_t>(index) >= draft->transactions.size()) {
1052 return nullptr;
1053 }
1054 return &draft->transactions[static_cast<std::size_t>(index)];
1055}
1056
1058ImportWorkflow::currentTransactionDraft() const {
1059 return const_cast<ImportWorkflow*>(this)->currentTransactionDraft();
1060}
1061
1063 const auto* draft = state_->draft();
1064 return draft ? static_cast<int>(draft->transactions.size()) : 0;
1065}
1066
1068ImportWorkflow::stateSnapshot() const {
1069 return stateSnapshotProvider_ ? stateSnapshotProvider_()
1070 : core::ports::workspace::WorkspaceSnapshot{};
1071}
1072
1074 const auto* draft = state_->draft();
1075 return draft ? QString::fromStdString(draft->id) : QString();
1076}
1077
1079 const auto* draft = state_->draft();
1080 return draft ? QString::fromStdString(draft->name) : QString();
1081}
1082
1084ImportWorkflow::statementDraft() noexcept {
1085 return state_->draft();
1086}
1087
1089ImportWorkflow::statementDraft() const noexcept {
1090 return state_->draft();
1091}
1092
1094 if (!state_->hasDraft()) {
1095 return;
1096 }
1097 const int previous = state_->currentTransactionIndex();
1098 state_->setCurrentTransactionIndex(index);
1099 if (state_->currentTransactionIndex() != previous) {
1100 emit stateChanged();
1101 }
1102}
1103
1104void ImportWorkflow::notifyDraftChanged() {
1105 emit stateChanged();
1106}
1107
1109 auto* draft = statementDraft();
1110 if (!draft || !importAdapter_) {
1111 return false;
1112 }
1115 edit.text = strings::toStdString(name);
1116 const auto result = importAdapter_->updateStatementDraft(*draft, edit);
1117 if (result.changed) {
1118 notifyDraftChanged();
1119 }
1120 return result.changed;
1121}
1122
1124 auto* draft = statementDraft();
1125 if (!draft || !importAdapter_) {
1126 return -1;
1127 }
1129 edit.kind = core::ports::importing::draft::StatementDraftEditKind::
1130 InsertTransactionAfter;
1132 const int newIndex = importAdapter_->updateStatementDraft(*draft, edit)
1133 .selectedTransactionIndex;
1134 if (newIndex >= 0) {
1136 }
1137 return newIndex;
1138}
1139
1141 auto* draft = statementDraft();
1142 if (!draft || !importAdapter_) {
1143 return -1;
1144 }
1146 edit.kind = core::ports::importing::draft::StatementDraftEditKind::
1147 RemoveTransactionAt;
1149 const int newIndex = importAdapter_->updateStatementDraft(*draft, edit)
1150 .selectedTransactionIndex;
1151 if (newIndex >= 0) {
1153 }
1154 return newIndex;
1155}
1156
1157bool ImportWorkflow::applyCurrentTransactionPatch(
1159 auto* transaction = currentTransactionDraft();
1160 if (!transaction || !importAdapter_) {
1161 return false;
1162 }
1165 edit.patch = patch;
1166 if (!importAdapter_->updateTransactionDraft(*transaction, {}, edit)) {
1167 return false;
1168 }
1169 notifyDraftChanged();
1170 return true;
1171}
1172
1175 patch.hasName = true;
1176 patch.name = strings::toStdString(name);
1177 return applyCurrentTransactionPatch(patch);
1178}
1179
1181 const QString& bookingDate) {
1183 patch.hasBookingDate = true;
1184 patch.bookingDate = strings::toStdString(bookingDate);
1185 return applyCurrentTransactionPatch(patch);
1186}
1187
1190 patch.hasValuta = true;
1191 patch.valuta = strings::toStdString(valuta);
1192 return applyCurrentTransactionPatch(patch);
1193}
1194
1197 patch.hasStatus = true;
1198 patch.status = status;
1199 return applyCurrentTransactionPatch(patch);
1200}
1201
1204 patch.hasAllocatable = true;
1205 patch.allocatable = allocatable;
1206 patch.allocatableSelected = true;
1207 return applyCurrentTransactionPatch(patch);
1208}
1209
1211 const QString& amountText) {
1212 auto* transaction = currentTransactionDraft();
1213 if (!transaction || !importAdapter_) {
1214 return false;
1215 }
1217 edit.kind =
1219 edit.text = strings::toStdString(amountText);
1220 if (!importAdapter_->updateTransactionDraft(*transaction, {}, edit)) {
1221 return false;
1222 }
1223 notifyDraftChanged();
1224 return true;
1225}
1226
1228 auto* transaction = currentTransactionDraft();
1229 if (!transaction || !importAdapter_) {
1230 return false;
1231 }
1233 edit.kind =
1235 edit.id = strings::toStdString(actorId);
1236 const bool changed =
1237 importAdapter_->updateTransactionDraft(*transaction, {}, edit);
1238 if (changed) {
1239 notifyDraftChanged();
1240 }
1241 return changed;
1242}
1243
1245 auto* transaction = currentTransactionDraft();
1246 if (!transaction || !importAdapter_) {
1247 return false;
1248 }
1250 edit.kind =
1252 const bool changed =
1253 importAdapter_->updateTransactionDraft(*transaction, {}, edit);
1254 if (changed) {
1255 notifyDraftChanged();
1256 }
1257 return changed;
1258}
1259
1261 const QString& propertyId, bool selected) {
1262 auto* transaction = currentTransactionDraft();
1263 if (!transaction || !importAdapter_) {
1264 return false;
1265 }
1267 edit.kind = core::ports::importing::draft::TransactionDraftEditKind::
1268 SetPropertySelected;
1269 edit.id = strings::toStdString(propertyId);
1270 edit.selected = selected;
1271 const bool changed =
1272 importAdapter_->updateTransactionDraft(*transaction, {}, edit);
1273 if (changed) {
1274 notifyDraftChanged();
1275 }
1276 return changed;
1277}
1278
1280 const QString& contractId) {
1281 auto* transaction = currentTransactionDraft();
1282 if (!transaction || !importAdapter_) {
1283 return false;
1284 }
1286 edit.kind = core::ports::importing::draft::TransactionDraftEditKind::
1287 SelectContractFromCatalog;
1288 edit.id = strings::toStdString(contractId);
1289 const bool changed = importAdapter_->updateTransactionDraft(
1290 *transaction, catalogSnapshotForDraft(), edit);
1291 if (changed) {
1292 notifyDraftChanged();
1293 }
1294 return changed;
1295}
1296
1298 auto* transaction = currentTransactionDraft();
1299 if (!transaction || !importAdapter_) {
1300 return false;
1301 }
1303 edit.kind =
1305 const bool changed =
1306 importAdapter_->updateTransactionDraft(*transaction, {}, edit);
1307 if (changed) {
1308 notifyDraftChanged();
1309 }
1310 return changed;
1311}
1312
1314 const QString& actorName) {
1315 if (!commands_ || !selectors_) {
1316 return false;
1317 }
1318 const QString trimmedName = actorName.trimmed();
1319 if (trimmedName.isEmpty()) {
1320 return false;
1321 }
1322 QString actorId = selectors_->actorIdByName(trimmedName);
1323 if (actorId.isEmpty()) {
1324 actorId = commands_->saveActor({}, trimmedName);
1325 }
1326 return !actorId.isEmpty() && selectCurrentTransactionActor(actorId);
1327}
1328
1330 const QString& propertyName) {
1331 if (!commands_ || !selectors_) {
1332 return false;
1333 }
1334 const QString trimmedName = propertyName.trimmed();
1335 if (trimmedName.isEmpty()) {
1336 return false;
1337 }
1338 QString propertyId = selectors_->propertyIdByName(trimmedName);
1339 if (propertyId.isEmpty()) {
1340 propertyId = commands_->saveProperty({}, trimmedName);
1341 }
1342 return !propertyId.isEmpty() &&
1343 setCurrentTransactionPropertySelected(propertyId, true);
1344}
1345
1347 const QString& contractName, const QString& contractType,
1348 const QString& allocatableMode) {
1349 auto* transaction = currentTransactionDraft();
1350 if (!transaction || !commands_ || !selectors_) {
1351 return false;
1352 }
1353 const QString trimmedType = contractType.trimmed();
1354 if (trimmedType.isEmpty()) {
1355 return false;
1356 }
1357 const QString effectiveName = contractName.trimmed().isEmpty()
1358 ? selectors_->nextContractName()
1359 : contractName.trimmed();
1360 QStringList actorIds;
1361 const QString actorId =
1362 QString::fromStdString(transaction->actorId).trimmed();
1363 if (!actorId.isEmpty()) {
1364 actorIds.push_back(actorId);
1365 }
1366 QStringList propertyIds;
1367 propertyIds.reserve(static_cast<int>(transaction->propertyIds.size()));
1368 for (const auto& propertyId : transaction->propertyIds) {
1369 propertyIds.push_back(QString::fromStdString(propertyId));
1370 }
1371
1372 QString contractId = selectors_->contractIdBySignature(
1373 effectiveName, trimmedType, actorIds, propertyIds);
1374 if (contractId.isEmpty()) {
1375 QString mode = allocatableMode.trimmed().toLower();
1376 if (mode.isEmpty()) {
1377 mode = QStringLiteral("mixed");
1378 }
1379 contractId = commands_->saveContract({}, effectiveName, trimmedType,
1380 actorIds, propertyIds, {}, mode);
1381 }
1382 return !contractId.isEmpty() && selectCurrentTransactionContract(contractId);
1383}
1384
1386 const auto* transaction = currentTransactionDraft();
1387 if (!transaction || !importAdapter_) {
1388 return {};
1389 }
1390 try {
1391 return importAdapter_->transactionDraftView(*transaction,
1392 catalogSnapshotForDraft());
1393 } catch (...) {
1395 std::current_exception());
1396 return {};
1397 }
1398}
1399
1401ImportWorkflow::currentStatementDraftSnapshot() const {
1402 const auto* draft = statementDraft();
1403 if (!draft || !importAdapter_) {
1404 return {};
1405 }
1406 return importAdapter_->buildStatementDraftSnapshot(*draft,
1407 catalogSnapshotForDraft());
1408}
1409
1410QString ImportWorkflow::finalizeCurrentStatementDraft() {
1411 if (!commands_ || !importAdapter_ || !state_->hasDraft()) {
1412 return {};
1413 }
1414 flushActiveDraftToWorkspace();
1415 const auto snapshot = currentStatementDraftSnapshot();
1416 if (snapshot.id.empty() || snapshot.transactions.empty()) {
1417 return {};
1418 }
1419 return commands_->finalizeStatementDraft(snapshot);
1420}
1421
1423 if (!commands_ || !importAdapter_ || !state_->hasDraft()) {
1424 return;
1425 }
1426
1427 const auto* draft = state_->draft();
1428 if (!draft || draft->id.empty()) {
1429 return;
1430 }
1431
1432 const auto snapshot = importAdapter_->buildStatementDraftSnapshot(
1433 *draft, catalogSnapshotForDraft());
1434 if (snapshot.id.empty()) {
1435 return;
1436 }
1437
1438 commands_->saveStatementDraft(snapshot);
1439}
1440
1442ImportWorkflow::catalogSnapshotForDraft() const {
1443 const auto liveSnapshot = stateSnapshot();
1444 if (!state_->hasDraft() || !importAdapter_) {
1445 return liveSnapshot;
1446 }
1447
1448 return importAdapter_->mergeWorkspaceState(state_->catalogSnapshot(),
1449 liveSnapshot);
1450}
1451
1452} // namespace ui
Defines UI-specific runtime constants that are not shared with core.
Declares the import adapter used by UI import workflows.
Coordinates asynchronous statement imports and in-memory draft sessions.
Declarations for the UI Origins component.
Declares lightweight conversions between common Qt string types and standard strings.
Provides translated C++ UI strings for shell, workflow, adapter and view model code.
Declarations for the UI Trace component.
Declares read-only workspace selectors for QML payloads.
bool clearCurrentTransactionContract()
Clears the contract selection for the current transaction draft.
void startStatementImport()
Starts importing the selected or next queued statement file.
void discardActiveDraft()
Discards the active draft and removes its workspace draft state.
bool createActorForCurrentTransaction(const QString &actorName)
Creates or reuses an actor and selects it for the current draft.
bool setCurrentTransactionPropertySelected(const QString &propertyId, bool selected)
Selects or deselects a property for the current transaction draft.
ImportWorkflow(std::shared_ptr< ui::adapters::ImportAdapter > importAdapter, std::shared_ptr< core::ports::diagnostics::IErrorReporter > errorReporter, StateSnapshotProvider stateSnapshotProvider={}, WorkspaceCommands *commands=nullptr, WorkspaceSelectors *selectors=nullptr, QObject *parent=nullptr)
Creates an import workflow.
void cancelImport()
Cancels the current running import.
void setSelectedFile(const QString &path)
Sets the selected import file.
QString currentDraftId() const
Returns the active draft id.
void setWorkspaceRoles(WorkspaceCommands *commands, WorkspaceSelectors *selectors)
Rebinds workspace roles used by the workflow.
bool commitCurrentTransactionName(const QString &name)
Updates the current transaction draft name.
void addFiles(const QStringList &paths)
Adds files to the import selection or queue.
bool clearCurrentTransactionActor()
Clears the actor selection for the current transaction draft.
bool openStoredDraft(const QString &draftId={})
Opens a statement draft stored in workspace state.
void finalizeActiveDraft()
Finalizes the active draft into workspace statement data.
void resumeImport()
Resumes the current paused import.
bool commitCurrentTransactionValuta(const QString &valuta)
Updates the current transaction draft valuta date.
bool createOrSelectContractForCurrentTransaction(const QString &contractName, const QString &contractType, const QString &allocatableMode)
Creates or reuses a contract and selects it for the current draft.
bool selectCurrentTransactionContract(const QString &contractId)
Selects a contract for the current transaction draft.
bool setCurrentTransactionStatus(int status)
Updates the current transaction draft status.
void setCurrentTransactionIndex(int index)
Selects the current transaction draft by index.
void importFailed(const QString &error)
Emitted when an import failed.
void pauseImport()
Pauses the current running import.
bool isRunning() const noexcept
Reports whether an import is running.
QString selectedFile() const
Returns the selected import file.
TransactionDraftView currentTransactionView() const
Builds the QML-facing view of the current transaction draft.
void clearDraft()
Clears the active draft and may continue queued imports.
double progress() const noexcept
Returns import progress.
bool selectCurrentTransactionActor(const QString &actorId)
Selects an actor for the current transaction draft.
bool hasDraft() const noexcept
Reports whether an active statement draft exists.
int queuedCount() const noexcept
Returns queued import file count.
bool applyCurrentTransactionAmountText(const QString &amountText)
Applies user-entered amount text to the current transaction draft.
void stateChanged()
Emitted when import workflow state changed.
void removeAttachedImportLog(const QString &logId, bool draftAttached, const QString &draftId)
Removes an import log and optionally its attached draft.
void cancelQueuedImports()
Cancels the running import and clears queued imports.
void flushActiveDraftToWorkspace()
Saves the active in-memory draft into workspace draft state.
bool createPropertyForCurrentTransaction(const QString &propertyName)
Creates or reuses a property and selects it for the current draft.
bool isPaused() const noexcept
Reports whether the running import is paused.
bool commitCurrentTransactionBookingDate(const QString &bookingDate)
Updates the current transaction draft booking date.
~ImportWorkflow() override
Destroys the workflow and its internal state.
QString currentStatementName() const
Returns the editable name of the active statement draft.
int insertTransactionAfterCurrent()
Inserts a transaction draft after the current one.
void resetStatus()
Resets import status fields when no import is running.
void pauseActiveDraft()
Saves the active draft and returns to the import overview.
int removeCurrentTransaction()
Removes the current transaction draft.
int currentTransactionIndex() const noexcept
Returns the selected transaction draft index.
QString phase() const
Returns the current import phase text.
QStringList queuedFiles() const
Returns queued import files.
QString error() const
Returns the current import error text.
std::function< core::ports::workspace::WorkspaceSnapshot()> StateSnapshotProvider
bool renameCurrentStatementDraft(const QString &name)
Renames the active statement draft.
int transactionCount() const noexcept
Returns transaction count in the active draft.
bool setCurrentTransactionAllocatable(bool allocatable)
Updates the current transaction draft allocatable flag.
Maps UI intent to core workspace writer commands.
Projects workspace store data into QML-facing read models.
bool setSelectedFile(const QString &path)
const core::ports::workspace::WorkspaceSnapshot & catalogSnapshot() const noexcept
core::ports::importing::draft::StatementDraft * draft() noexcept
bool populateDraft(const core::ports::importing::draft::StatementDraft &draft, const core::ports::workspace::WorkspaceSnapshot &state, const std::map< std::string, std::vector< uint8_t > > &artifacts, int currentTransactionIndex)
const QString & selectedFile() const noexcept
const core::ports::importing::draft::StatementDraft * draft() const noexcept
bool addFiles(const QStringList &paths)
bool restoreDraft(const core::ports::importing::draft::StatementDraft &draft, const core::ports::workspace::WorkspaceSnapshot &state, const QString &draftId, int currentTransactionIndex)
void updateProgress(double progress, const QString &phase)
void beginImport(const QString &path)
bool takeNextQueuedFile(QString &nextFile)
const QString & error() const noexcept
int currentTransactionIndex() const noexcept
bool cancelRequested() const noexcept
const QString & phase() const noexcept
void rejectStart(const QString &errorMessage)
const QStringList & queuedFiles() const noexcept
void recordFailed(const QString &errorMessage)
Defines shared structured error codes.
constexpr double kMinimum
Minimum visible progress value shown by the import UI.
Definition Defaults.h:116
constexpr double kMaximum
Terminal progress value shown after a successful import.
Definition Defaults.h:120
constexpr double kInitial
Initial progress value shown once an import has started.
Definition Defaults.h:118
constexpr auto kQueuedCount
Definition Trace.h:36
constexpr auto kFile
Definition Trace.h:26
void reportFlow(core::ports::diagnostics::IErrorReporter *reporter, core::errors::ErrorSeverity severity, const char *code, const char *origin, std::string message, core::errors::ErrorContext context={})
Reports a UI flow error event.
Definition Trace.h:132
void traceWorkflow(const char *origin, std::string message, core::errors::ErrorContext context={})
Emits a workflow trace event.
Definition Trace.h:108
std::string toStdString(const QString &value)
Converts a QString to a UTF-8 encoded std::string.
std::string toEncodedPath(const QString &path)
Converts a filesystem path to the platform-encoded byte representation.
QString phaseRunning()
Phase text after a paused import resumes.
Definition Text.h:400
QString statusDraft()
Import log status for draft rows.
Definition Text.h:340
QString statusDraftDiscarded()
Import log status for discarded drafts.
Definition Text.h:352
QString messageFinalizeFailed()
Import log detail shown when finalizing a draft fails.
Definition Text.h:372
QString phaseStopping()
Phase text while a cancellation request is being processed.
Definition Text.h:392
QString messageDraftDiscarded()
Import log detail shown after a draft is discarded.
Definition Text.h:364
QString messageFinalized()
Import log detail shown after a draft becomes a statement.
Definition Text.h:376
QString statusFinalized()
Import log status for finalized drafts.
Definition Text.h:344
QString phaseStarting()
Phase text when an import has just been submitted.
Definition Text.h:396
QString statusRunning()
Import log status for active runs.
Definition Text.h:332
QString phaseFinished()
Phase text after an import finished.
Definition Text.h:416
QString phasePaused()
Phase text while an import is paused for draft review.
Definition Text.h:404
QString phaseFailed()
Phase text after an import failed.
Definition Text.h:412
QString statusFinalizeFailed()
Import log status for failed draft finalization.
Definition Text.h:356
QString phaseCanceled()
Phase text after an import was canceled.
Definition Text.h:408
QString messageDraftPaused()
Import log detail shown while a draft waits for review.
Definition Text.h:360
QString noFileSelected()
Error text when an operation requires a selected file.
Definition Text.h:239
QString importWorkflowUnavailable()
Error text when import workflow wiring is unavailable.
Definition Text.h:235
QString importFailed()
Generic import failure text.
Definition Text.h:243
bool empty() const noexcept
Checks whether all snapshot collections are empty and no file is open.
std::vector< StatementDraftSnapshot > statementDrafts
std::vector< TransactionSnapshot > transactions
Qt-friendly projection of the currently edited import transaction.
Declares UI-side workspace command submission.