22#include <unordered_map>
32std::string safeFilePart(
const std::string &text) {
34 out.reserve(text.size());
35 for (
const char c : text) {
36 const bool ok = (c >=
'a' && c <=
'z') || (c >=
'A' && c <=
'Z') ||
37 (c >=
'0' && c <=
'9') || c ==
'_' || c ==
'-' || c ==
'.';
38 out.push_back(ok ? c :
'_');
40 return out.empty() ?
"entry" : out;
43std::string extensionFor(AnalysisExportFormat format) {
45 case AnalysisExportFormat::Csv:
47 case AnalysisExportFormat::Xlsx:
49 case AnalysisExportFormat::Jpg:
51 case AnalysisExportFormat::Png:
57bool writeCsvTable(
const std::filesystem::path &outputPath,
58 const std::vector<std::vector<std::string>> &rows) {
59 std::ofstream stream(outputPath, std::ios::binary);
62 const unsigned char bom[] = {0xEF, 0xBB, 0xBF};
63 stream.write(
reinterpret_cast<const char *
>(bom),
sizeof(bom));
64 for (
const auto &row : rows) {
65 for (
size_t i = 0; i < row.size(); ++i) {
75std::vector<std::vector<std::string>>
77 std::vector<std::vector<std::string>> rows;
79 rows.push_back({
"Category",
"Value"});
80 for (
const auto &row : result.
table) {
83 rows.push_back(row.size() == 1
84 ? std::vector<std::string>{row[0],
"0"}
85 : std::vector<std::string>{row[0], row[1]});
90 rows.push_back({
"Month",
"Total"});
91 for (
const auto &row : result.
table) {
94 std::string month = row[0];
95 std::string totalValue = row.size() > 1 ? row[1] :
"0";
96 rows.push_back({month, totalValue});
103std::unordered_map<std::string, std::string>
105 std::unordered_map<std::string, std::string> out;
107 for (
const auto &property : state.
properties()) {
108 if (!property || property->id().empty())
110 out[
property->id()] =
111 property->name().empty() ?
property->id() :
property->name();
116std::vector<std::vector<std::string>>
120 return normalizedRowsForExport(result);
122 const std::string unassigned =
124 std::set<std::string> contractTypes;
125 std::map<std::string, std::map<std::string, double>> amountsByProperty;
126 const auto nameById = propertyNameById(state);
129 const std::string contractType =
130 !tx.contractType.empty() ? tx.contractType : unassigned;
131 contractTypes.insert(contractType);
132 if (tx.propertyIds.empty()) {
133 amountsByProperty[unassigned][contractType] += tx.amount;
136 bool hasMappedProperty =
false;
137 for (
size_t i = 0; i < tx.propertyIds.size(); ++i) {
138 const auto &propertyId = tx.propertyIds[i];
139 if (propertyId.empty())
141 std::string propertyName;
142 if (i < tx.propertyNames.size() && !tx.propertyNames[i].empty())
143 propertyName = tx.propertyNames[i];
145 const auto it = nameById.find(propertyId);
146 propertyName = it != nameById.end() && !it->second.empty() ? it->second
149 if (propertyName.empty())
151 amountsByProperty[propertyName][contractType] += tx.amount;
152 hasMappedProperty =
true;
154 if (!hasMappedProperty)
155 amountsByProperty[unassigned][contractType] += tx.amount;
158 auto formatAmount = [](
double value) {
159 std::ostringstream stream;
160 stream << std::fixed << std::setprecision(2) << value;
163 std::vector<std::string> orderedContractTypes(contractTypes.begin(),
164 contractTypes.end());
165 std::stable_sort(orderedContractTypes.begin(), orderedContractTypes.end(),
166 [&](
const std::string &lhs,
const std::string &rhs) {
167 const bool lhsUnassigned = lhs == unassigned;
168 const bool rhsUnassigned = rhs == unassigned;
169 if (lhsUnassigned != rhsUnassigned)
170 return !lhsUnassigned;
173 std::vector<std::string> orderedProperties;
174 for (
const auto &[propertyName, _] : amountsByProperty)
175 orderedProperties.push_back(propertyName);
176 std::stable_sort(orderedProperties.begin(), orderedProperties.end(),
177 [&](
const std::string &lhs,
const std::string &rhs) {
178 const bool lhsUnassigned = lhs == unassigned;
179 const bool rhsUnassigned = rhs == unassigned;
180 if (lhsUnassigned != rhsUnassigned)
181 return !lhsUnassigned;
185 std::vector<std::vector<std::string>> rows;
186 std::vector<std::string> header;
189 for (
const auto &contractType : orderedContractTypes)
190 header.push_back(contractType);
192 rows.push_back(std::move(header));
194 std::vector<double> totalsByContract(orderedContractTypes.size(), 0.0);
195 double grandTotal = 0.0;
196 for (
const auto &propertyName : orderedProperties) {
197 const auto &byContract = amountsByProperty[propertyName];
198 std::vector<std::string> row;
199 row.push_back(propertyName);
200 double rowTotal = 0.0;
201 for (
size_t i = 0; i < orderedContractTypes.size(); ++i) {
202 const auto it = byContract.find(orderedContractTypes[i]);
203 const double value = it != byContract.end() ? it->second : 0.0;
204 row.push_back(formatAmount(value));
206 totalsByContract[i] += value;
208 row.push_back(formatAmount(rowTotal));
209 rows.push_back(std::move(row));
210 grandTotal += rowTotal;
212 std::vector<std::string> totalRow;
214 for (
const double value : totalsByContract)
215 totalRow.push_back(formatAmount(value));
216 totalRow.push_back(formatAmount(grandTotal));
217 rows.push_back(std::move(totalRow));
221std::unordered_map<std::string, std::string>
223 std::unordered_map<std::string, std::string> folderByAnnualId;
224 std::set<std::string> usedFolderNames;
225 for (
const auto &annual : state.
annuals()) {
226 if (!annual || annual->id().empty())
228 const std::string baseName =
229 safeFilePart(!annual->name().empty() ? annual->name() : annual->id());
230 std::string candidate = baseName;
232 while (usedFolderNames.find(candidate) != usedFolderNames.end()) {
233 candidate = baseName +
"_" + std::to_string(idx++);
235 usedFolderNames.insert(candidate);
236 folderByAnnualId[annual->id()] = candidate;
238 return folderByAnnualId;
241std::filesystem::path outputPathForItem(
242 const std::filesystem::path &baseOutput,
243 const std::unordered_map<std::string, std::string> &folderByAnnualId,
244 const ExportObjectRequest &item) {
245 const std::string fileStem =
246 safeFilePart(!item.name.empty() ? item.name : item.objectId);
247 const std::string ext = extensionFor(item.format);
248 if (item.annualId.empty())
249 return baseOutput / (fileStem +
"." + ext);
250 const auto it = folderByAnnualId.find(item.annualId);
251 const std::string annualFolder =
252 it != folderByAnnualId.end() ? it->second : safeFilePart(item.annualId);
253 return baseOutput / annualFolder / (fileStem +
"." + ext);
262bool shouldStop(
const core::ports::exporting::ExportRequest &request) {
269core::ports::exporting::ExportResult canceledResult(
270 const core::ports::exporting::ExportRequest &request) {
271 core::ports::exporting::ExportResult result;
284 const std::shared_ptr<core::ports::archive::IArchive> &archive,
285 const std::shared_ptr<core::ports::xlsx_writer::IXlsxWriter> &xlsxWriter,
286 const std::shared_ptr<
294 result.
status = ExportStatus::InvalidInput;
303 if (shouldStop(request)) {
304 return canceledResult(request);
306 std::filesystem::path baseOutput =
308 if (baseOutput.has_extension())
309 baseOutput = baseOutput.parent_path() / baseOutput.stem();
310 if (!std::filesystem::exists(baseOutput))
311 std::filesystem::create_directories(baseOutput);
313 const auto folderByAnnualId = annualFolderNames(state);
315 std::vector<ExportObjectRequest> analysisItems;
317 if (item.type == ExportObjectType::Analysis && !item.objectId.empty())
318 analysisItems.push_back(item);
321 const std::size_t total = analysisItems.size();
322 for (
size_t i = 0; i < total; ++i) {
323 if (shouldStop(request)) {
324 return canceledResult(request);
326 const auto &item = analysisItems[i];
330 (
static_cast<double>(i) / std::max<std::size_t>(1, total))),
331 "Exporting analysis " + std::to_string(i + 1) +
"/" +
332 std::to_string(total));
335 state, item.objectId);
336 if (shouldStop(request)) {
337 return canceledResult(request);
341 const std::filesystem::path outputFile =
342 outputPathForItem(baseOutput, folderByAnnualId, item);
343 std::filesystem::create_directories(outputFile.parent_path());
346 const auto tableRows =
347 normalizedRowsForExport(computed, state);
348 switch (item.format) {
349 case AnalysisExportFormat::Csv:
350 ok = writeCsvTable(outputFile, tableRows);
352 case AnalysisExportFormat::Xlsx:
354 xlsxWriter->writeTable(outputFile, tableRows,
"Analysis");
356 case AnalysisExportFormat::Jpg:
357 case AnalysisExportFormat::Png:
358 ok = imageRenderer &&
359 imageRenderer->renderToImage(
361 computed.type.empty() ? item.objectId : computed.type,
366 result.
status = ExportStatus::WriteFailed;
376 if (shouldStop(request)) {
377 return canceledResult(request);
381 const std::filesystem::path archivePath =
382 baseOutput.string() +
385 !archive->create(baseOutput, archivePath, request.
packageFormat)) {
386 result.
status = ExportStatus::ArchiveFailed;
398 if (shouldStop(request)) {
399 return canceledResult(request);
401 result.
status = ExportStatus::Ok;
405 result.
status = ExportStatus::InternalError;
Defines stable analysis type, configuration, filter, and result keys.
Declares the application service that resolves and executes analyses.
Defines private constants shared by core export implementations.
Declares the analysis rendering port used by the export layer.
Declares the archive abstraction used by export packaging.
Declares the XLSX writing port used by the export layer.
Declares object export execution helpers.
Coordinates analysis lookup, execution, and transaction preview filtering.
core::ports::analysis::AnalysisResult runAnalysisById(const core::domain::catalog::WorkspaceCatalog &state, const std::string &analysisId, const std::string &filterSpec={}) const
Resolves an analysis by identifier and executes it.
const PropertyList & properties() const noexcept
Returns all properties in the catalog.
const AnnualList & annuals() const noexcept
Returns all annuals in the catalog.
Renders analysis results to image artifacts.
constexpr std::string_view kHistogram
constexpr std::string_view kPie
constexpr std::string_view kTypeTab
constexpr std::string_view kOutputPathEmpty
constexpr std::string_view kFileWriteFailed
constexpr std::string_view kArchiveFailed
constexpr std::string_view kInternalError
constexpr std::string_view kTotal
constexpr std::string_view kPropertyHeader
constexpr std::string_view kUnassigned
constexpr std::string_view kArchiveFailed
constexpr std::string_view kOutputPathEmpty
constexpr std::string_view kInternalError
constexpr std::string_view kFileWriteFailed
constexpr std::string_view kZipExtension
core::ports::exporting::ExportResult exportObjectRequests(const core::domain::catalog::WorkspaceCatalog &state, const core::ports::exporting::ExportRequest &request, const std::shared_ptr< core::ports::archive::IArchive > &archive, const std::shared_ptr< core::ports::xlsx_writer::IXlsxWriter > &xlsxWriter, const std::shared_ptr< core::ports::analysis_rendering::IAnalysisRenderer > &imageRenderer)
Export selected workspace objects according to an export request.
std::vector< AnalysisTransaction > transactions
std::vector< std::vector< std::string > > table
Describes one persisted analysis or annual export target.
Describes one export run against a workspace snapshot.
std::function< void(double, const std::string &)> progressCallback
std::function< void()> waitIfPaused
PackageFormat packageFormat
std::vector< ExportObjectRequest > objectRequests
std::function< bool()> shouldCancel
ExportFormat actualFormat
std::string resolvedOutputPath