diff --git a/i18n/en.pot b/i18n/en.pot index 56b0abcf..d46353a9 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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-17T18:02:29.553Z\n" +"PO-Revision-Date: 2026-03-17T18:02:29.553Z\n" msgid "Events - Create/update" msgstr "" @@ -230,6 +230,21 @@ msgstr "" msgid "All" msgstr "" +msgid "Import successful" +msgstr "" + +msgid "Import with warnings" +msgstr "" + +msgid "Import with errors" +msgstr "" + +msgid "Network error" +msgstr "" + +msgid "Import pending" +msgstr "" + msgid "" "There has been an error. You can either retry or contact your administrator " "if you think there has been an un recoverable error" diff --git a/i18n/es.po b/i18n/es.po index be274f85..5b461595 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -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-17T18:02:29.553Z\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -258,6 +258,24 @@ msgstr "" msgid "All" msgstr "Todos" +#, fuzzy +msgid "Import successful" +msgstr "Importar datos" + +msgid "Import with warnings" +msgstr "" + +#, fuzzy +msgid "Import with errors" +msgstr "Importar solo los datos nuevos" + +msgid "Network error" +msgstr "" + +#, fuzzy +msgid "Import pending" +msgstr "Importados" + msgid "" "There has been an error. You can either retry or contact your administrator " "if you think there has been an un recoverable error" @@ -1290,9 +1308,9 @@ msgid "" "Samaritan’s Purse, Medecins Sans Frontières (MSF), the the Norwegian Refugee " "Council (NRC) and the Clinton Health Access Initiative (CHAI) to support " "countries in strengthening the collection and use of health data by using " -"DHIS2. The application has been developed by [EyeSeeTea SL](http://eyeseetea." -"com). Source code, documentation and release notes can be found at the " -"[EyeSeetea GitHub Project Page](https://eyeseetea.github.io/Bulk-Load-" +"DHIS2. The application has been developed by [EyeSeeTea SL](http://" +"eyeseetea.com). Source code, documentation and release notes can be found at " +"the [EyeSeetea GitHub Project Page](https://eyeseetea.github.io/Bulk-Load-" "blessed/)." msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 38648ff4..1febca33 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -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-17T18:02:29.553Z\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -252,6 +252,24 @@ msgstr "" msgid "All" msgstr "Tout" +#, fuzzy +msgid "Import successful" +msgstr "Importer des données" + +msgid "Import with warnings" +msgstr "" + +#, fuzzy +msgid "Import with errors" +msgstr "Importez quand même toutes les données" + +msgid "Network error" +msgstr "" + +#, fuzzy +msgid "Import pending" +msgstr "Importé" + msgid "" "There has been an error. You can either retry or contact your administrator " "if you think there has been an un recoverable error" @@ -1309,9 +1327,9 @@ msgid "" "Samaritan’s Purse, Medecins Sans Frontières (MSF), the the Norwegian Refugee " "Council (NRC) and the Clinton Health Access Initiative (CHAI) to support " "countries in strengthening the collection and use of health data by using " -"DHIS2. The application has been developed by [EyeSeeTea SL](http://eyeseetea." -"com). Source code, documentation and release notes can be found at the " -"[EyeSeetea GitHub Project Page](https://eyeseetea.github.io/Bulk-Load-" +"DHIS2. The application has been developed by [EyeSeeTea SL](http://" +"eyeseetea.com). Source code, documentation and release notes can be found at " +"the [EyeSeetea GitHub Project Page](https://eyeseetea.github.io/Bulk-Load-" "blessed/)." msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 926b8a67..677adad0 100644 --- a/i18n/pt.po +++ b/i18n/pt.po @@ -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-17T18:02:29.553Z\n" "Language: pt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -260,6 +260,24 @@ msgstr "" msgid "All" msgstr "Tudo" +#, fuzzy +msgid "Import successful" +msgstr "Importar dados" + +msgid "Import with warnings" +msgstr "" + +#, fuzzy +msgid "Import with errors" +msgstr "Importar apenas novos registros" + +msgid "Network error" +msgstr "" + +#, fuzzy +msgid "Import pending" +msgstr "Importado" + msgid "" "There has been an error. You can either retry or contact your administrator " "if you think there has been an un recoverable error" @@ -1345,9 +1363,9 @@ msgid "" "Samaritan’s Purse, Medecins Sans Frontières (MSF), the the Norwegian Refugee " "Council (NRC) and the Clinton Health Access Initiative (CHAI) to support " "countries in strengthening the collection and use of health data by using " -"DHIS2. The application has been developed by [EyeSeeTea SL](http://eyeseetea." -"com). Source code, documentation and release notes can be found at the " -"[EyeSeetea GitHub Project Page](https://eyeseetea.github.io/Bulk-Load-" +"DHIS2. The application has been developed by [EyeSeeTea SL](http://" +"eyeseetea.com). Source code, documentation and release notes can be found at " +"the [EyeSeetea GitHub Project Page](https://eyeseetea.github.io/Bulk-Load-" "blessed/)." msgstr "" diff --git a/i18n/ru.po b/i18n/ru.po index 4aa1643f..c410e88b 100644 --- a/i18n/ru.po +++ b/i18n/ru.po @@ -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-17T18:02:29.553Z\n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -261,6 +261,24 @@ msgstr "" msgid "All" msgstr "Все" +#, fuzzy +msgid "Import successful" +msgstr "Импортные данные" + +msgid "Import with warnings" +msgstr "" + +#, fuzzy +msgid "Import with errors" +msgstr "Импортировать только новые записи" + +msgid "Network error" +msgstr "" + +#, fuzzy +msgid "Import pending" +msgstr "Импортировано" + msgid "" "There has been an error. You can either retry or contact your administrator " "if you think there has been an un recoverable error" @@ -1349,9 +1367,9 @@ msgid "" "Samaritan’s Purse, Medecins Sans Frontières (MSF), the the Norwegian Refugee " "Council (NRC) and the Clinton Health Access Initiative (CHAI) to support " "countries in strengthening the collection and use of health data by using " -"DHIS2. The application has been developed by [EyeSeeTea SL](http://eyeseetea." -"com). Source code, documentation and release notes can be found at the " -"[EyeSeetea GitHub Project Page](https://eyeseetea.github.io/Bulk-Load-" +"DHIS2. The application has been developed by [EyeSeeTea SL](http://" +"eyeseetea.com). Source code, documentation and release notes can be found at " +"the [EyeSeetea GitHub Project Page](https://eyeseetea.github.io/Bulk-Load-" "blessed/)." msgstr "" diff --git a/src/domain/entities/HistoryEntry.ts b/src/domain/entities/HistoryEntry.ts index 9aa4a5b4..cd419f46 100644 --- a/src/domain/entities/HistoryEntry.ts +++ b/src/domain/entities/HistoryEntry.ts @@ -1,6 +1,6 @@ import { Id } from "./ReferenceObject"; import { generateUid } from "d2/uid"; -import { SynchronizationResult } from "./SynchronizationResult"; +import { SynchronizationResult, computeOverallSyncStatus } from "./SynchronizationResult"; import { Maybe } from "../../types/utils"; import { ImportTemplateError } from "../usecases/ImportTemplateUseCase"; import { isAdmin, User } from "./User"; @@ -117,17 +117,8 @@ export class HistoryEntry { if (!this.syncResults || this.syncResults.length === 0) { return "ERROR"; } - const hasError = this.syncResults.some( - result => result.status === "ERROR" || result.status === "NETWORK ERROR" - ); - if (hasError) { - return "ERROR"; - } - const hasWarning = this.syncResults.some(result => result.status === "WARNING"); - if (hasWarning) { - return "WARNING"; - } - return "SUCCESS"; + const status = computeOverallSyncStatus(this.syncResults); + return status === "NETWORK ERROR" || status === "PENDING" ? "ERROR" : status; } public toDetails(): HistoryEntryDetails { diff --git a/src/domain/entities/SynchronizationResult.ts b/src/domain/entities/SynchronizationResult.ts index 8bf848f5..68f3ef20 100644 --- a/src/domain/entities/SynchronizationResult.ts +++ b/src/domain/entities/SynchronizationResult.ts @@ -23,3 +23,11 @@ export interface SynchronizationResult { errors?: ErrorMessage[]; rawResponse: object; } + +export function computeOverallSyncStatus(results: Pick[]): SynchronizationStatus { + const priority: SynchronizationStatus[] = ["NETWORK ERROR", "ERROR", "WARNING", "SUCCESS"]; + for (const status of priority) { + if (results.some(r => r.status === status)) return status; + } + return "PENDING"; +} diff --git a/src/webapp/components/app/themes/dhis2.theme.js b/src/webapp/components/app/themes/dhis2.theme.js index cd14680f..56978d56 100644 --- a/src/webapp/components/app/themes/dhis2.theme.js +++ b/src/webapp/components/app/themes/dhis2.theme.js @@ -24,6 +24,7 @@ export const colors = { warning: "#F19C02", positive: "#3D9305", info: "#EAF4FF", + defaultStatus: "#3e2723", }; export const palette = { @@ -63,6 +64,7 @@ export const palette = { warning: colors.warning, positive: colors.positive, info: colors.info, + default: colors.defaultStatus, }, background: { paper: colors.white, diff --git a/src/webapp/components/history/HistoryStatusIndicator.tsx b/src/webapp/components/history/HistoryStatusIndicator.tsx index 671b8f23..3848203f 100644 --- a/src/webapp/components/history/HistoryStatusIndicator.tsx +++ b/src/webapp/components/history/HistoryStatusIndicator.tsx @@ -2,6 +2,7 @@ import React from "react"; import { Icon } from "@material-ui/core"; import { HistoryEntryStatus } from "../../../domain/entities/HistoryEntry"; import i18n from "../../../utils/i18n"; +import { getStatusConfig } from "../../utils/statusConfig"; interface HistoryStatusIndicatorProps { status: HistoryEntryStatus; @@ -9,42 +10,27 @@ interface HistoryStatusIndicatorProps { iconStyle?: React.CSSProperties; } -function getStatusConfig(status: HistoryEntryStatus) { +function getHistoryStatusConfig(status: HistoryEntryStatus) { + const base = getStatusConfig(status); switch (status) { case "SUCCESS": - return { - icon: "check_circle", - label: i18n.t("Success"), - color: "#4caf50", - }; + return { ...base, label: i18n.t("Success") }; case "ERROR": - return { - icon: "error", - label: i18n.t("Error"), - color: "#f44336", - }; + return { ...base, label: i18n.t("Error") }; case "WARNING": - return { - icon: "warning", - label: i18n.t("Warning"), - color: "#ff9800", - }; + return { ...base, label: i18n.t("Warning") }; default: - return { - icon: "help", - label: status, - color: "#666", - }; + return { ...base, label: status }; } } export function HistoryStatusIndicator({ status, style, iconStyle }: HistoryStatusIndicatorProps) { - const statusConfig = getStatusConfig(status); + const config = getHistoryStatusConfig(status); return ( - {statusConfig.icon} + {config.icon} - {statusConfig.label} + {config.label} ); } diff --git a/src/webapp/components/import-result-badge/ImportResultBadge.tsx b/src/webapp/components/import-result-badge/ImportResultBadge.tsx new file mode 100644 index 00000000..f186ea91 --- /dev/null +++ b/src/webapp/components/import-result-badge/ImportResultBadge.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { Icon, Link, makeStyles } from "@material-ui/core"; +import { + SynchronizationResult as FullSynchronizationResult, + SynchronizationStatus, + computeOverallSyncStatus, +} from "../../../domain/entities/SynchronizationResult"; +import i18n from "../../../utils/i18n"; +import { getStatusConfig } from "../../utils/statusConfig"; +import { colors } from "../app/themes/dhis2.theme"; + +type SynchronizationResult = Pick; + +interface ImportResultBadgeProps { + results: SynchronizationResult[]; + onClick: () => void; +} + +function getBadgeConfig(status: SynchronizationStatus) { + const base = getStatusConfig(status); + switch (status) { + case "SUCCESS": + return { ...base, label: i18n.t("Import successful") }; + case "WARNING": + return { ...base, label: i18n.t("Import with warnings") }; + case "ERROR": + return { ...base, label: i18n.t("Import with errors") }; + case "NETWORK ERROR": + return { ...base, label: i18n.t("Network error") }; + case "PENDING": + return { ...base, color: colors.grey, label: i18n.t("Import pending") }; + default: { + const _exhaustive: never = status; + return _exhaustive; + } + } +} + +const useStyles = makeStyles({ + badge: { + display: "inline-flex", + alignItems: "center", + marginTop: 12, + marginBottom: 12, + padding: "8px 16px 8px 12px", + borderRadius: 16, + color: colors.white, + fontWeight: 500, + fontSize: "0.95em", + boxShadow: "0 1px 3px rgba(0, 0, 0, 0.2)", + transition: "filter 0.2s ease", + "&:hover": { + filter: "brightness(0.9)", + }, + }, + icon: { + marginRight: 6, + fontSize: 20, + }, +}); + +export function ImportResultBadge({ results, onClick }: ImportResultBadgeProps) { + const classes = useStyles(); + const overallStatus = computeOverallSyncStatus(results); + const config = getBadgeConfig(overallStatus); + + return ( + + {config.icon} + {config.label} + + ); +} diff --git a/src/webapp/components/sync-summary/SyncSummary.tsx b/src/webapp/components/sync-summary/SyncSummary.tsx index 95bcee9b..38623b5f 100644 --- a/src/webapp/components/sync-summary/SyncSummary.tsx +++ b/src/webapp/components/sync-summary/SyncSummary.tsx @@ -20,6 +20,7 @@ import { SynchronizationStats, } from "../../../domain/entities/SynchronizationResult"; import i18n from "../../../utils/i18n"; +import { getStatusColor } from "../../utils/statusConfig"; const useStyles = makeStyles(theme => ({ accordionHeading1: { @@ -46,14 +47,7 @@ const useStyles = makeStyles(theme => ({ export const formatStatusTag = (value: string) => { const text = _.startCase(_.toLower(value)); - const color = - value === "ERROR" || value === "FAILURE" || value === "NETWORK ERROR" - ? "#e53935" - : value === "DONE" || value === "SUCCESS" || value === "OK" - ? "#7cb342" - : "#3e2723"; - - return {text}; + return {text}; }; const buildSummaryTable = (stats: SynchronizationStats[]) => { diff --git a/src/webapp/pages/import-template/ImportTemplatePage.tsx b/src/webapp/pages/import-template/ImportTemplatePage.tsx index 2ae835ee..dc07c249 100644 --- a/src/webapp/pages/import-template/ImportTemplatePage.tsx +++ b/src/webapp/pages/import-template/ImportTemplatePage.tsx @@ -12,6 +12,7 @@ import { SynchronizationResult } from "../../../domain/entities/SynchronizationR import { ImportTemplateUseCaseParams } from "../../../domain/usecases/ImportTemplateUseCase"; import i18n from "../../../utils/i18n"; import ModalDialog, { ModalDialogProps } from "../../components/modal-dialog/ModalDialog"; +import { ImportResultBadge } from "../../components/import-result-badge/ImportResultBadge"; import SyncSummaryDialog from "../../components/sync-summary/SyncSummaryDialog"; import { useAppContext } from "../../contexts/app-context"; import { orgUnitListParams } from "../../utils/template"; @@ -43,6 +44,8 @@ export default function ImportTemplatePage({ settings }: RouteComponentProps) { const [importState, setImportState] = useState(); const [messages, setMessages] = useState([]); const [dialogProps, updateDialog] = useState(); + const [syncResults, setSyncResults] = useState(null); + const [isSyncDialogOpen, setIsSyncDialogOpen] = useState(false); useEffect(() => { compositionRoot.orgUnits.getUserRoots().then(setOrgUnitTreeRootIds); @@ -57,6 +60,8 @@ export default function ImportTemplatePage({ settings }: RouteComponentProps) { setMessages([]); setSelectedOrgUnits([]); setOrgUnitTreeFilter([]); + setSyncResults(null); + setIsSyncDialogOpen(false); const file = files[0]; if (!file) { @@ -118,6 +123,7 @@ export default function ImportTemplatePage({ settings }: RouteComponentProps) { success: syncResults => { loading.reset(); setSyncResults(syncResults); + setIsSyncDialogOpen(true); }, error: error => { loading.reset(); @@ -290,18 +296,18 @@ export default function ImportTemplatePage({ settings }: RouteComponentProps) { } }; + const closeSyncDialog = useCallback(() => setIsSyncDialogOpen(false), []); + const openSyncDialog = useCallback(() => setIsSyncDialogOpen(true), []); + const onOverwriteOrgUnitsChange = useCallback((_event, overwriteOrgUnits) => { setOverwriteOrgUnits(overwriteOrgUnits); }, []); - const [syncResults, setSyncResults] = useState(null); - const hideSyncResults = useCallback(() => setSyncResults(null), [setSyncResults]); - return ( {dialogProps && } - {syncResults && } + {syncResults && isSyncDialogOpen && }

{i18n.t("Bulk data import")}

@@ -344,11 +350,13 @@ export default function ImportTemplatePage({ settings }: RouteComponentProps) { )} + {syncResults && } + {importState?.dataForm && (
= { + FAILURE: colors.negative, + DONE: colors.positive, + OK: colors.positive, +}; + +export function getStatusColor(status: string): string { + return legacyColorAliases[status] ?? getStatusConfig(status as SynchronizationStatus).color; +} + +export function getStatusConfig(status: SynchronizationStatus): StatusConfig { + switch (status) { + case "ERROR": + case "NETWORK ERROR": + return { icon: "error", color: colors.negative }; + case "SUCCESS": + return { icon: "check_circle", color: colors.positive }; + case "WARNING": + return { icon: "warning", color: colors.warning }; + case "PENDING": + return { icon: "hourglass_empty", color: colors.grey }; + default: { + const _exhaustive: never = status; + return _exhaustive; + } + } +}