diff --git a/app/airflow/dags/libs/queries.py b/app/airflow/dags/libs/queries.py index d37e06490..e110761e3 100644 --- a/app/airflow/dags/libs/queries.py +++ b/app/airflow/dags/libs/queries.py @@ -139,7 +139,8 @@ -- Because concepts may or may not have the standard_concept_id, in general. And we prefer to use the standard_concept_id, if it exists. WHERE target_concept.concept_id = COALESCE(temp_existing_concepts.standard_concept_id, temp_existing_concepts.source_concept_id); --- When the scan report table is a death table, override dest_table_id to the death OMOP table so rules show destination table: death, field: cause_concept_id +-- If the scan report table has the "death_table" flag set to TRUE, set the destination table to OMOP's "death" table. +-- This ensures that mapping rules will use "death" as the destination table and, for example, will select "cause_concept_id" as the mapped field. UPDATE temp_existing_concepts_%(table_id)s temp_existing_concepts SET dest_table_id = (SELECT id FROM mapping_omoptable WHERE "table" = 'death' LIMIT 1) WHERE (SELECT death_table FROM mapping_scanreporttable WHERE id = %(table_id)s) = TRUE; diff --git a/app/next-client-app/api/scanreports.ts b/app/next-client-app/api/scanreports.ts index 8ac524bf3..4ee080c60 100644 --- a/app/next-client-app/api/scanreports.ts +++ b/app/next-client-app/api/scanreports.ts @@ -149,6 +149,7 @@ export async function getScanReportTable( permissions: [], jobs: [], trigger_reuse: true, + death_table: false, }; } } diff --git a/app/next-client-app/app/(protected)/scanreports/[id]/columns.tsx b/app/next-client-app/app/(protected)/scanreports/[id]/columns.tsx index 971211261..d32f0285b 100644 --- a/app/next-client-app/app/(protected)/scanreports/[id]/columns.tsx +++ b/app/next-client-app/app/(protected)/scanreports/[id]/columns.tsx @@ -3,6 +3,7 @@ import { ColumnDef } from "@tanstack/react-table"; import { DataTableColumnHeader } from "@/components/data-table/DataTableColumnHeader"; import { EditButton } from "@/components/scanreports/EditButton"; +import { Tooltips } from "@/components/core/Tooltips"; import JobDialog from "@/components/jobs/JobDialog"; import { FindGeneralStatus, DivideJobs } from "@/components/jobs/JobUtils"; import Link from "next/link"; @@ -89,6 +90,28 @@ export const columns: ColumnDef[] = [ ); } }, + { + id: "note", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { death_table } = row.original; + return ( +
+ {death_table && ( +

+ {" "} + Death table + +

+ )} +
+ ); + } + }, { id: "edit", header: ({ column }) => , diff --git a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/page.tsx b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/page.tsx index 5b7717bfb..0b029769d 100644 --- a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/page.tsx +++ b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/page.tsx @@ -72,7 +72,9 @@ export default async function ScanReportsValue(props: ScanReportsValueProps) { id={id} tableId={tableId} fieldId={fieldId} - tableName={table.name} + tableName={ + table.death_table ? `${table.name} (Death table)` : table.name + } fieldName={field.name} variant="field" /> diff --git a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/update/page.tsx b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/update/page.tsx index 1980563e3..9dd2c7b31 100644 --- a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/update/page.tsx +++ b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/fields/[fieldId]/update/page.tsx @@ -40,7 +40,9 @@ export default async function ScanReportsEditField(props: ScanReportsEditFieldPr id={id} tableId={tableId} fieldId={fieldId} - tableName={table.name} + tableName={ + table.death_table ? `${table.name} (Death table)` : table.name + } fieldName={field.name} variant="fieldUpdate" /> diff --git a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/page.tsx b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/page.tsx index 4104c99d6..63563e5bb 100644 --- a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/page.tsx +++ b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/page.tsx @@ -60,7 +60,11 @@ export default async function ScanReportsField(props: ScanReportsFieldProps) {
diff --git a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/update/page.tsx b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/update/page.tsx index a89c48f76..c02281e29 100644 --- a/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/update/page.tsx +++ b/app/next-client-app/app/(protected)/scanreports/[id]/tables/[tableId]/update/page.tsx @@ -74,7 +74,7 @@ export default async function UpdateTable(props: UpdateTableProps) {
{(table.date_event === null || table.person_id === null) && ( diff --git a/app/next-client-app/components/core/Tooltips.tsx b/app/next-client-app/components/core/Tooltips.tsx index 7b17b3b66..852855967 100644 --- a/app/next-client-app/components/core/Tooltips.tsx +++ b/app/next-client-app/components/core/Tooltips.tsx @@ -48,4 +48,4 @@ export function Tooltips({ ); -} +} \ No newline at end of file diff --git a/app/next-client-app/components/form-components/FormikSelect.tsx b/app/next-client-app/components/form-components/FormikSelect.tsx index c528c2e4f..ae7121cca 100644 --- a/app/next-client-app/components/form-components/FormikSelect.tsx +++ b/app/next-client-app/components/form-components/FormikSelect.tsx @@ -1,8 +1,6 @@ import { Field, FieldInputProps, FieldProps, FormikProps } from "formik"; import Select from "react-select"; import makeAnimated from "react-select/animated"; -import config from "@/tailwind.config"; -import { useTheme } from "next-themes"; type Option = Object & { value: number; @@ -36,8 +34,8 @@ const CustomSelect = ({ const onChange = (newValue: any, actionMeta: any) => { const selectedValues = isMulti - ? (newValue as Option[]).map((option) => option.value) - : (newValue as Option).value; + ? ((newValue as Option[] | null) ?? []).map((option) => option.value) + : ((newValue as Option | null)?.value ?? null); form.setFieldValue(field.name, selectedValues); }; diff --git a/app/next-client-app/components/scanreports/ScanReportTableUpdateForm.tsx b/app/next-client-app/components/scanreports/ScanReportTableUpdateForm.tsx index 2cad5ab96..50e3258d4 100644 --- a/app/next-client-app/components/scanreports/ScanReportTableUpdateForm.tsx +++ b/app/next-client-app/components/scanreports/ScanReportTableUpdateForm.tsx @@ -1,21 +1,67 @@ "use client"; +import { useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { updateScanReportTable } from "@/api/scanreports"; -import { Save } from "lucide-react"; +import { Check, Save, X } from "lucide-react"; import { toast } from "sonner"; import { FormDataFilter } from "../form-components/FormikUtils"; -import { Formik } from "formik"; +import { Formik, useFormikContext } from "formik"; import { FormField, FormItem, FormLabel, FormControl, FormMessage, FormDescription } from "@/components/ui/form"; import { FormikSelect } from "../form-components/FormikSelect"; import { useRouter } from "next/navigation"; import { Checkbox } from "../ui/checkbox"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Tooltips } from "@/components/core/Tooltips"; import { enableReuseTriggerOption } from "@/constants"; interface FormData { personId: number | null; dateEvent: number | null; triggerReuse: boolean; + death_table: boolean; +} + +function DeathDateModalController({ + deathDateFieldId, + setIsDialogOpen, + pendingRevertRef, +}: { + deathDateFieldId?: number; + setIsDialogOpen: (open: boolean) => void; + pendingRevertRef: React.MutableRefObject; +}) { + const { values, setFieldValue } = useFormikContext(); + const prevDateEventRef = useRef(values.dateEvent); + + useEffect(() => { + const prev = prevDateEventRef.current; + const next = values.dateEvent; + prevDateEventRef.current = next; + + if (!deathDateFieldId || next === prev) return; + + if (next === deathDateFieldId && !values.death_table) { + pendingRevertRef.current = prev ?? null; + setIsDialogOpen(true); + return; + } + + if (values.death_table && next !== deathDateFieldId) { + setFieldValue("death_table", false); + } + }, [deathDateFieldId, values.dateEvent, values.death_table]); + + return null; } export function ScanReportTableUpdateForm({ @@ -32,6 +78,8 @@ export function ScanReportTableUpdateForm({ dateEvent: ScanReportField; }) { const router = useRouter(); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const pendingDateEventRevertRef = useRef(undefined); const canUpdate = permissions.includes("CanEdit") || permissions.includes("CanAdmin"); @@ -39,12 +87,16 @@ export function ScanReportTableUpdateForm({ const initialPersonId = FormDataFilter(personId); const initialDateEvent = FormDataFilter(dateEvent); + const deathDateField = scanreportFields.find( + (field) => field.name?.trim().toLowerCase() === "death_date", + ); const handleSubmit = async (data: FormData) => { const submittingData = { person_id: data.personId !== 0 ? data.personId : null, date_event: data.dateEvent !== 0 ? data.dateEvent : null, trigger_reuse: data.triggerReuse, + death_table: data.death_table, }; const response = await updateScanReportTable( @@ -68,6 +120,7 @@ export function ScanReportTableUpdateForm({ dateEvent: initialDateEvent[0].value, personId: initialPersonId[0].value, triggerReuse: scanreportTable.trigger_reuse, + death_table: Boolean(scanreportTable.death_table), }} onSubmit={(data) => { handleSubmit(data); @@ -76,6 +129,11 @@ export function ScanReportTableUpdateForm({ {({ handleSubmit, values, setFieldValue }) => (
+ Person ID @@ -109,6 +167,93 @@ export function ScanReportTableUpdateForm({ + +
+ + Does this table contain only death data for the OMOP CDM Death + table? + + + { + setIsDialogOpen(open); + if (!open && pendingDateEventRevertRef.current !== undefined) { + setFieldValue("dateEvent", pendingDateEventRevertRef.current); + pendingDateEventRevertRef.current = undefined; + } + }} + > + + + Please Confirm Your Choice + + Are you sure you want to set this table as a Death table? + Doing so will result in the following: +
    +
  • + Mapping Rules that are created either manually or + automatically (built from OMOP vocabulary or Reused) + will have Destination table as{" "} + Death. +
  • +
  • + All concepts in this table will be recognised as{" "} + Cause of Death in + OMOP CDM. +
  • +
  • + Destination of Date Event will be{" "} + Death date field in + OMOP CDM. +
  • +
+

+ You can turn off this setting later. Mapping rules will + be refreshed when you save. +

+
+
+ + + + +
+
+ { + if (checked) { + setIsDialogOpen(true); + } else { + setFieldValue("death_table", false); + } + }} + disabled={!canUpdate} + /> + +
+
+ {enableReuseTriggerOption === "true" && ( {({ field }) => ( diff --git a/app/next-client-app/types/scanreport.ts b/app/next-client-app/types/scanreport.ts index 7b04c72ce..8dc1a5a72 100644 --- a/app/next-client-app/types/scanreport.ts +++ b/app/next-client-app/types/scanreport.ts @@ -29,6 +29,7 @@ interface ScanReportTable { permissions: Permission[]; jobs: Job[]; trigger_reuse: boolean; + death_table?: boolean; } interface ScanReportField {