Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2026-03-11T19:21:15.859Z\n"
"PO-Revision-Date: 2026-03-11T19:21:15.859Z\n"
"POT-Creation-Date: 2026-03-18T04:23:33.926Z\n"
"PO-Revision-Date: 2026-03-18T04:23:33.926Z\n"

msgid "Events - Create/update"
msgstr ""
Expand Down Expand Up @@ -38,9 +38,24 @@ msgstr ""
msgid "Data values - Create/update"
msgstr ""

msgid "No data values to import"
msgstr ""

msgid "Failed to import data values"
msgstr ""

msgid ""
"{{count}} chunk(s) returned no summary — import result unknown for those "
"records."
msgid_plural ""
"{{count}} chunk(s) returned no summary — import result unknown for those "
"records."
msgstr[0] ""
msgstr[1] ""

msgid "Chunk (unknown outcome)"
msgstr ""

msgid "Service"
msgstr ""

Expand Down
18 changes: 17 additions & 1 deletion i18n/es.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Bulk Load\n"
"POT-Creation-Date: 2026-03-11T19:21:15.859Z\n"
"POT-Creation-Date: 2026-03-18T04:23:33.926Z\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -41,9 +41,25 @@ msgstr "Valores de los datos - Borrar"
msgid "Data values - Create/update"
msgstr "Valores de los datos - Crear/actualizar"

#, fuzzy
msgid "No data values to import"
msgstr "valores de datos"

msgid "Failed to import data values"
msgstr ""

msgid ""
"{{count}} chunk(s) returned no summary — import result unknown for those "
"records."
msgid_plural ""
"{{count}} chunk(s) returned no summary — import result unknown for those "
"records."
msgstr[0] ""
msgstr[1] ""

msgid "Chunk (unknown outcome)"
msgstr ""

msgid "Service"
msgstr ""

Expand Down
18 changes: 17 additions & 1 deletion i18n/fr.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Bulk Load App\n"
"POT-Creation-Date: 2026-03-11T19:21:15.859Z\n"
"POT-Creation-Date: 2026-03-18T04:23:33.926Z\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -41,9 +41,25 @@ msgstr ""
msgid "Data values - Create/update"
msgstr ""

#, fuzzy
msgid "No data values to import"
msgstr "valeurs de données"

msgid "Failed to import data values"
msgstr ""

msgid ""
"{{count}} chunk(s) returned no summary — import result unknown for those "
"records."
msgid_plural ""
"{{count}} chunk(s) returned no summary — import result unknown for those "
"records."
msgstr[0] ""
msgstr[1] ""

msgid "Chunk (unknown outcome)"
msgstr ""

msgid "Service"
msgstr ""

Expand Down
18 changes: 17 additions & 1 deletion i18n/pt.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Bulk Load\n"
"POT-Creation-Date: 2026-03-11T19:21:15.859Z\n"
"POT-Creation-Date: 2026-03-18T04:23:33.926Z\n"
"Language: pt\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -41,9 +41,25 @@ msgstr "Valores de dados - Eliminar"
msgid "Data values - Create/update"
msgstr "Valores dos dados - Criar/actualizar"

#, fuzzy
msgid "No data values to import"
msgstr "valores de dados"

msgid "Failed to import data values"
msgstr ""

msgid ""
"{{count}} chunk(s) returned no summary — import result unknown for those "
"records."
msgid_plural ""
"{{count}} chunk(s) returned no summary — import result unknown for those "
"records."
msgstr[0] ""
msgstr[1] ""

msgid "Chunk (unknown outcome)"
msgstr ""

msgid "Service"
msgstr ""

Expand Down
19 changes: 18 additions & 1 deletion i18n/ru.po
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
msgid ""
msgstr ""
"Project-Id-Version: Bulk Load\n"
"POT-Creation-Date: 2026-03-11T19:21:15.859Z\n"
"POT-Creation-Date: 2026-03-18T04:23:33.926Z\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -42,9 +42,26 @@ msgstr "Значения данных - Удалить"
msgid "Data values - Create/update"
msgstr "Значения данных - создание/обновление"

#, fuzzy
msgid "No data values to import"
msgstr "значения данных"

msgid "Failed to import data values"
msgstr ""

msgid ""
"{{count}} chunk(s) returned no summary — import result unknown for those "
"records."
msgid_plural ""
"{{count}} chunk(s) returned no summary — import result unknown for those "
"records."
msgstr[0] ""
msgstr[1] ""
msgstr[2] ""

msgid "Chunk (unknown outcome)"
msgstr ""

msgid "Service"
msgstr ""

Expand Down
96 changes: 83 additions & 13 deletions src/data/InstanceDhisRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ import { DhisInstance } from "../domain/entities/DhisInstance";
import { Locale } from "../domain/entities/Locale";
import { OrgUnit } from "../domain/entities/OrgUnit";
import { NamedRef, Ref } from "../domain/entities/ReferenceObject";
import { SynchronizationResult } from "../domain/entities/SynchronizationResult";
import {
SynchronizationResult,
SynchronizationStats,
SynchronizationStatus,
} from "../domain/entities/SynchronizationResult";
import { Program, TrackedEntityInstance } from "../domain/entities/TrackedEntityInstance";
import {
BuilderMetadata,
Expand All @@ -46,6 +50,7 @@ import {
D2TrackedEntityType,
DataStore,
DataValueSetsGetResponse,
DataValueSetsPostResponse,
Id,
SelectedPick,
D2SharingSchema,
Expand Down Expand Up @@ -458,13 +463,28 @@ export class InstanceDhisRepository implements InstanceRepository {
const title =
importStrategy === "DELETE" ? i18n.t("Data values - Delete") : i18n.t("Data values - Create/update");

const { response } = await this.api.dataValues
.postSetAsync({ importStrategy }, { dataSet: dataSetId, dataValues })
.getData();
if (dataValues.length === 0) {
return {
title,
status: "SUCCESS",
message: i18n.t("No data values to import"),
stats: [{ imported: 0, deleted: 0, updated: 0, ignored: 0 }],
errors: [],
rawResponse: {},
};
}

const importSummary = await this.api.system.waitFor(response.jobType, response.id).getData();
const chunks = _.chunk(dataValues, 3000);

if (!importSummary) {
const chunkResults = await promiseMap(chunks, async chunk => {
const { response } = await this.api.dataValues
.postSetAsync({ importStrategy }, { dataSet: dataSetId, dataValues: chunk })
.getData();

return this.api.system.waitFor(response.jobType, response.id).getData();
});

if (chunkResults.every(r => !r)) {
return {
title,
status: "ERROR",
Expand All @@ -475,21 +495,71 @@ export class InstanceDhisRepository implements InstanceRepository {
};
}

const { status, description, conflicts, importCount } = importSummary;
const { imported, deleted, updated, ignored } = importCount;
const errors = conflicts?.map(({ object, value }) => ({ id: object, message: value, details: "" })) ?? [];
const { mergedStatus, mergedDescription, mergedImportCount, nullChunkStats, summaries } =
this.mergeChunkResults(chunks, chunkResults);

const allConflicts = _.flatMap(summaries, s => s.conflicts ?? []);
const errors = allConflicts.map(({ object, value }) => ({ id: object, message: value, details: "" }));
const errorDetails = await getMetadataDetailsFromErrors(this.api, errors);

return {
title,
status,
message: description,
stats: [{ imported, deleted, updated, ignored }],
status: mergedStatus,
message: mergedDescription,
stats: [mergedImportCount, ...nullChunkStats],
errors: errorDetails,
rawResponse: importSummary,
rawResponse: summaries,
};
}

private mergeChunkResults(chunks: AggregatedDataValue[][], chunkResults: Array<DataValueSetsPostResponse | null>) {
const emptyChunkCount = chunkResults.filter(r => !r).length;
const hasEmptySummaries = emptyChunkCount > 0;
const summaries = _.compact(chunkResults);

const uniqueStatuses = _.uniq(summaries.map(s => s.status));
const mergedStatus: SynchronizationStatus =
hasEmptySummaries || uniqueStatuses.length !== 1 ? "WARNING" : uniqueStatuses[0] ?? "WARNING";

const mergedDescription = [
..._.uniq(summaries.map(s => s.description).filter(Boolean)),
...(hasEmptySummaries
? [
i18n.t("{{count}} chunk(s) returned no summary — import result unknown for those records.", {
count: emptyChunkCount,
}),
]
: []),
].join(" / ");

const mergedImportCount = summaries.reduce(
(acc, s) => ({
imported: acc.imported + s.importCount.imported,
deleted: acc.deleted + s.importCount.deleted,
updated: acc.updated + s.importCount.updated,
ignored: acc.ignored + s.importCount.ignored,
}),
{ imported: 0, deleted: 0, updated: 0, ignored: 0 }
);

// Add a dedicated stat row per null chunk with total to unknown outcomes are explicitly.
const nullChunkStats: SynchronizationStats[] = hasEmptySummaries
? chunkResults
.map((result, i) => ({ result, chunk: chunks[i] }))
.filter(({ result }) => !result)
.map(({ chunk }) => ({
type: i18n.t("Chunk (unknown outcome)"),
imported: 0,
deleted: 0,
updated: 0,
ignored: 0,
total: chunk?.length ?? 0,
}))
: [];

return { mergedStatus, mergedDescription, mergedImportCount, nullChunkStats, summaries };
}

// TODO: Review when data validation comes in
private async validateAggregateImportPackage(dataValues: AggregatedDataValue[]) {
const dataElements = _.uniq(dataValues.map(({ dataElement }) => dataElement));
Expand Down
Loading