MED-198: add notification for new analysis result

This commit is contained in:
Danel Kungla
2025-10-08 16:32:19 +03:00
parent 3a8d73e742
commit 8386e541cb
16 changed files with 126 additions and 16 deletions

View File

@@ -3,6 +3,9 @@ import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { createNotificationsApi } from '@/packages/features/notifications/src/server/api';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip'; import { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip';
import { pathsConfig } from '@kit/shared/config'; import { pathsConfig } from '@kit/shared/config';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
@@ -25,7 +28,9 @@ export default async function AnalysisResultsPage({
id: string; id: string;
}>; }>;
}) { }) {
const supabaseClient = getSupabaseServerClient();
const { id: analysisOrderId } = await params; const { id: analysisOrderId } = await params;
const notificationsApi = createNotificationsApi(supabaseClient);
const [{ account }, analysisResponse] = await Promise.all([ const [{ account }, analysisResponse] = await Promise.all([
loadCurrentUserAccount(), loadCurrentUserAccount(),
@@ -41,6 +46,11 @@ export default async function AnalysisResultsPage({
action: PageViewAction.VIEW_ANALYSIS_RESULTS, action: PageViewAction.VIEW_ANALYSIS_RESULTS,
}); });
await notificationsApi.dismissNotification(
`/home/analysis-results/${analysisOrderId}`,
'link',
);
if (!analysisResponse) { if (!analysisResponse) {
return ( return (
<> <>

View File

@@ -90,6 +90,14 @@ async function OrdersPage() {
), ),
); );
if (
medusaOrderItemsAnalysisPackages.length === 0 &&
medusaOrderItemsOther.length === 0 &&
medusaOrderItemsTtoServices.length === 0
) {
return null;
}
return ( return (
<React.Fragment key={medusaOrder.id}> <React.Fragment key={medusaOrder.id}>
<Divider className="my-6" /> <Divider className="my-6" />

View File

@@ -32,6 +32,7 @@ import { Trans } from '@kit/ui/trans';
// home imports // home imports
import type { UserWorkspace } from '../_lib/server/load-user-workspace'; import type { UserWorkspace } from '../_lib/server/load-user-workspace';
import { UserNotifications } from './user-notifications';
const PERSONAL_ACCOUNT_SLUG = 'personal'; const PERSONAL_ACCOUNT_SLUG = 'personal';
@@ -90,7 +91,7 @@ export function HomeMobileNavigation(props: {
return ( return (
<DropdownMenu> <DropdownMenu>
<div className="flex justify-between gap-4"> <div className="flex justify-between gap-3">
<Link href={pathsConfig.app.cart}> <Link href={pathsConfig.app.cart}>
<Button <Button
variant="ghost" variant="ghost"
@@ -108,6 +109,9 @@ export function HomeMobileNavigation(props: {
)} )}
</Button> </Button>
</Link> </Link>
<UserNotifications userId={user.id} />
<DropdownMenuTrigger> <DropdownMenuTrigger>
<Menu className="h-6 w-6" /> <Menu className="h-6 w-6" />
</DropdownMenuTrigger> </DropdownMenuTrigger>

View File

@@ -61,6 +61,7 @@ export default function OrderBlock({
id: analysisOrder.id, id: analysisOrder.id,
status: analysisOrder.status, status: analysisOrder.status,
}} }}
isPackage
/> />
)} )}
{itemsTtoService && ( {itemsTtoService && (

View File

@@ -32,11 +32,13 @@ export default function OrderItemsTable({
title, title,
order, order,
type = 'analysisOrder', type = 'analysisOrder',
isPackage = false,
}: { }: {
items: StoreOrderLineItem[]; items: StoreOrderLineItem[];
title: string; title: string;
order: Order; order: Order;
type?: OrderItemType; type?: OrderItemType;
isPackage?: boolean;
}) { }) {
const router = useRouter(); const router = useRouter();
const [isConfirmOpen, setIsConfirmOpen] = useState(false); const [isConfirmOpen, setIsConfirmOpen] = useState(false);
@@ -100,9 +102,15 @@ export default function OrderItemsTable({
</TableCell> </TableCell>
)} )}
<TableCell className="min-w-[180px] px-6"> <TableCell className="min-w-[180px] px-6">
<Trans {isPackage ? (
i18nKey={`orders:status.${type}.${order?.status ?? 'CONFIRMED'}`} <Trans
/> i18nKey={`orders:status.analysisPackageOrder.${order?.status ?? 'CONFIRMED'}`}
/>
) : (
<Trans
i18nKey={`orders:status.${type}.${order?.status ?? 'CONFIRMED'}`}
/>
)}
</TableCell> </TableCell>
<TableCell className="px-6 text-right"> <TableCell className="px-6 text-right">

View File

@@ -1,8 +1,6 @@
'use server'; 'use server';
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
import { createNotificationsApi } from '@/packages/features/notifications/src/server/api';
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
import { listProductTypes } from '@lib/data'; import { listProductTypes } from '@lib/data';
import { initiateMultiPaymentSession, placeOrder } from '@lib/data/cart'; import { initiateMultiPaymentSession, placeOrder } from '@lib/data/cart';
import type { StoreCart, StoreOrder } from '@medusajs/types'; import type { StoreCart, StoreOrder } from '@medusajs/types';
@@ -327,7 +325,6 @@ const sendEmail = async ({
partnerLocationName: string; partnerLocationName: string;
language: string; language: string;
}) => { }) => {
const client = getSupabaseServerAdminClient();
try { try {
const { renderSynlabAnalysisPackageEmail } = await import( const { renderSynlabAnalysisPackageEmail } = await import(
'@kit/email-templates' '@kit/email-templates'
@@ -353,10 +350,6 @@ const sendEmail = async ({
.catch((error) => { .catch((error) => {
throw new Error(`Failed to send email, message=${error}`); throw new Error(`Failed to send email, message=${error}`);
}); });
await createNotificationsApi(client).createNotification({
account_id: account.id,
body: html,
});
} catch (error) { } catch (error) {
throw new Error(`Failed to send email, message=${error}`); throw new Error(`Failed to send email, message=${error}`);
} }

View File

@@ -3,7 +3,9 @@
import type { PostgrestError } from '@supabase/supabase-js'; import type { PostgrestError } from '@supabase/supabase-js';
import { GetMessageListResponse, MedipostAction } from '@/lib/types/medipost'; import { GetMessageListResponse, MedipostAction } from '@/lib/types/medipost';
import { createNotificationsApi } from '@/packages/features/notifications/src/server/api';
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
import { pathsConfig } from '@/packages/shared/src/config';
import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis'; import { AnalysisOrderStatus } from '@/packages/shared/src/types/medipost-analysis';
import type { import type {
MedipostOrderResponse, MedipostOrderResponse,
@@ -16,6 +18,7 @@ import axios from 'axios';
import { toArray } from '@kit/shared/utils'; import { toArray } from '@kit/shared/utils';
import { Tables } from '@kit/supabase/database'; import { Tables } from '@kit/supabase/database';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element'; import type { AnalysisResponseElement } from '~/lib/types/analysis-response-element';
import type { AnalysisOrder } from '~/lib/types/order'; import type { AnalysisOrder } from '~/lib/types/order';
@@ -268,6 +271,7 @@ export async function syncPrivateMessage({
order: Tables<{ schema: 'medreport' }, 'analysis_orders'>; order: Tables<{ schema: 'medreport' }, 'analysis_orders'>;
}) { }) {
const supabase = getSupabaseServerAdminClient(); const supabase = getSupabaseServerAdminClient();
const { t } = await createI18nServerInstance();
const orderStatus = AnalysisOrderStatus[TellimuseOlek]; const orderStatus = AnalysisOrderStatus[TellimuseOlek];
@@ -300,6 +304,7 @@ export async function syncPrivateMessage({
log, log,
}); });
let newElementsAdded = 0;
for (const element of newElements) { for (const element of newElements) {
try { try {
await upsertAnalysisResponseElement({ await upsertAnalysisResponseElement({
@@ -308,6 +313,7 @@ export async function syncPrivateMessage({
analysis_response_id: analysisResponseId, analysis_response_id: analysisResponseId,
}, },
}); });
newElementsAdded++;
} catch (e) { } catch (e) {
log( log(
`Failed to create order response element for response id ${analysisResponseId}, element id '${element.analysis_element_original_id}' (order id: ${order.id})`, `Failed to create order response element for response id ${analysisResponseId}, element id '${element.analysis_element_original_id}' (order id: ${order.id})`,
@@ -316,6 +322,16 @@ export async function syncPrivateMessage({
} }
} }
log(`Added ${newElementsAdded} new elements`);
if (newElementsAdded !== 0) {
await createNotificationsApi(supabase).createNotification({
account_id: analysisOrder.user_id,
body: t('analysis-results:notification.body'),
link: `${pathsConfig.app.analysisResults}/${order.id}`,
});
}
return (await hasAllAnalysisResponseElements({ analysisResponseId, order })) return (await hasAllAnalysisResponseElements({ analysisResponseId, order }))
? { isCompleted: orderStatus === 'COMPLETED' } ? { isCompleted: orderStatus === 'COMPLETED' }
: { isPartial: true }; : { isPartial: true };
@@ -371,8 +387,13 @@ export async function readPrivateMessageResponse({
const hasInvalidOrderId = isNaN(analysisOrderId); const hasInvalidOrderId = isNaN(analysisOrderId);
if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) { if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) {
console.log({
privateMessageContent,
saadetis: privateMessageContent?.Saadetis,
messageResponse,
});
console.error( console.error(
`Invalid order id or message response or patient personal code, medipostExternalOrderId=${medipostExternalOrderId}, privateMessageId=${privateMessageId}`, `Invalid !order id or message response or patient personal code, medipostExternalOrderId=${medipostExternalOrderId}, privateMessageId=${privateMessageId}`,
); );
await upsertMedipostActionLog({ await upsertMedipostActionLog({
action: 'sync_analysis_results_from_medipost', action: 'sync_analysis_results_from_medipost',

View File

@@ -62,7 +62,10 @@ export const listOrders = async (
credentials: 'include', credentials: 'include',
}) })
.then(({ orders }) => orders) .then(({ orders }) => orders)
.catch((err) => medusaError(err)); .catch((err) => {
console.error('Error receiving orders', { err });
return medusaError(err);
});
}; };
export const listOrdersByIds = async (ids: string[]) => { export const listOrdersByIds = async (ids: string[]) => {

View File

@@ -50,4 +50,13 @@ class NotificationsApi {
createNotification(params: Notification['Insert']) { createNotification(params: Notification['Insert']) {
return this.service.createNotification(params); return this.service.createNotification(params);
} }
/**
* @name createNotification
* @description Create a new notification in the database
* @param params
*/
dismissNotification(eqValue: string, eqColumn?: string) {
return this.service.dismissNotification(eqColumn, eqValue);
}
} }

View File

@@ -29,4 +29,21 @@ class NotificationsService {
throw error; throw error;
} }
} }
async dismissNotification(eqColumn = 'id', eqValue: string) {
const logger = await getLogger();
const { error } = await this.client
.schema('medreport')
.from('notifications')
.update({ dismissed: true })
.eq(eqColumn, eqValue);
if (error) {
logger.error(
{ eqColumn, eqValue },
`Could not dismiss notification: ${error.message}`,
);
throw error;
}
}
} }

View File

@@ -21,5 +21,8 @@
} }
}, },
"orderTitle": "Order number {{orderNumber}}", "orderTitle": "Order number {{orderNumber}}",
"view": "View results" "view": "View results",
"notification": {
"body": "You have new analysis results"
}
} }

View File

@@ -17,6 +17,15 @@
"REJECTED": "Rejected", "REJECTED": "Rejected",
"CANCELLED": "Cancelled", "CANCELLED": "Cancelled",
"analysisOrder": { "analysisOrder": {
"QUEUED": "Queued",
"PROCESSING": "Sent to Synlab",
"PARTIAL_ANALYSIS_RESPONSE": "Partial results",
"FULL_ANALYSIS_RESPONSE": "All results received",
"COMPLETED": "Confirmed",
"REJECTED": "Rejected",
"CANCELLED": "Cancelled"
},
"analysisPackageOrder": {
"QUEUED": "Queued", "QUEUED": "Queued",
"PROCESSING": "Sent to Synlab", "PROCESSING": "Sent to Synlab",
"PARTIAL_ANALYSIS_RESPONSE": "Partial results", "PARTIAL_ANALYSIS_RESPONSE": "Partial results",

View File

@@ -21,5 +21,8 @@
} }
}, },
"orderTitle": "Tellimus {{orderNumber}}", "orderTitle": "Tellimus {{orderNumber}}",
"view": "Vaata tulemusi" "view": "Vaata tulemusi",
"notification": {
"body": "Teil on valmis uued analüüsi tulemused"
}
} }

View File

@@ -19,6 +19,15 @@
"REJECTED": "Tagastatud", "REJECTED": "Tagastatud",
"CANCELLED": "Tühistatud", "CANCELLED": "Tühistatud",
"analysisOrder": { "analysisOrder": {
"QUEUED": "Esitatud",
"PROCESSING": "Synlabile edastatud",
"PARTIAL_ANALYSIS_RESPONSE": "Osalised tulemused",
"FULL_ANALYSIS_RESPONSE": "Kõik tulemused käes",
"COMPLETED": "Kinnitatud",
"REJECTED": "Tagastatud",
"CANCELLED": "Tühistatud"
},
"analysisPackageOrder": {
"QUEUED": "Esitatud", "QUEUED": "Esitatud",
"PROCESSING": "Synlabile edastatud", "PROCESSING": "Synlabile edastatud",
"PARTIAL_ANALYSIS_RESPONSE": "Osalised tulemused", "PARTIAL_ANALYSIS_RESPONSE": "Osalised tulemused",

View File

@@ -20,5 +20,8 @@
"isNotWithinNorm": "Не в норме" "isNotWithinNorm": "Не в норме"
} }
}, },
"orderTitle": "Заказ {{orderNumber}}" "orderTitle": "Заказ {{orderNumber}}",
"notification": {
"body": "Teil on valmis uued analüüsi tulemused"
}
} }

View File

@@ -17,6 +17,15 @@
"REJECTED": "Отклонено", "REJECTED": "Отклонено",
"CANCELLED": "Отменено", "CANCELLED": "Отменено",
"analysisOrder": { "analysisOrder": {
"QUEUED": "Отправлено",
"PROCESSING": "Отправлено в Synlab",
"PARTIAL_ANALYSIS_RESPONSE": "Частичные результаты",
"FULL_ANALYSIS_RESPONSE": "Все результаты получены",
"COMPLETED": "Подтверждено",
"REJECTED": "Отклонено",
"CANCELLED": "Отменено"
},
"analysisPackageOrder": {
"QUEUED": "Отправлено", "QUEUED": "Отправлено",
"PROCESSING": "Отправлено в Synlab", "PROCESSING": "Отправлено в Synlab",
"PARTIAL_ANALYSIS_RESPONSE": "Частичные результаты", "PARTIAL_ANALYSIS_RESPONSE": "Частичные результаты",