diff --git a/app/api/order/medipost-test-response/route.ts b/app/api/order/medipost-test-response/route.ts index 5142972..5b7ac9a 100644 --- a/app/api/order/medipost-test-response/route.ts +++ b/app/api/order/medipost-test-response/route.ts @@ -3,7 +3,7 @@ import { getAnalysisOrder } from "~/lib/services/order.service"; import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipost/medipostTest.service"; import { retrieveOrder } from "@lib/data"; import { getAccountAdmin } from "~/lib/services/account.service"; -import { createMedipostActionLog } from "~/lib/services/medipost/medipostMessageBase.service"; +import { upsertMedipostActionLog } from "~/lib/services/medipost/medipostMessageBase.service"; import { getOrderedAnalysisIds } from "~/lib/services/medusaOrder.service"; export async function POST(request: Request) { @@ -35,7 +35,7 @@ export async function POST(request: Request) { }); try { - await createMedipostActionLog({ + await upsertMedipostActionLog({ action: 'send_fake_analysis_results_to_medipost', xml: messageXml, medusaOrderId, diff --git a/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx b/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx index d19d0d1..757b120 100644 --- a/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/_components/analysis.tsx @@ -60,19 +60,14 @@ const Analysis = ({ const unit = results?.unit || ''; const normLower = results?.normLower; const normUpper = results?.normUpper; + const normStatus = results?.normStatus ?? null; const [showTooltip, setShowTooltip] = useState(false); const analysisResultLevel = useMemo(() => { - if (!results) { + if (normStatus === null) { return null; } - if (results.responseValue === null || results.responseValue === undefined) { - return null; - } - - const normStatus = results.normStatus; - switch (normStatus) { case 1: return AnalysisResultLevel.WARNING; @@ -82,12 +77,13 @@ const Analysis = ({ default: return AnalysisResultLevel.NORMAL; } - }, [results]); + }, [normStatus]); const isCancelled = Number(results?.status) === 5; const hasNestedElements = results?.nestedElements.length > 0; const normRangeText = normLower !== null ? `${normLower} - ${normUpper || ''}` : null; + const hasTextualResponse = hasIsNegative || hasIsWithinNorm; return (
@@ -127,10 +123,18 @@ const Analysis = ({ {isCancelled || !results || hasNestedElements ? null : ( <>
-
{value}
+
+ {value} +
{unit}
- {!(hasIsNegative || hasIsWithinNorm) && ( + {!hasTextualResponse && ( <>
{normRangeText} diff --git a/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts b/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts index e4da467..14ef88e 100644 --- a/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts +++ b/app/home/(user)/(dashboard)/analysis-results/test/test-responses.ts @@ -139,7 +139,7 @@ const big1: AnalysisTestResponse = { "unit": null, "normLower": null, "normUpper": 2, - "normStatus": 0, + "normStatus": 2, "responseTime": "2024-02-29T10:13:01+00:00", "responseValue": null, "responseValueIsNegative": null, @@ -150,6 +150,26 @@ const big1: AnalysisTestResponse = { "analysisElementOriginalId": "59156-0" } }, + { + "analysisIdOriginal": "59156-0", + "isWaitingForResults": false, + "analysisName": "Glükoos", + "results": { + "nestedElements": [], + "unit": null, + "normLower": null, + "normUpper": 2, + "normStatus": 0, + "responseTime": "2024-02-29T10:13:01+00:00", + "responseValue": null, + "responseValueIsNegative": null, + "responseValueIsWithinNorm": true, + "normLowerIncluded": false, + "normUpperIncluded": false, + "status": "4", + "analysisElementOriginalId": "59156-0" + } + }, { "analysisIdOriginal": "13955-0", "isWaitingForResults": false, diff --git a/lib/services/analysis-order.service.ts b/lib/services/analysis-order.service.ts index cd57b9a..5fa5ed5 100644 --- a/lib/services/analysis-order.service.ts +++ b/lib/services/analysis-order.service.ts @@ -18,16 +18,32 @@ export async function getExistingAnalysisResponseElements({ return data as AnalysisResponseElement[]; } -export async function createAnalysisResponseElement({ +export async function upsertAnalysisResponseElement({ element, }: { element: Omit; }) { - await getSupabaseServerAdminClient() + const { data } = await getSupabaseServerAdminClient() .schema('medreport') .from('analysis_response_elements') - .insert(element) + .upsert( + element, + { + onConflict: 'analysis_response_id,analysis_element_original_id', + ignoreDuplicates: false + } + ) + .select('id') .throwOnError(); + + const analysisResponseElementId = data?.[0]?.id; + if (!analysisResponseElementId) { + throw new Error( + `Failed to insert or update analysis response element (response id: ${element.analysis_response_id}, element id: ${element.analysis_element_original_id})` + ); + } + + return { analysisResponseElementId }; } export async function upsertAnalysisResponse({ diff --git a/lib/services/medipost/medipostMessageBase.service.ts b/lib/services/medipost/medipostMessageBase.service.ts index ef452ec..d3194e8 100644 --- a/lib/services/medipost/medipostMessageBase.service.ts +++ b/lib/services/medipost/medipostMessageBase.service.ts @@ -27,7 +27,7 @@ export async function getLatestMessage({ ); } -export async function createMedipostActionLog({ +export async function upsertMedipostActionLog({ action, xml, hasAnalysisResults = false, @@ -40,8 +40,7 @@ export async function createMedipostActionLog({ action: | 'send_order_to_medipost' | 'sync_analysis_results_from_medipost' - | 'send_fake_analysis_results_to_medipost' - | 'send_analysis_results_to_medipost'; + | 'send_fake_analysis_results_to_medipost'; xml: string; hasAnalysisResults?: boolean; medusaOrderId?: string | null; @@ -50,19 +49,34 @@ export async function createMedipostActionLog({ medipostExternalOrderId?: string | null; medipostPrivateMessageId?: string | null; }) { - await getSupabaseServerAdminClient() + const { data } = await getSupabaseServerAdminClient() .schema('medreport') .from('medipost_actions') - .insert({ - action, - xml, - has_analysis_results: hasAnalysisResults, - medusa_order_id: medusaOrderId, - response_xml: responseXml, - has_error: hasError, - medipost_external_order_id: medipostExternalOrderId, - medipost_private_message_id: medipostPrivateMessageId, - }) + .upsert( + { + action, + xml, + has_analysis_results: hasAnalysisResults, + medusa_order_id: medusaOrderId, + response_xml: responseXml, + has_error: hasError, + medipost_external_order_id: medipostExternalOrderId, + medipost_private_message_id: medipostPrivateMessageId, + }, + { + onConflict: 'medipost_private_message_id', + ignoreDuplicates: false + } + ) .select('id') .throwOnError(); + + const medipostActionId = data?.[0]?.id; + if (!medipostActionId) { + throw new Error( + `Failed to insert or update medipost action (private message id: ${medipostPrivateMessageId})` + ); + } + + return { medipostActionId }; } diff --git a/lib/services/medipost/medipostPrivateMessage.service.ts b/lib/services/medipost/medipostPrivateMessage.service.ts index f6356e3..fba8b76 100644 --- a/lib/services/medipost/medipostPrivateMessage.service.ts +++ b/lib/services/medipost/medipostPrivateMessage.service.ts @@ -21,7 +21,7 @@ import { Tables } from '@kit/supabase/database'; import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client'; import { getAnalysisElementsAdmin } from '../analysis-element.service'; import { getAnalyses } from '../analyses.service'; -import { createMedipostActionLog, getLatestMessage } from './medipostMessageBase.service'; +import { upsertMedipostActionLog, getLatestMessage } from './medipostMessageBase.service'; import { validateMedipostResponse } from './medipostValidate.service'; import { getAnalysisOrder, updateAnalysisOrderStatus } from '../order.service'; import { parseXML } from '../util/xml.service'; @@ -29,7 +29,7 @@ import { composeOrderXML, OrderedAnalysisElement } from './medipostXML.service'; import { getAccountAdmin } from '../account.service'; import { logMedipostDispatch } from '../audit.service'; import { MedipostValidationError } from './MedipostValidationError'; -import { createAnalysisResponseElement, getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service'; +import { upsertAnalysisResponseElement, getExistingAnalysisResponseElements, upsertAnalysisResponse } from '../analysis-order.service'; const BASE_URL = process.env.MEDIPOST_URL!; const USER = process.env.MEDIPOST_USER!; @@ -242,7 +242,7 @@ export async function syncPrivateMessage({ for (const element of newElements) { try { - await createAnalysisResponseElement({ + await upsertAnalysisResponseElement({ element: { ...element, analysis_response_id: analysisResponseId, @@ -305,7 +305,7 @@ export async function readPrivateMessageResponse({ const hasInvalidOrderId = isNaN(analysisOrderId); if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) { - await createMedipostActionLog({ + await upsertMedipostActionLog({ action: 'sync_analysis_results_from_medipost', xml: privateMessageXml, hasAnalysisResults: false, @@ -342,7 +342,7 @@ export async function readPrivateMessageResponse({ const status = await syncPrivateMessage({ messageResponse, order: analysisOrder }); - await createMedipostActionLog({ + await upsertMedipostActionLog({ action: 'sync_analysis_results_from_medipost', xml: privateMessageXml, hasAnalysisResults: true, @@ -475,7 +475,7 @@ export async function sendOrderToMedipost({ isMedipostError, errorMessage: e.response, }); - await createMedipostActionLog({ + await upsertMedipostActionLog({ action: 'send_order_to_medipost', xml: orderXml, hasAnalysisResults: false, @@ -489,7 +489,7 @@ export async function sendOrderToMedipost({ isSuccess: false, isMedipostError, }); - await createMedipostActionLog({ + await upsertMedipostActionLog({ action: 'send_order_to_medipost', xml: orderXml, hasAnalysisResults: false, @@ -505,7 +505,7 @@ export async function sendOrderToMedipost({ isSuccess: true, isMedipostError: false, }); - await createMedipostActionLog({ + await upsertMedipostActionLog({ action: 'send_order_to_medipost', xml: orderXml, hasAnalysisResults: false, diff --git a/supabase/migrations/20250918143530_analysis_response_element_unique_by_order.sql b/supabase/migrations/20250918143530_analysis_response_element_unique_by_order.sql new file mode 100644 index 0000000..082cab4 --- /dev/null +++ b/supabase/migrations/20250918143530_analysis_response_element_unique_by_order.sql @@ -0,0 +1,18 @@ +-- Add unique constraint on analysis_response_elements by analysis_response_id + analysis_element_original_id +CREATE UNIQUE INDEX analysis_response_elements_unique_by_response_and_element +ON "medreport"."analysis_response_elements" +USING btree (analysis_response_id, analysis_element_original_id); + +ALTER TABLE "medreport"."analysis_response_elements" +ADD CONSTRAINT "analysis_response_elements_unique_by_response_and_element" +UNIQUE USING INDEX "analysis_response_elements_unique_by_response_and_element"; + +-- Add updated_at column to medipost_actions table +ALTER TABLE "medreport"."medipost_actions" ADD COLUMN "updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(); + +-- Add unique constraint on medipost_actions by medipost_private_message_id +-- Using partial index to allow multiple NULL values but enforce uniqueness for non-NULL values +CREATE UNIQUE INDEX medipost_actions_unique_by_private_message_id +ON "medreport"."medipost_actions" +USING btree (medipost_private_message_id) +WHERE medipost_private_message_id IS NOT NULL;