diff --git a/frontend-v2/src/features/data/CreateDosingProtocols.tsx b/frontend-v2/src/features/data/CreateDosingProtocols.tsx index bb75ca64..c4dfcb67 100644 --- a/frontend-v2/src/features/data/CreateDosingProtocols.tsx +++ b/frontend-v2/src/features/data/CreateDosingProtocols.tsx @@ -25,6 +25,112 @@ import { SINGLE_TABLE_BREAKPOINTS, } from "../../shared/calculateTableHeights"; +function findFieldByType(type: string, state: StepperState) { + return ( + state.fields.find((field) => state.normalisedFields.get(field) === type) || + type + ); +} + +/** + * Assign an administration ID to each dosing row based on the dosing compartment and group ID. + * @param dosingRows + * @param administrationIdField + * @param groupIdField + * @returns an array of modified dosing rows. + */ +function generateAdministrationIds( + dosingRows: Row[], + administrationIdField: string, + groupIdField: string, +) { + const dosingCompartments = [ + ...new Set(dosingRows.map((row) => row["Amount Variable"])), + ]; + const groupIds = dosingRows.map((row) => row[groupIdField]); + const uniqueGroupIds = [...new Set(groupIds)]; + const administrationIds: string[] = []; + dosingRows.forEach((row) => { + const groupIndex = uniqueGroupIds.indexOf(row[groupIdField]) + 1; + const compartmentIndex = dosingCompartments.indexOf(row["Amount Variable"]); + const adminId = `${compartmentIndex * 10 + groupIndex}`; + if (row[administrationIdField] !== adminId) { + row[administrationIdField] = adminId; + } + administrationIds.push(adminId); + }); + return dosingRows; +} + +/** + * Create dosing rows for each subject and dosing compartment. + * Each row has an administration ID based on the dosing compartment and group ID. + * @param state stepper state + * @param administrationIdField column name for administration ID + * @param dosingCompartments dosing compartments for the model + * @param amountUnit amount unit for the model (either pmol or pmol/kg.) + */ +function createDosingRows( + state: StepperState, + administrationIdField: string, + dosingCompartments: string[], + amountUnit?: UnitRead, +) { + const idField = findFieldByType("ID", state); + const timeField = findFieldByType("Time", state); + const timeUnitField = findFieldByType("Time Unit", state); + const amountField = findFieldByType("Amount", state); + const amountUnitField = findFieldByType("Amount Unit", state); + const covariateFields = state.fields.filter( + (field) => state.normalisedFields.get(field) === "Cat Covariate", + ); + const nextData = [...state.data]; + const uniqueIds = new Set(nextData.map((row) => row[idField])); + const uniqueGroupIds = [...new Set(nextData.map((row) => row["Group ID"]))]; + const newRows: Row[] = []; + dosingCompartments.forEach((compartment, index) => { + uniqueIds.forEach((id) => { + const subjectRow = state.data.find((row) => row[idField] === id); + const groupId = subjectRow?.["Group ID"]; + const groupIndex = groupId ? uniqueGroupIds.indexOf(groupId) + 1 : 0; + const adminId = index * 10 + groupIndex; + const timeUnit = subjectRow?.[timeUnitField] || "h"; + const newRow: Row = { + [idField]: id, + [administrationIdField]: adminId.toString(), + "Amount Variable": compartment, + [amountUnitField]: amountUnit?.symbol === "pmol" ? "mg" : "mg/kg", + [amountField]: "0", + [timeField]: "0", + [timeUnitField]: timeUnit, + "Infusion Duration": "0.0833", + "Additional Doses": ".", + "Interdose Interval": ".", + }; + if (groupId) { + newRow["Group ID"] = groupId; + newRow[state.groupColumn] = groupId; + } + covariateFields.forEach((field) => { + newRow[field] = subjectRow?.[field] || ""; + }); + newRows.push(newRow); + }); + }); + newRows.sort((a, b) => parseInt(a[idField]) - parseInt(b[idField])); + state.setData([...nextData, ...newRows]); + state.setNormalisedFields( + new Map([ + ...state.normalisedFields.entries(), + ["Amount Variable", "Amount Variable"], + ["Amount Unit", "Amount Unit"], + ["Infusion Duration", "Infusion Duration"], + ["Additional Doses", "Additional Doses"], + ["Interdose Interval", "Interdose Interval"], + ]), + ); +} + type NumericTableCellProps = { id: string; disabled: boolean; @@ -64,6 +170,7 @@ interface IDosingProtocols { administrationIdField: string; amountUnitField?: string; amountUnit?: UnitRead; + dosingCompartments?: string[]; state: StepperState; units: UnitRead[]; variables: VariableRead[]; @@ -73,16 +180,10 @@ interface IDosingProtocols { }; } -function findFieldByName(name: string, state: StepperState) { - return ( - state.fields.find((field) => state.normalisedFields.get(field) === name) || - name - ); -} - const CreateDosingProtocols: FC = ({ administrationIdField, amountUnit, + dosingCompartments = [], state, units, variables, @@ -92,13 +193,21 @@ const CreateDosingProtocols: FC = ({ (field) => field === "Amount" || state.normalisedFields.get(field) === "Amount", ); - const amountUnitField = findFieldByName("Amount Unit", state); - const timeField = findFieldByName("Time", state); - const groupIdField = findFieldByName("Group ID", state); + const amountUnitField = findFieldByType("Amount Unit", state); + const timeField = findFieldByType("Time", state); + const groupIdField = findFieldByType("Group ID", state); // ignore rows with no amount and administration ID set to 0. - const dosingRows: Row[] = state.data.filter((row) => + let dosingRows: Row[] = state.data.filter((row) => parseInt(row[administrationIdField]), ); + if (!dosingRows.length) { + createDosingRows( + state, + administrationIdField, + dosingCompartments, + amountUnit, + ); + } if (!amountField) { const newNormalisedFields = new Map([ ...state.normalisedFields.entries(), @@ -108,10 +217,12 @@ const CreateDosingProtocols: FC = ({ state.setNormalisedFields(newNormalisedFields); state.setData(newData); } - - const administrationIds = administrationIdField - ? dosingRows.map((row) => row[administrationIdField]) - : []; + dosingRows = generateAdministrationIds( + dosingRows, + administrationIdField, + groupIdField, + ); + const administrationIds = dosingRows.map((row) => row[administrationIdField]); const uniqueAdministrationIds = [...new Set(administrationIds)]; const isAmount = (variable: VariableRead) => { @@ -127,46 +238,6 @@ const CreateDosingProtocols: FC = ({ }; const modelAmounts = variables?.filter(isAmount) || []; - function createDosingRow(id: string, qname: string, nextData: Row[]) { - const subjectIds = nextData - .filter((row) => row[administrationIdField] === id) - .map((row) => row["ID"]); - const uniqueSubjectIds = [...new Set(subjectIds)]; - uniqueSubjectIds.forEach((subjectId) => { - const groupId = state.data.find((row) => row["ID"] === subjectId)?.[ - groupIdField - ]; - const newRow: Row = { - ID: subjectId, - [administrationIdField]: id, - "Amount Variable": qname, - "Amount Unit": amountUnit?.symbol === "pmol" ? "mg" : "mg/kg", - Amount: "0", - Time: "0", - "Time Unit": "h", - "Infusion Duration": "0.0833", - "Additional Doses": ".", - "Interdose Interval": ".", - Observation: ".", - }; - if (groupId) { - newRow[groupIdField] = groupId; - newRow[state.groupColumn] = groupId; - } - nextData.push(newRow); - state.setData(nextData); - state.setNormalisedFields( - new Map([ - ...state.normalisedFields.entries(), - ["Amount Variable", "Amount Variable"], - ["Amount Unit", "Amount Unit"], - ["Infusion Duration", "Infusion Duration"], - ["Additional Doses", "Additional Doses"], - ["Interdose Interval", "Interdose Interval"], - ]), - ); - }); - } const handleAmountMappingChange = (id: string) => (event: SelectChangeEvent) => { const nextData = [...state.data]; @@ -174,14 +245,10 @@ const CreateDosingProtocols: FC = ({ const dosingRows = nextData.filter( (row) => row[administrationIdField] === id && row["Amount"] !== ".", ); - if (dosingRows.length) { - dosingRows.forEach((row) => { - row["Amount Variable"] = value; - }); - state.setData(nextData); - } else { - createDosingRow(id, value, nextData); - } + dosingRows.forEach((row) => { + row["Amount Variable"] = value; + }); + state.setData(nextData); }; type InputChangeEvent = | ChangeEvent @@ -223,7 +290,7 @@ const CreateDosingProtocols: FC = ({ - {administrationIdField} + Group Dosing Compartment @@ -249,118 +316,111 @@ const CreateDosingProtocols: FC = ({ - {uniqueAdministrationIds.map((adminId) => { - const currentRow = state.data.find((row) => - administrationIdField - ? row[administrationIdField] === adminId && - row["Amount"] !== "." - : true, - ); - const selectedVariable = variables?.find( - (variable) => - variable.qname === currentRow?.["Amount Variable"], + {dosingCompartments.map((compartment) => { + const dosingRows = state.data.filter( + (row) => + row["Amount Variable"] === compartment && + row["Amount"] !== ".", ); - const compatibleUnits = units?.find( - (unit) => unit.id === selectedVariable?.unit, - )?.compatible_units; - const defaultAmountUnit = - amountUnit?.symbol === "pmol" ? "mg" : "mg/kg"; - const selectedAmountUnit = - currentRow?.[amountUnitField] || defaultAmountUnit; - return ( - - {adminId} - - - - Variable - - - - - - - - - Units - - - - - - - {currentRow?.["Time Unit"]} - - - - + const administrationIds = dosingRows.map( + (row) => row[administrationIdField], ); + const uniqueAdministrationIds = [...new Set(administrationIds)]; + return uniqueAdministrationIds.map((adminId) => { + const [currentRow] = dosingRows.filter((row) => + administrationIdField + ? row[administrationIdField] === adminId + : true, + ); + const selectedVariable = variables?.find( + (variable) => + variable.qname === currentRow?.["Amount Variable"], + ); + const compatibleUnits = units?.find( + (unit) => unit.id === selectedVariable?.unit, + )?.compatible_units; + const defaultAmountUnit = + amountUnit?.symbol === "pmol" ? "mg" : "mg/kg"; + const selectedAmountUnit = + currentRow?.[amountUnitField] || defaultAmountUnit; + return ( + + + {currentRow?.[groupIdField]} + + + {selectedVariable?.name} + + + + + + Units + + + + + + + {currentRow?.["Time Unit"]} + + + + + ); + }); })} diff --git a/frontend-v2/src/features/data/DosingProtocols.tsx b/frontend-v2/src/features/data/DosingProtocols.tsx index ce3fe312..4bae2d4b 100644 --- a/frontend-v2/src/features/data/DosingProtocols.tsx +++ b/frontend-v2/src/features/data/DosingProtocols.tsx @@ -38,7 +38,7 @@ interface IDosingProtocols { }; } -function findFieldByName(name: string, state: StepperState) { +function findFieldByType(name: string, state: StepperState) { return ( state.fields.find((field) => state.normalisedFields.get(field) === name) || name @@ -54,12 +54,12 @@ const DosingProtocols: FC = ({ variables, notificationsInfo, }: IDosingProtocols) => { - const amountField = findFieldByName("Amount", state); - const amountVariableField = findFieldByName("Amount Variable", state); - const timeField = findFieldByName("Time", state); - const timeUnitField = findFieldByName("Time Unit", state); - const addlDosesField = findFieldByName("Additional Doses", state); - const interDoseField = findFieldByName("Interdose Interval", state); + const amountField = findFieldByType("Amount", state); + const amountVariableField = findFieldByType("Amount Variable", state); + const timeField = findFieldByType("Time", state); + const timeUnitField = findFieldByType("Time Unit", state); + const addlDosesField = findFieldByType("Additional Doses", state); + const interDoseField = findFieldByType("Interdose Interval", state); const dosingRows: Row[] = amountField ? state.data.filter( (row) => @@ -153,6 +153,9 @@ const DosingProtocols: FC = ({ {administrationIdField} + + Group ID + Dosing Compartment @@ -198,6 +201,9 @@ const DosingProtocols: FC = ({ return ( {adminId} + + {currentRow?.["Group ID"] || "."} + = ({ { skip: !projectId }, ); const isPreclinical = project?.species !== "H"; + const { data: projectProtocols, isLoading: isProtocolsLoading } = + useProtocolListQuery({ projectId: projectIdOrZero }, { skip: !projectId }); const { data: models = [] } = useCombinedModelListQuery( { projectId: projectIdOrZero }, { skip: !projectId }, @@ -79,6 +82,15 @@ const MapDosing: FC = ({ state.setNormalisedFields(newNormalisedFields); } + const dosingCompartments = projectProtocols?.map((protocol) => { + return ( + protocol.mapped_qname || + variables?.find((variable) => variable.id === protocol.variables[0]) + ?.qname || + "" + ); + }); + return hasDosingRows ? ( = ({ administrationIdField={administrationIdField || "Administration ID"} amountUnitField={amountUnitField || ""} amountUnit={amountUnit} + dosingCompartments={dosingCompartments} state={state} units={units || []} variables={variables || []} diff --git a/frontend-v2/src/features/data/Stratification.tsx b/frontend-v2/src/features/data/Stratification.tsx index 0b8d67af..edf94e0b 100644 --- a/frontend-v2/src/features/data/Stratification.tsx +++ b/frontend-v2/src/features/data/Stratification.tsx @@ -43,22 +43,6 @@ function validateGroupProtocols(groups: Group[], protocols: IProtocol[]) { }); return groupedProtocols.every((protocols) => protocols.length <= 1); } - -/** - * Generate a unique administration ID for each group. - * @param data - * @returns data with administration ID column. - */ -function generateAdministrationIds(data: { [key: string]: string }[]) { - const newData = data.map((row) => ({ ...row })); - const uniqueGroupIds = [...new Set(data.map((row) => row["Group ID"]))]; - newData.forEach((row) => { - const administrationId = uniqueGroupIds.indexOf(row["Group ID"]) + 1; - row["Administration ID"] = `${administrationId}`; - }); - return newData; -} - /** * Assign a group ID to each row based on a categorical covariate column. * @param data @@ -98,9 +82,6 @@ const Stratification: FC = ({ const values = state.data.map((row) => row[field]); return [...new Set(values)]; }); - const administrationIdField = state.fields.find( - (field) => state.normalisedFields.get(field) === "Administration ID", - ); const [firstRow] = state.data; const [tab, setTab] = useState(0); @@ -138,10 +119,7 @@ const Stratification: FC = ({ } if (!firstRow["Group ID"]) { - let newData = groupDataRows(state.data, groupColumn); - if (!administrationIdField) { - newData = generateAdministrationIds(newData); - } + const newData = groupDataRows(state.data, groupColumn); state.setData(newData); state.setNormalisedFields( new Map([...state.normalisedFields.entries(), ["Group ID", "Group ID"]]), @@ -154,10 +132,7 @@ const Stratification: FC = ({ const handleGroupChange = (event: ChangeEvent) => { const newGroup = event.target.value; - let newData = groupDataRows(state.data, newGroup); - if (!administrationIdField) { - newData = generateAdministrationIds(newData); - } + const newData = groupDataRows(state.data, newGroup); setGroupColumn(newGroup); state.setData(newData); }; diff --git a/frontend-v2/src/features/main/Sidebar.tsx b/frontend-v2/src/features/main/Sidebar.tsx index 6db93231..395f3508 100644 --- a/frontend-v2/src/features/main/Sidebar.tsx +++ b/frontend-v2/src/features/main/Sidebar.tsx @@ -109,8 +109,12 @@ export default function Sidebar() { ); }; - const doses = groups?.flatMap((group) => group.protocols.map((p) => p.doses)); - const groupsAreIncomplete = doses?.some((dosing) => !dosing[0]?.amount); + const protocolsAreComplete = groups?.flatMap((group) => { + return group.protocols + .map((p) => p.doses.every((d) => d.amount > 0)) + .some((d) => d); + }); + const groupsAreComplete = protocolsAreComplete?.every((dosing) => dosing); const noSecondaryParameters = model ? model.derived_variables.reduce((acc, dv) => { return acc && dv.type !== "AUC"; @@ -123,7 +127,7 @@ export default function Sidebar() { errors[PageName.MODEL] = "Model is incomplete, see the Model tab for details"; } - if (groupsAreIncomplete) { + if (!groupsAreComplete) { warnings[PageName.TRIAL_DESIGN] = "Trial design is incomplete, one or more dose amounts are zero"; } @@ -471,7 +475,7 @@ export default function Sidebar() { fontWeight: "bold", paddingLeft: "1rem", fontFamily: "Comfortaa", - flexGrow: project ? 0 : 1 + flexGrow: project ? 0 : 1, }} > pkpd explorer