Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
7 changes: 5 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-18T04:23:33.926Z\n"
"PO-Revision-Date: 2026-03-18T04:23:33.926Z\n"
"POT-Creation-Date: 2026-04-15T11:34:40.806Z\n"
"PO-Revision-Date: 2026-04-15T11:34:40.806Z\n"

msgid "Events - Create/update"
msgstr ""
Expand Down Expand Up @@ -948,6 +948,9 @@ msgstr ""
msgid "Synchronization Results"
msgstr ""

msgid "Selected template has unsupported period type: {{periodType}}"
msgstr ""

msgid "Select organisation unit to populate data"
msgstr ""

Expand Down
5 changes: 4 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-18T04:23:33.926Z\n"
"POT-Creation-Date: 2026-04-15T09:39:22.405Z\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -1009,6 +1009,9 @@ msgstr "Respuesta JSON"
msgid "Synchronization Results"
msgstr "Resultados de la sincronización"

msgid "Selected template has unsupported period type: {{periodType}}"
msgstr "La plantilla seleccionada tiene el tipo de período no soportado: {{periodType}}"

msgid "Select organisation unit to populate data"
msgstr "Seleccione la unidad de organización para rellenar los datos"

Expand Down
5 changes: 4 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-18T04:23:33.926Z\n"
"POT-Creation-Date: 2026-04-15T09:39:22.405Z\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -1028,6 +1028,9 @@ msgstr ""
msgid "Synchronization Results"
msgstr ""

msgid "Selected template has unsupported period type: {{periodType}}"
msgstr "Modèle sélectionné avec un type de période non supporté: {{periodType}}"

msgid "Select organisation unit to populate data"
msgstr ""

Expand Down
5 changes: 4 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-18T04:23:33.926Z\n"
"POT-Creation-Date: 2026-04-15T09:39:22.405Z\n"
"Language: pt\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -1064,6 +1064,9 @@ msgstr "Resposta do JSON"
msgid "Synchronization Results"
msgstr "Resultados da Sincronização"

msgid "Selected template has unsupported period type: {{periodType}}"
msgstr "Modelo selecionado com tipo de período não suportado: {{periodType}}"

msgid "Select organisation unit to populate data"
msgstr "Selecione a unidade da organização para preencher os dados"

Expand Down
5 changes: 4 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-18T04:23:33.926Z\n"
"POT-Creation-Date: 2026-04-15T09:39:22.405Z\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
Expand Down Expand Up @@ -1067,6 +1067,9 @@ msgstr "Ответ JSON"
msgid "Synchronization Results"
msgstr "Результаты синхронизации"

msgid "Selected template has unsupported period type: {{periodType}}"
msgstr "Выбранный шаблон имеет неподдерживаемый тип периода: {{periodType}}"

msgid "Select organisation unit to populate data"
msgstr "Выберите организационную единицу для заполнения данных"

Expand Down
28 changes: 27 additions & 1 deletion src/domain/entities/DataForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,29 @@ export const dataFormTypeMap = {
} as const;
export const dataFormTypes = Object.values(dataFormTypeMap) as typeof dataFormTypeMap[keyof typeof dataFormTypeMap][];
export type DataFormType = typeof dataFormTypes[number];
export type DataFormPeriod = "Daily" | "Monthly" | "Yearly" | "Weekly" | "Quarterly";

const dataFormPeriods = [
"Daily",
"Monthly",
"Yearly",
"Weekly",
"Quarterly",
"BiWeekly",
"BiMonthly",
"WeeklyWednesday",
"WeeklyThursday",
"WeeklySaturday",
"WeeklySunday",
"QuarterlyNov",
"SixMonthly",
"SixMonthlyApril",
"SixMonthlyNov",
"FinancialApril",
"FinancialJuly",
"FinancialOct",
"FinancialNov",
] as const;
export type DataFormPeriod = typeof dataFormPeriods[number];

export function getTranslations() {
return {
Expand All @@ -22,6 +44,10 @@ export function getTranslations() {
};
}

export function isDataFormPeriod(value: unknown): value is DataFormPeriod {
return typeof value === "string" && (dataFormPeriods as readonly string[]).includes(value);
}

export interface DataForm {
type: DataFormType;
id: Id;
Expand Down
210 changes: 210 additions & 0 deletions src/test/utils/periods.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import moment from "moment";
import { buildAllPossiblePeriods } from "../../webapp/utils/periods";

// Input validation
describe("buildAllPossiblePeriods - Input validation", () => {
it("returns empty array if startDate is missing", () => {
const result = buildAllPossiblePeriods("Monthly", undefined, moment("2024-12-31"));
expect(result).toEqual([]);
});

it("returns empty array if endDate is missing", () => {
const result = buildAllPossiblePeriods("Monthly", moment("2024-01-01"), undefined);
expect(result).toEqual([]);
});

it("returns empty array if both dates are missing", () => {
const result = buildAllPossiblePeriods("Monthly", undefined, undefined);
expect(result).toEqual([]);
});

it("returns empty array if startDate is after endDate", () => {
const result = buildAllPossiblePeriods("Monthly", moment("2024-12-31"), moment("2024-01-01"));
expect(result).toEqual([]);
});

it("throws error if periodType is undefined", () => {
expect(() => buildAllPossiblePeriods(undefined, moment("2024-01-01"), moment("2024-12-31"))).toThrow(
"Missing period type"
);
});

it("throws error for unsupported period type", () => {
expect(() =>
buildAllPossiblePeriods("UnsupportedType" as any, moment("2024-01-01"), moment("2024-12-31"))
).toThrow("Unsupported period type: UnsupportedType");
});
});

// Daily
describe("buildAllPossiblePeriods - Daily", () => {
it("generates daily periods", () => {
const result = buildAllPossiblePeriods("Daily", moment("2024-01-27"), moment("2024-02-02"));
expect(result).toEqual(["20240127", "20240128", "20240129", "20240130", "20240131", "20240201", "20240202"]);
});

it("includes leap day in range", () => {
const result = buildAllPossiblePeriods("Daily", moment("2024-02-28"), moment("2024-03-01"));
expect(result).toEqual(["20240228", "20240229", "20240301"]);
});
});

// Weekly variants
describe("buildAllPossiblePeriods - Weekly variants", () => {
it("Weekly generates period for sub-week range", () => {
const result = buildAllPossiblePeriods("Weekly", moment("2024-12-30"), moment("2025-01-04"));
expect(result).toEqual(["2025W1"]);
});
it("Weekly generates weeks", () => {
const result = buildAllPossiblePeriods("Weekly", moment("2024-12-01"), moment("2025-01-15"));
expect(result).toEqual(["2024W48", "2024W49", "2024W50", "2024W51", "2024W52", "2025W1", "2025W2", "2025W3"]);
});

it("WeeklyWednesday generates Wed-based weeks", () => {
const result = buildAllPossiblePeriods("WeeklyWednesday", moment("2024-01-01"), moment("2024-01-31"));
expect(result).toEqual(["2023WedW52", "2024WedW1", "2024WedW2", "2024WedW3", "2024WedW4", "2024WedW5"]);
});

it("WeeklyThursday generates Thu-based weeks", () => {
const result = buildAllPossiblePeriods("WeeklyThursday", moment("2024-01-01"), moment("2024-01-31"));
expect(result).toEqual(["2023ThuW52", "2024ThuW1", "2024ThuW2", "2024ThuW3", "2024ThuW4"]);
});

it("WeeklySaturday generates Sat-based weeks", () => {
const result = buildAllPossiblePeriods("WeeklySaturday", moment("2024-01-01"), moment("2024-01-31"));
expect(result).toEqual(["2023SatW52", "2024SatW1", "2024SatW2", "2024SatW3", "2024SatW4"]);
});

it("WeeklySunday generates Sun-based weeks", () => {
const result = buildAllPossiblePeriods("WeeklySunday", moment("2024-01-01"), moment("2024-01-31"));
expect(result).toEqual(["2023SunW52", "2024SunW1", "2024SunW2", "2024SunW3", "2024SunW4"]);
});

it("Weekly uses ISO week year", () => {
const result = buildAllPossiblePeriods("Weekly", moment("2019-12-30"), moment("2020-01-05"));
expect(result).toEqual(["2020W1"]);
});

it("includes week 53 in years that have 53 ISO weeks", () => {
const result = buildAllPossiblePeriods("Weekly", moment("2020-12-28"), moment("2021-01-03"));
expect(result).toEqual(["2020W53"]);
});
});

// Monthly
describe("buildAllPossiblePeriods - Monthly", () => {
it("generates monthly periods", () => {
const result = buildAllPossiblePeriods("Monthly", moment("2023-11-01"), moment("2024-03-31"));
expect(result).toEqual(["202311", "202312", "202401", "202402", "202403"]);
});
});

// Yearly variants
describe("buildAllPossiblePeriods - Yearly", () => {
it("generates yearly periods", () => {
const result = buildAllPossiblePeriods("Yearly", moment("2020-01-01"), moment("2024-12-31"));
expect(result).toEqual(["2020", "2021", "2022", "2023", "2024"]);
});

it("generates FinancialApril periods", () => {
const result = buildAllPossiblePeriods("FinancialApril", moment("2023-03-01"), moment("2025-06-30"));
expect(result).toEqual(["2023April", "2024April"]);
});

it("generates FinancialJuly periods", () => {
const result = buildAllPossiblePeriods("FinancialJuly", moment("2023-06-01"), moment("2025-09-30"));
expect(result).toEqual(["2023July", "2024July"]);
});

it("generates FinancialOct periods", () => {
const result = buildAllPossiblePeriods("FinancialOct", moment("2023-09-01"), moment("2025-12-31"));
expect(result).toEqual(["2023Oct", "2024Oct"]);
});

// FinancialNov seems bugged in DHIS2 as its not following the "Financial Year X" starts in Year X convention of the rest
// See: https://dhis2.atlassian.net/browse/DHIS2-18609
it("generates FinancialNov periods", () => {
const result = buildAllPossiblePeriods("FinancialNov", moment("2023-10-01"), moment("2025-12-31"));
expect(result).toEqual(["2023Nov", "2024Nov"]);
});
});

// Quarterly Variants
describe("buildAllPossiblePeriods - Quarterly", () => {
it("generates Quarterly periods", () => {
const result = buildAllPossiblePeriods("Quarterly", moment("2023-12-01"), moment("2024-12-31"));
expect(result).toEqual(["2023Q4", "2024Q1", "2024Q2", "2024Q3", "2024Q4"]);
});

it("generates QuarterlyNov periods", () => {
const result = buildAllPossiblePeriods("QuarterlyNov", moment("2023-10-01"), moment("2024-08-31"));
expect(result).toEqual(["2023NovQ4", "2024NovQ1", "2024NovQ2", "2024NovQ3", "2024NovQ4"]);
});
});

// SixMonthly
describe("buildAllPossiblePeriods - SixMonthly", () => {
it("generates periods within same year", () => {
const result = buildAllPossiblePeriods("SixMonthly", moment("2024-01-01"), moment("2024-12-31"));
expect(result).toEqual(["2024S1", "2024S2"]);
});

it("generates periods across multiple years", () => {
const result = buildAllPossiblePeriods("SixMonthly", moment("2023-06-01"), moment("2024-12-31"));
expect(result).toEqual(["2023S1", "2023S2", "2024S1", "2024S2"]);
});
});

describe("buildAllPossiblePeriods - SixMonthlyApril", () => {
it("generates periods within same year", () => {
const result = buildAllPossiblePeriods("SixMonthlyApril", moment("2024-04-01"), moment("2024-09-30"));
expect(result).toEqual(["2024AprilS1"]);
});

it("generates periods across multiple years", () => {
const result = buildAllPossiblePeriods("SixMonthlyApril", moment("2022-10-01"), moment("2024-09-30"));
expect(result).toEqual(["2022AprilS2", "2023AprilS1", "2023AprilS2", "2024AprilS1"]);
});
});

describe("buildAllPossiblePeriods - SixMonthlyNov", () => {
it("generates periods within same year", () => {
const result = buildAllPossiblePeriods("SixMonthlyNov", moment("2025-01-01"), moment("2025-04-30"));
expect(result).toEqual(["2025NovS1"]);
});

it("generates periods across multiple years", () => {
const result = buildAllPossiblePeriods("SixMonthlyNov", moment("2023-10-01"), moment("2024-12-31"));
expect(result).toEqual(["2023NovS2", "2024NovS1", "2024NovS2", "2025NovS1"]);
});
});

// BiWeekly
describe("buildAllPossiblePeriods - BiWeekly", () => {
it("generates biweekly periods across multiple years", () => {
const result = buildAllPossiblePeriods("BiWeekly", moment("2024-12-01"), moment("2025-01-31"));
expect(result).toEqual(["2024BiW24", "2024BiW25", "2024BiW26", "2025BiW1", "2025BiW2", "2025BiW3"]);
});

it("generates BiW27 period in a 53 ISO week year", () => {
const result = buildAllPossiblePeriods("BiWeekly", moment("2020-12-14"), moment("2021-01-17"));
expect(result).toEqual(["2020BiW26", "2020BiW27", "2021BiW1"]);
});
});

// BiMonthly
describe("buildAllPossiblePeriods - BiMonthly", () => {
it("generates bimontly periods across multiple years", () => {
const result = buildAllPossiblePeriods("BiMonthly", moment("2023-01-01"), moment("2024-03-31"));
expect(result).toEqual([
"202301B",
"202302B",
"202303B",
"202304B",
"202305B",
"202306B",
"202401B",
"202402B",
]);
});
});
Loading
Loading