Skip to content

Commit e162f97

Browse files
authored
Merge pull request #505 from EyeSeeTea/fix/period-dates-data-entry
Fix: add period dates and update data entry
2 parents 608a8b2 + 3cc190f commit e162f97

File tree

8 files changed

+206
-50
lines changed

8 files changed

+206
-50
lines changed

src/data/D2ApiIndicator.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,6 @@ export class D2ApiIndicator {
277277
const hideInApp = dataElement.attributeValues.find(
278278
attribute => attribute.attribute.id === config.attributes.hideInApp.id
279279
);
280-
if (hideInApp) console.log(hideInApp, dataElement);
281280
if (hideInApp && hideInApp.value === "true") return undefined;
282281

283282
const theme = dataElement.dataElementGroups.find(deg =>

src/data/entry-form/CustomForm.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { convertAttributeValueToDate } from "$/data/utils";
1919
import templateVelocity from "$/data/entry-form/template.vm?raw";
2020
import templateJs from "$/data/entry-form/template.js?raw";
2121
import templateCss from "$/data/entry-form/template.css?raw";
22+
import { toISODateWithoutTimezone } from "$/utils/date";
2223

2324
function getCategoryCombo(dataSetElement: DataSetTemplate["dataSetElements"][0]) {
2425
const { categoryCombo } = dataSetElement;
@@ -423,18 +424,14 @@ function mapDataElementRefs(
423424
}
424425

425426
function generatePeriods(dataSet: DataSetTemplate, d2Config: D2Config): Maybe<TemplatePeriodDate> {
426-
const outComeAttribute = dataSet.attributeValues.find(
427-
attr => attr.attribute.id === d2Config.attributes.outcomeDates.id
427+
const periodDates = dataSet.attributeValues.find(
428+
attr => attr.attribute.id === d2Config.attributes.periodDates.id
428429
);
429-
const outPutAttribute = dataSet.attributeValues.find(
430-
attr => attr.attribute.id === d2Config.attributes.outputDates.id
431-
);
432-
if (!outComeAttribute || !outPutAttribute) return undefined;
430+
if (!periodDates) return undefined;
433431

434-
const outComeValidYears = generatePeriodsFromAttributeValues(outComeAttribute);
435-
const outPutValidYears = generatePeriodsFromAttributeValues(outPutAttribute);
432+
const periodDateValidYears = generatePeriodsFromAttributeValues(periodDates);
436433

437-
return { output: outPutValidYears, outcome: outComeValidYears };
434+
return { output: periodDateValidYears, outcome: periodDateValidYears };
438435
}
439436

440437
function generateIndicatorMatchingReference(dataSet: DataSet) {
@@ -458,8 +455,10 @@ function generatePeriodsFromAttributeValues(
458455
return [
459456
year,
460457
{
461-
start: convertAttributeValueToDate(startDate),
462-
end: convertAttributeValueToDate(endDate),
458+
start: toISODateWithoutTimezone(
459+
new Date(convertAttributeValueToDate(startDate))
460+
),
461+
end: toISODateWithoutTimezone(new Date(convertAttributeValueToDate(endDate))),
463462
},
464463
] as [string, TemplateDate];
465464
})

src/data/repositories/DataSetD2Repository.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,7 @@ export class DataSetD2Repository implements DataSetRepository {
687687
notifyCompletingUser: dataSet.notifyUser,
688688
openFuturePeriods: dataSet.openFuturePeriods,
689689
expiryDays: dataSet.expiryDays,
690+
dataInputPeriods: this.buildDataInputPeriod(dataSet),
690691
};
691692
}
692693

@@ -746,6 +747,18 @@ export class DataSetD2Repository implements DataSetRepository {
746747
return [...filteredExisting, ...attributesToSave];
747748
}
748749

750+
private buildDataInputPeriod(dataSet: DataSetToSave): D2DataSetToSave["dataInputPeriods"] {
751+
return (
752+
dataSet.periodDate?.dataInputPeriods.map(p => ({
753+
closingDate: p.endDate,
754+
openingDate: p.startDate,
755+
period: {
756+
id: p.period,
757+
},
758+
})) ?? []
759+
);
760+
}
761+
749762
private parsePeriodDate(
750763
dataSetToSave: DataSetToSave,
751764
attributes: D2Config["attributes"]
@@ -877,4 +890,5 @@ type D2DataSetToSave = {
877890
users: Record<Id, D2ApiSharingName>;
878891
userGroups: Record<Id, D2ApiSharingName>;
879892
};
893+
dataInputPeriods: { closingDate: string; openingDate: string; period: Ref }[];
880894
};

src/domain/entities/DataSet.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,25 @@ export class DataSet extends Struct<DataSetAttrs>() {
7373
const orgsUnits = project ? project.orgsUnits : this.orgUnits;
7474

7575
const accessGroupsFromProject = this.getAccessFromProject(project, config);
76+
//TODO: confirm period data value when project updates
77+
// should default be current periodDate or undefined?
78+
const periodDate =
79+
project?.startDate && project?.endDate
80+
? DatePeriod.create({
81+
startDate: project.startDate,
82+
endDate: project.endDate,
83+
periods: [],
84+
}).initializePeriods(config)
85+
: undefined;
7686

7787
return this._update({
7888
access: accessGroupsFromProject,
7989
shortName,
8090
project,
8191
name,
8292
orgUnits: orgsUnits,
93+
periodDate,
94+
openFuturePeriods: DatePeriod.getFuturePeriods(periodDate?.endDate),
8395
});
8496
}
8597

src/domain/entities/DatePeriod.ts

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
import { Struct } from "$/domain/entities/generic/Struct";
2+
import { Config } from "$/domain/entities/Config";
3+
import { addToDate, toISODateWithoutTimezone, getDiff, stringToTime } from "$/utils/date";
4+
import _, { Collection } from "$/domain/entities/generic/Collection";
5+
import { Maybe } from "$/utils/ts-utils";
26

3-
export type PeriodDetailsAttrs = { year: number; startDate: string; endDate: string };
4-
type DatePeriodAttrs = { startDate: string; endDate: string; periods: PeriodDetailsAttrs[] };
7+
const DEFAULT_FUTURE_PERIODS = 1;
8+
9+
export type YearlyPeriodDetailsAttrs = {
10+
year: number;
11+
startDate: string;
12+
endDate: string;
13+
};
14+
15+
type DatePeriodAttrs = { startDate: string; endDate: string; periods: YearlyPeriodDetailsAttrs[] };
516

617
export class DatePeriod extends Struct<DatePeriodAttrs>() {
718
get years() {
@@ -24,10 +35,87 @@ export class DatePeriod extends Struct<DatePeriodAttrs>() {
2435
}));
2536
}
2637

38+
get dataInputPeriods(): MonthlyPeriodDetails[] {
39+
const { periods, startDate: startDateStr, endDate: endDateStr } = this;
40+
if (!periods.length) {
41+
return [];
42+
}
43+
44+
const allStartTimes = this.extractPeriodDateAsTimes("startDate");
45+
const allEndTimes = this.extractPeriodDateAsTimes("endDate");
46+
47+
if (!allStartTimes.length || !allEndTimes.length) {
48+
return [];
49+
}
50+
51+
const startDate = new Date(startDateStr);
52+
const endDate = new Date(endDateStr);
53+
54+
const openingDate = new Date(Math.min(...allStartTimes, startDate.getTime()));
55+
const closingDate = new Date(Math.max(...allEndTimes, endDate.getTime()));
56+
57+
const startYear = startDate.getFullYear();
58+
const startMonth = startDate.getMonth();
59+
60+
const totalMonths = Math.round(getDiff(startDate, endDate, "month"));
61+
62+
return Collection.range(0, totalMonths)
63+
.map(monthOffset => {
64+
const targetYear = startYear + Math.floor((startMonth + monthOffset) / 12);
65+
const targetMonth = ((startMonth + monthOffset) % 12) + 1;
66+
67+
return MonthlyPeriodDetails.create({
68+
year: targetYear,
69+
month: targetMonth,
70+
startDate: toISODateWithoutTimezone(openingDate),
71+
endDate: toISODateWithoutTimezone(closingDate),
72+
});
73+
})
74+
.value();
75+
}
76+
77+
private extractPeriodDateAsTimes<K extends "startDate" | "endDate">(field: K) {
78+
return _(this.periods)
79+
.compactMap(p => stringToTime(p[field]))
80+
.value();
81+
}
82+
83+
generatePeriods(config: DataPeriodConfig): DatePeriod["periods"] {
84+
const { startDate, endDate, periods, years } = this;
85+
86+
const month = config.periodEndDateMonth;
87+
const day = config.periodEndDateDay;
88+
const units = config.periodLastYearUnits;
89+
const unitValue = config.periodLastYearEndDate;
90+
const lastYear = years[years.length - 1];
91+
92+
return years.map(year => {
93+
const currentPeriod = periods.find(period => period.year === year);
94+
95+
const defaultEndDate = new Date(year + 1, month - 1, day, 0, 0, 0).toISOString();
96+
97+
const lastYearEndDate =
98+
units && unitValue ? addToDate(endDate ?? "", units, unitValue) : endDate;
99+
100+
const endM = year === lastYear ? lastYearEndDate : defaultEndDate;
101+
102+
return {
103+
year,
104+
startDate: currentPeriod?.startDate ?? startDate ?? "",
105+
endDate: currentPeriod?.endDate ?? endM ?? "",
106+
};
107+
});
108+
}
109+
110+
initializePeriods(config: DataPeriodConfig): DatePeriod {
111+
const periods = this.generatePeriods(config);
112+
return this.updatedPeriods(periods);
113+
}
114+
27115
private buildShortFormat(date: string) {
28116
if (!date) return "";
29117

30-
const datePart = new Date(date).toISOString().split("T")[0];
118+
const datePart = toISODateWithoutTimezone(new Date(date)).split("T")[0];
31119
if (!datePart) return "";
32120
return datePart.replace(/-/g, "");
33121
}
@@ -46,7 +134,30 @@ export class DatePeriod extends Struct<DatePeriodAttrs>() {
46134
return this._update({ [fieldName]: value });
47135
}
48136

49-
updatedPeriods(periods: PeriodDetailsAttrs[]): DatePeriod {
137+
updatedPeriods(periods: YearlyPeriodDetailsAttrs[]): DatePeriod {
50138
return this._update({ periods });
51139
}
140+
141+
static getFuturePeriods(endDateStr: Maybe<string>): number {
142+
if (!endDateStr) return DEFAULT_FUTURE_PERIODS;
143+
144+
const end = new Date(endDateStr);
145+
const now = new Date();
146+
147+
const monthsDiff = Math.ceil(getDiff(now, end, "month"));
148+
149+
return Math.max(monthsDiff + 1, DEFAULT_FUTURE_PERIODS);
150+
}
52151
}
152+
153+
export type MonthlyPeriodDetailsAttrs = YearlyPeriodDetailsAttrs & { month: number };
154+
export class MonthlyPeriodDetails extends Struct<MonthlyPeriodDetailsAttrs>() {
155+
get period() {
156+
return `${this.year}${String(this.month).padStart(2, "0")}`;
157+
}
158+
}
159+
160+
type DataPeriodConfig = Pick<
161+
Config,
162+
"periodEndDateMonth" | "periodEndDateDay" | "periodLastYearUnits" | "periodLastYearEndDate"
163+
>;

src/domain/usecases/SavePeriodDateUseCase.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { Future, FutureData } from "$/domain/entities/generic/Future";
22
import { DataSet } from "$/domain/entities/DataSet";
33
import { Id } from "$/domain/entities/Ref";
44
import { DataSetRepository } from "$/domain/repositories/DataSetRepository";
5-
import _ from "$/domain/entities/generic/Collection";
65
import { LogRepository } from "$/domain/repositories/LogRepository";
76
import { UserUtils } from "$/domain/usecases/common/UserUtils";
87
import { UserRepository } from "$/domain/repositories/UserRepository";
@@ -22,7 +21,11 @@ export class SavePeriodDateUseCase {
2221
return this.getDataSetsByIds(options.dataSetsIds).flatMap(dataSets => {
2322
return this.userUtils.checkDataSetAccess(dataSets).flatMap(() => {
2423
const dataSetsToSave = dataSets.map(dataSet => {
25-
return DataSet.create({ ...dataSet, periodDate: options.periodDate });
24+
return DataSet.create({
25+
...dataSet,
26+
periodDate: options.periodDate,
27+
openFuturePeriods: DatePeriod.getFuturePeriods(options.periodDate.endDate),
28+
});
2629
});
2730
return this.dataSetRepository
2831
.save(dataSetsToSave)

src/utils/date.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ISODateString } from "$/domain/entities/Ref";
22
import { UnitDate } from "$/domain/entities/UnitDate";
33
import i18n from "$/utils/i18n";
44
import { DropdownItem } from "@eyeseetea/d2-ui-components";
5+
import { Maybe } from "$/utils/ts-utils";
56

67
export function toLongDateString(isoDate: ISODateString, options?: Intl.DateTimeFormatOptions) {
78
return new Date(isoDate).toLocaleString("default", {
@@ -37,6 +38,38 @@ export function addToDate(date: string, units: UnitDate, unitValue: number): str
3738
return newDate.toISOString();
3839
}
3940

41+
export function getDiff(dateA: Date, dateB: Date, unit: UnitDate): number {
42+
const diffTime = dateB.getTime() - dateA.getTime();
43+
44+
switch (unit) {
45+
case "day":
46+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
47+
case "month": {
48+
const monthDiff =
49+
12 * (dateB.getFullYear() - dateA.getFullYear()) +
50+
dateB.getMonth() -
51+
dateA.getMonth();
52+
if (dateA.getTime() !== dateB.getTime()) {
53+
const dateADay = dateA.getDate();
54+
const dateBDay = dateB.getDate();
55+
const daysInMonth = new Date(
56+
dateB.getFullYear(),
57+
dateB.getMonth() + 1,
58+
0
59+
).getDate();
60+
const dayFraction = (dateBDay - dateADay) / daysInMonth;
61+
return monthDiff + dayFraction;
62+
} else {
63+
return monthDiff;
64+
}
65+
}
66+
case "week":
67+
return Math.ceil(diffTime / (1000 * 60 * 60 * 24 * 7));
68+
default:
69+
throw new Error("Invalid Date Unit");
70+
}
71+
}
72+
4073
export function getMonths() {
4174
const currentYear = new Date().getFullYear();
4275
return Array.from({ length: 12 }, (_, i) => ({
@@ -67,3 +100,17 @@ export function getDaysPerMonthYear(
67100
value: String(index + 1),
68101
}));
69102
}
103+
104+
export function toISODateWithoutTimezone(date: Date) {
105+
const dateParts = [
106+
date.getFullYear(),
107+
String(date.getMonth() + 1).padStart(2, "0"),
108+
String(date.getDate()).padStart(2, "0"),
109+
];
110+
return dateParts.join("-") + "T00:00:00.000";
111+
}
112+
113+
export function stringToTime(dateStr: Maybe<string>): Maybe<number> {
114+
const time = dateStr ? new Date(dateStr).getTime() : NaN;
115+
return isNaN(time) ? undefined : time;
116+
}

src/webapp/components/dataset-periods/DataSetPeriodDates.tsx

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { Typography } from "@material-ui/core";
55

66
import { Id } from "$/domain/entities/Ref";
77
import { useGetDataSetsByIds } from "$/webapp/hooks/useDataSets";
8-
import { addToDate } from "$/utils/date";
9-
import { DatePeriod, PeriodDetailsAttrs } from "$/domain/entities/DatePeriod";
8+
import { DatePeriod, YearlyPeriodDetailsAttrs } from "$/domain/entities/DatePeriod";
109
import i18n from "$/utils/i18n";
1110
import { useAppContext } from "$/webapp/contexts/app-context";
1211

@@ -19,36 +18,8 @@ export type DataSetPeriodDatesProps = {
1918
function useGetYears(props: { period: DatePeriod }) {
2019
const { config } = useAppContext();
2120
const { period } = props;
22-
const { startDate, endDate, periods, years } = period;
2321

24-
const lastYear = years[years.length - 1];
25-
26-
const periodsByYear = React.useMemo(() => {
27-
if (!startDate || !endDate) return [];
28-
29-
return years.map((year): DatePeriod["periods"][number] => {
30-
const month = config.periodEndDateMonth;
31-
const day = config.periodEndDateDay;
32-
const units = config.periodLastYearUnits;
33-
const unitValue = config.periodLastYearEndDate;
34-
const currentPeriod = periods.find(period => period.year === year);
35-
36-
const defaultEndDate = new Date(year + 1, month - 1, day, 0, 0, 0).toISOString();
37-
38-
const lastYearEndDate =
39-
units && unitValue ? addToDate(endDate ?? "", units, unitValue) : endDate;
40-
41-
const endM = year === lastYear ? lastYearEndDate : defaultEndDate;
42-
43-
return {
44-
year,
45-
startDate: currentPeriod?.startDate ?? startDate ?? "",
46-
endDate: currentPeriod?.endDate ?? endM ?? "",
47-
};
48-
});
49-
}, [years, startDate, endDate, lastYear, periods, config]);
50-
51-
return periodsByYear;
22+
return React.useMemo(() => period.generatePeriods(config), [period, config]);
5223
}
5324

5425
const emptyPeriodDate = DatePeriod.create({
@@ -76,7 +47,7 @@ export const DataSetPeriodDates = React.memo((props: DataSetPeriodDatesProps) =>
7647
});
7748

7849
const updatePeriod = (
79-
period: PeriodDetailsAttrs,
50+
period: YearlyPeriodDetailsAttrs,
8051
value: string,
8152
fieldName: "startDate" | "endDate"
8253
) => {

0 commit comments

Comments
 (0)