Skip to content
Open
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
3 changes: 2 additions & 1 deletion app/airflow/dags/libs/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions app/next-client-app/api/scanreports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export async function getScanReportTable(
permissions: [],
jobs: [],
trigger_reuse: true,
death_table: false,
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -89,6 +90,28 @@ export const columns: ColumnDef<ScanReportTable>[] = [
);
}
},
{
id: "note",
header: ({ column }) => (
<DataTableColumnHeader column={column} title="Note" />
),
cell: ({ row }) => {
const { death_table } = row.original;
return (
<div>
{death_table && (
<h3 className="flex">
{" "}
Death table
<Tooltips
content="This table is marked as a Death table. In the Carrot data standard, death data is typically provided in a separate file (e.g. death.csv) for mapping to the OMOP Death table."
Comment thread
AndrewThien marked this conversation as resolved.
/>
</h3>
)}
</div>
);
}
},
{
id: "edit",
header: ({ column }) => <DataTableColumnHeader column={column} title="" />,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ export default async function ScanReportsField(props: ScanReportsFieldProps) {
<TableBreadcrumbs
id={id}
tableId={tableId}
tableName={tableName.name}
tableName={
tableName.death_table
? `${tableName.name} (Death table)`
: tableName.name
}
variant="table"
/>
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default async function UpdateTable(props: UpdateTableProps) {
<div>
<Link href={`/scanreports/${id}/tables/${tableId}`}>
<Button variant={"secondary"} className="mb-3">
Update Table: {table.name}
Update Table: {table.name} {table.death_table && "(Death table)"}
</Button>
</Link>
{(table.date_event === null || table.person_id === null) && (
Expand Down
2 changes: 1 addition & 1 deletion app/next-client-app/components/core/Tooltips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ export function Tooltips({
</Tooltip>
</TooltipProvider>
);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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<number | null | undefined>;
}) {
const { values, setFieldValue } = useFormikContext<FormData>();
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({
Expand All @@ -32,19 +78,25 @@ export function ScanReportTableUpdateForm({
dateEvent: ScanReportField;
}) {
const router = useRouter();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const pendingDateEventRevertRef = useRef<number | null | undefined>(undefined);
const canUpdate =
permissions.includes("CanEdit") || permissions.includes("CanAdmin");

const fieldOptions = FormDataFilter<ScanReportField>(scanreportFields);

const initialPersonId = FormDataFilter<ScanReportField>(personId);
const initialDateEvent = FormDataFilter<ScanReportField>(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(
Expand All @@ -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);
Expand All @@ -76,6 +129,11 @@ export function ScanReportTableUpdateForm({
{({ handleSubmit, values, setFieldValue }) => (
<form className="w-full max-w-2xl" onSubmit={handleSubmit}>
<div className="flex flex-col gap-5">
<DeathDateModalController
deathDateFieldId={deathDateField?.id}
setIsDialogOpen={setIsDialogOpen}
pendingRevertRef={pendingDateEventRevertRef}
/>

<FormItem>
<FormLabel>Person ID</FormLabel>
Expand Down Expand Up @@ -109,6 +167,93 @@ export function ScanReportTableUpdateForm({
</FormControl>
</FormItem>

<FormItem>
<div className="flex flex-wrap items-center gap-2">
<FormLabel className="mb-0">
Does this table contain only death data for the OMOP CDM Death
table?
</FormLabel>
<Tooltips
content="In the Carrot data standard, death data is provided in a separate file (e.g. death.csv). Mark Yes if this table contains that death data to be mapped to the OMOP Death table."
/>
<Dialog
open={isDialogOpen}
onOpenChange={(open) => {
setIsDialogOpen(open);
if (!open && pendingDateEventRevertRef.current !== undefined) {
setFieldValue("dateEvent", pendingDateEventRevertRef.current);
pendingDateEventRevertRef.current = undefined;
}
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Please Confirm Your Choice</DialogTitle>
<DialogDescription>
Are you sure you want to set this table as a Death table?
Doing so will result in the following:
<ul className="text-gray-500 list-disc pl-4 py-2">
<li>
Mapping Rules that are created either manually or
automatically (built from OMOP vocabulary or Reused)
will have Destination table as{" "}
<span className="font-bold">Death</span>.
</li>
<li>
All concepts in this table will be recognised as{" "}
<span className="font-bold">Cause of Death</span> in
OMOP CDM.
</li>
<li>
Destination of Date Event will be{" "}
<span className="font-bold">Death date</span> field in
OMOP CDM.
</li>
</ul>
<p className="text-muted-foreground text-pretty">
You can turn off this setting later. Mapping rules will
be refreshed when you save.
</p>
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex gap-3">
<Button
type="button"
onClick={() => setIsDialogOpen(false)}
variant="outline"
>
Cancel <X className="size-4 ml-2" />
</Button>
<Button
type="button"
onClick={() => {
pendingDateEventRevertRef.current = undefined;
setFieldValue("death_table", true);
setIsDialogOpen(false);
}}
>
Confirm <Check className="size-4 ml-2" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Switch
checked={values.death_table}
onCheckedChange={(checked) => {
if (checked) {
setIsDialogOpen(true);
} else {
setFieldValue("death_table", false);
}
}}
disabled={!canUpdate}
/>
<Label className="text-lg">
{values.death_table === true ? "YES" : "NO"}
</Label>
</div>
</FormItem>

{enableReuseTriggerOption === "true" && (
<FormField name="triggerReuse">
{({ field }) => (
Expand Down
1 change: 1 addition & 0 deletions app/next-client-app/types/scanreport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface ScanReportTable {
permissions: Permission[];
jobs: Job[];
trigger_reuse: boolean;
death_table?: boolean;
}

interface ScanReportField {
Expand Down
Loading