Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/data/common/Dhis2DataValueRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export class Dhis2DataValueRepository implements DataValueRepository {
period: dv.period,
categoryOptionComboId: dv.categoryOptionCombo,
isRequired,
comment: dv.comment || "",
};

const { type } = dataElement;
Expand Down Expand Up @@ -203,6 +204,7 @@ export class Dhis2DataValueRepository implements DataValueRepository {
period: combo.period,
categoryOptionComboId: combo.categoryOptionComboId,
isRequired: true,
comment: "",
};

const { type } = dataElement;
Expand Down
32 changes: 20 additions & 12 deletions src/domain/common/entities/DataValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface DataValueBase {
period: Period;
categoryOptionComboId: Id;
isRequired?: boolean;
comment: string;
}

export interface DataValueBoolean extends DataValueBase {
Expand Down Expand Up @@ -107,6 +108,8 @@ export type DataValue =
| DataValueDate
| DataValueMultiText;

export type DataValueLookup = Pick<DataValueBase, "orgUnitId" | "period" | "categoryOptionComboId">;

export type Period = string;

type DataValueSelector = string; // `${dataElementId.period.categoryOptionComboId}`
Expand Down Expand Up @@ -137,7 +140,7 @@ export class DataValueStore {
return new DataValueStore({ ...this.store, [key]: dataValue });
}

get(dataElement: DataElement, base: DataValueBase): Maybe<DataValue> {
get(dataElement: DataElement, base: DataValueLookup): Maybe<DataValue> {
const key = DataValueStore.getKey({
dataElementId: dataElement.id,
period: base.period,
Expand All @@ -148,7 +151,7 @@ export class DataValueStore {
return this.store[key] || getEmpty(dataElement, base);
}

getOrEmpty(dataElement: DataElement, base: DataValueBase): DataValue {
getOrEmpty(dataElement: DataElement, base: DataValueLookup): DataValue {
return this.get(dataElement, base) || getEmpty(dataElement, base);
}

Expand Down Expand Up @@ -176,32 +179,37 @@ export class DataValueStore {
}
}

export function getEmpty(dataElement: DataElement, base: DataValueBase): DataValue {
export function getEmpty(dataElement: DataElement, base: DataValueLookup): DataValue {
const { type } = dataElement;
const fullBase: DataValueBase = { comment: "", ...base };

switch (type) {
case "BOOLEAN":
return { ...base, dataElement, type: "BOOLEAN", isMultiple: false, value: undefined };
return { ...fullBase, dataElement, type: "BOOLEAN", isMultiple: false, value: undefined };
case "NUMBER":
return dataElement.options?.isMultiple
? { ...base, dataElement, type: "NUMBER", isMultiple: true, values: [] }
: { ...base, dataElement, type: "NUMBER", isMultiple: false, value: "" };
? { ...fullBase, dataElement, type: "NUMBER", isMultiple: true, values: [] }
: { ...fullBase, dataElement, type: "NUMBER", isMultiple: false, value: "" };
case "TEXT":
return dataElement.options?.isMultiple
? { ...base, dataElement, type: "TEXT", isMultiple: true, values: [] }
: { ...base, dataElement, type: "TEXT", isMultiple: false, value: "" };
? { ...fullBase, dataElement, type: "TEXT", isMultiple: true, values: [] }
: { ...fullBase, dataElement, type: "TEXT", isMultiple: false, value: "" };
case "MULTI_TEXT":
return { ...base, dataElement, type: "MULTI_TEXT", isMultiple: true, values: [] };
return { ...fullBase, dataElement, type: "MULTI_TEXT", isMultiple: true, values: [] };
case "PERCENTAGE":
return { ...base, dataElement, type: "PERCENTAGE", isMultiple: false, value: "" };
return { ...fullBase, dataElement, type: "PERCENTAGE", isMultiple: false, value: "" };
case "FILE":
return { ...base, dataElement, type: "FILE", file: undefined, isMultiple: false };
return { ...fullBase, dataElement, type: "FILE", file: undefined, isMultiple: false };
case "DATE":
return { ...base, dataElement, type: "DATE", value: undefined, isMultiple: false };
return { ...fullBase, dataElement, type: "DATE", value: undefined, isMultiple: false };

default:
assertUnreachable(type);
}
}

export function hasComment(dataValue: Pick<DataValueBase, "comment">): boolean {
return dataValue.comment.length > 0;
}

export const MULTI_TEXT_SEPARATOR = ",";
44 changes: 44 additions & 0 deletions src/domain/common/entities/__tests__/CommentIndicator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { DataValueStore, hasComment } from "../DataValue";
import { dataElementText, dataValueBase, dataValueTextSingle, dataValueTextWithComment } from "./dataFixtures";

describe("Comment indicator", () => {
describe("DataValue comment field", () => {
it("should be an empty string when data value has no comment", () => {
expect(dataValueTextSingle.comment).toEqual("");
});

it("should contain the comment string when data value has a comment", () => {
expect(dataValueTextWithComment.comment).toEqual("This value needs review");
});
});

describe("DataValueStore with comments", () => {
it("should preserve comment when storing a data value with a comment", () => {
const store = DataValueStore.from([dataValueTextWithComment]);
const retrieved = store.get(dataElementText, dataValueBase);
expect(retrieved?.comment).toEqual("This value needs review");
});

it("should return empty string comment for data values without comments", () => {
const store = DataValueStore.from([dataValueTextSingle]);
const retrieved = store.get(dataElementText, dataValueBase);
expect(retrieved?.comment).toEqual("");
});

it("should return empty string comment for empty data values", () => {
const store = new DataValueStore({});
const retrieved = store.getOrEmpty(dataElementText, dataValueBase);
expect(retrieved.comment).toEqual("");
});
});

describe("hasComment", () => {
it("should return true when comment is a non-empty string", () => {
expect(hasComment(dataValueTextWithComment)).toBe(true);
});

it("should return false when comment is an empty string", () => {
expect(hasComment(dataValueTextSingle)).toBe(false);
});
});
});
10 changes: 10 additions & 0 deletions src/domain/common/entities/__tests__/dataFixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const dataValueBase: DataValueBase = {
orgUnitId: "org1",
period: "202101",
categoryOptionComboId: "coc1",
comment: "",
};

export const dataValueTextSingle: DataValueTextSingle = {
Expand All @@ -98,4 +99,13 @@ export const dataValueTextSingle: DataValueTextSingle = {
value: "Sample text",
};

export const dataValueTextWithComment: DataValueTextSingle = {
...dataValueBase,
type: "TEXT",
isMultiple: false,
dataElement: dataElementText,
value: "Sample text",
comment: "This value needs review",
};

export const dataValues: DataValue[] = [dataValueTextSingle];
4 changes: 4 additions & 0 deletions src/domain/common/usecases/__tests__/data/dataValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const dataValueText: DataValueTextSingle = {
type: "TEXT",
value: "10",
isRequired: false,
comment: "",
};

export const dataValueTextMultiple: DataValueTextMultiple = {
Expand All @@ -52,6 +53,7 @@ export const dataValueTextMultiple: DataValueTextMultiple = {
values: ["value1", "value2"],
type: "TEXT",
isMultiple: true,
comment: "",
};

export const dataValueNumberSingle: DataValueNumberSingle = {
Expand All @@ -70,6 +72,7 @@ export const dataValueNumberSingle: DataValueNumberSingle = {
type: "NUMBER",
isMultiple: false,
isRequired: false,
comment: "",
};

export const dataValueFile: DataValueFile = {
Expand All @@ -86,4 +89,5 @@ export const dataValueFile: DataValueFile = {
url: "/path/to/file",
},
isRequired: false,
comment: "",
};
18 changes: 15 additions & 3 deletions src/webapp/reports/autogenerated-forms/CommentIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ export interface CommentIconProps {
dataElementId: Id;
categoryOptionComboId: Id;
period: string;
hasComment: boolean;
}

export const CommentIcon: React.FC<CommentIconProps> = React.memo(props => {
const { dataElementId, period, categoryOptionComboId } = props;
const { dataElementId, period, categoryOptionComboId, hasComment } = props;
const { api } = useAppContext();
const tagId = `${period}-${dataElementId}-${categoryOptionComboId}-comment`;
const title = i18n.t("View comment and audit history");
Expand All @@ -32,11 +33,22 @@ export const CommentIcon: React.FC<CommentIconProps> = React.memo(props => {
title={title}
style={styles.image}
></img>
{hasComment && <span style={styles.badge} />}
</div>
);
});

const styles = {
wrapper: { marginInlineStart: 10 },
const styles: Record<string, React.CSSProperties> = {
wrapper: { marginInlineStart: 10, position: "relative" },
image: { cursor: "pointer" },
badge: {
position: "absolute",
top: -2,
right: -4,
width: 8,
height: 8,
borderRadius: "50%",
backgroundColor: "#4CAF50",
border: "1.5px solid white",
},
};
15 changes: 14 additions & 1 deletion src/webapp/reports/autogenerated-forms/DataElementItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { DataElement } from "../../../domain/common/entities/DataElement";
import { DataFormInfo } from "./AutogeneratedForm";
import { CommentIcon } from "./CommentIcon";
import DataEntryItem, { DataEntryItemProps } from "./DataEntryItem";
import { DataValue } from "../../../domain/common/entities/DataValue";
import { DataValue, hasComment as dataValueHasComment } from "../../../domain/common/entities/DataValue";
import { Row } from "./GridWithTotalsViewModel";
import { isDev } from "../../..";
import { Maybe } from "../../../utils/ts-utils";
Expand Down Expand Up @@ -47,6 +47,18 @@ export const DataElementItem: React.FC<DataElementItemProps> = React.memo(props
const elId = _([dataElement.id, period]).compact().join("-");
const dataElementCocId = dataElement.cocId ?? dataFormInfo.categoryOptionComboId;
const auditId = _([dataElement.orgUnit, dataElement.id, dataElementCocId, "val"]).compact().join("-");
const hasComment = React.useMemo(
() =>
!noComment &&
dataValueHasComment(
dataFormInfo.data.values.getOrEmpty(dataElement, {
orgUnitId: dataElement.orgUnit || dataFormInfo.orgUnitId,
period: period || dataFormInfo.period,
categoryOptionComboId: dataElementCocId,
})
),
[noComment, dataFormInfo.data.values, dataElement, dataFormInfo.orgUnitId, period, dataFormInfo.period, dataElementCocId]
);
const notifyParent = React.useCallback<DataEntryItemProps["onValueChange"]>(
async dataValue => {
if (onChange) onChange(dataValue);
Expand Down Expand Up @@ -112,6 +124,7 @@ export const DataElementItem: React.FC<DataElementItemProps> = React.memo(props
dataElementId={dataElement.id}
categoryOptionComboId={dataElementCocId}
period={period || dataFormInfo.period}
hasComment={hasComment}
/>
</div>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { DataElement } from "../../../domain/common/entities/DataElement";
import { DataFormInfo } from "./AutogeneratedForm";
import { DebugLabel } from "../../components/debug/DebugLabel";
import { CustomInput } from "./widgets/NumberWidget";
import { DataValue, DataValueBase, DateObj } from "../../../domain/common/entities/DataValue";
import { DataValue, DataValueBase, DataValueLookup, DateObj } from "../../../domain/common/entities/DataValue";
import { resolveDataElement } from "./utils/resolveDataElement";
import { getValueAccordingType } from "./hooks/useApplyRules";

Expand All @@ -18,14 +18,13 @@ export interface MirrorDataElementItemProps {
export function getMirrorDataValue(
dataFormInfo: DataFormInfo,
dataElement: DataElement,
base: Pick<DataValueBase, "period" | "categoryOptionComboId" | "isRequired">
base: Pick<DataValueBase, "period" | "categoryOptionComboId">
): DataValue {
const resolvedDataElement = resolveDataElement(dataFormInfo, dataElement);
const dataValueBase: DataValueBase = {
const dataValueBase: DataValueLookup = {
orgUnitId: dataFormInfo.orgUnitId,
period: base.period,
categoryOptionComboId: base.categoryOptionComboId,
isRequired: base.isRequired,
};
return dataFormInfo.data.values.getOrEmpty(resolvedDataElement, dataValueBase);
}
Expand Down
Loading