Merge branch 'main' into B2B-88

This commit is contained in:
devmc-ee
2025-06-10 20:59:25 +03:00
14 changed files with 581 additions and 98 deletions

View File

@@ -4,6 +4,7 @@ NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=your-service-role-key NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
MEDIPOST_URL=your-medpost-url MEDIPOST_URL=your-medipost-url
MEDIPOST_USER=your-medpost-user MEDIPOST_USER=your-medipost-user
MEDIPOST_PASSWORD=your-medpost-password MEDIPOST_PASSWORD=your-medipost-password
MEDIPOST_RECIPIENT=your-medipost-recipient

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { MedReportTitle } from "@/components/MedReportTitle"; import { MedReportTitle } from "@/components/med-report-title";
import React from "react"; import React from "react";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";

View File

@@ -1,5 +1,4 @@
import { MedReportTitle } from "@/components/MedReportTitle"; import { MedReportTitle } from "@/components/med-report-title";
import { Button } from "@/packages/ui/src/shadcn/button";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";

View File

@@ -1,8 +1,8 @@
import { signOutAction } from "@/lib/actions/sign-out"; import { signOutAction } from "@/lib/actions/sign-out";
import { hasEnvVars } from "@/utils/supabase/check-env-vars"; import { hasEnvVars } from "@/utils/supabase/check-env-vars";
import Link from "next/link"; import Link from "next/link";
import { Badge } from "./ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "./ui/button"; import { Button } from "@/components/ui/button";
import { createClient } from "@/utils/supabase/server"; import { createClient } from "@/utils/supabase/server";
export default async function AuthButton() { export default async function AuthButton() {

View File

@@ -1,4 +1,4 @@
import { MedReportSmallLogo } from "@/public/assets/MedReportSmallLogo"; import { MedReportSmallLogo } from "@/public/assets/med-report-small-logo";
export const MedReportTitle = () => ( export const MedReportTitle = () => (
<div className="flex gap-2 justify-center"> <div className="flex gap-2 justify-center">

2
lib/constants.ts Normal file
View File

@@ -0,0 +1,2 @@
export const DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
export const DATE_FORMAT = "yyyy-mm-dd";

View File

@@ -1,16 +1,34 @@
import { import {
SupabaseClient,
createClient as createCustomClient,
} from '@supabase/supabase-js';
import {
getAnalysisGroup,
getClientInstitution,
getClientPerson,
getConfidentiality,
getOrderEnteredByPerson,
getPais,
getPatient,
getProviderInstitution,
getSpecimen,
} from '@/lib/templates/medipost-order';
import { SyncStatus } from '@/lib/types/audit';
import {
AnalysisOrderStatus,
GetMessageListResponse, GetMessageListResponse,
MaterjalideGrupp,
MedipostAction, MedipostAction,
MedipostPublicMessageResponse, MedipostPublicMessageResponse,
Message, Message,
UuringuGrupp, UuringuGrupp,
} from "@/lib/types/medipost"; } from '@/lib/types/medipost';
import { Tables } from "@/supabase/database.types"; import { toArray } from '@/lib/utils';
import { createClient, SupabaseClient } from "@supabase/supabase-js"; import { Tables } from '@/supabase/database.types';
import axios from "axios"; import axios from 'axios';
import { XMLParser } from "fast-xml-parser"; import { XMLParser } from 'fast-xml-parser';
import { SyncStatus } from "@/lib/types/audit"; import { uniqBy } from 'lodash';
import { toArray } from "@/lib/utils";
const BASE_URL = process.env.MEDIPOST_URL!; const BASE_URL = process.env.MEDIPOST_URL!;
const USER = process.env.MEDIPOST_USER!; const USER = process.env.MEDIPOST_USER!;
@@ -37,14 +55,14 @@ export async function getLatestPublicMessageListItem() {
Action: MedipostAction.GetPublicMessageList, Action: MedipostAction.GetPublicMessageList,
User: USER, User: USER,
Password: PASSWORD, Password: PASSWORD,
Sender: "syndev", Sender: 'syndev',
// LastChecked (date+time) can be used here to get only messages since the last check - add when cron is created // LastChecked (date+time) can be used here to get only messages since the last check - add when cron is created
// MessageType check only for messages of certain type // MessageType check only for messages of certain type
}, },
}); });
if (data.code && data.code !== 0) { if (data.code && data.code !== 0) {
throw new Error("Failed to get public message list"); throw new Error('Failed to get public message list');
} }
return getLatestMessage(data?.messages); return getLatestMessage(data?.messages);
@@ -59,7 +77,7 @@ export async function getPublicMessage(messageId: string) {
MessageId: messageId, MessageId: messageId,
}, },
headers: { headers: {
Accept: "application/xml", Accept: 'application/xml',
}, },
}); });
const parser = new XMLParser({ ignoreAttributes: false }); const parser = new XMLParser({ ignoreAttributes: false });
@@ -74,16 +92,16 @@ export async function getPublicMessage(messageId: string) {
export async function sendPrivateMessage(messageXml: string, receiver: string) { export async function sendPrivateMessage(messageXml: string, receiver: string) {
const body = new FormData(); const body = new FormData();
body.append("Action", MedipostAction.SendPrivateMessage); body.append('Action', MedipostAction.SendPrivateMessage);
body.append("User", USER); body.append('User', USER);
body.append("Password", PASSWORD); body.append('Password', PASSWORD);
body.append("Receiver", receiver); body.append('Receiver', receiver);
body.append("MessageType", "Tellimus"); body.append('MessageType', 'Tellimus');
body.append( body.append(
"Message", 'Message',
new Blob([messageXml], { new Blob([messageXml], {
type: "text/xml; charset=UTF-8", type: 'text/xml; charset=UTF-8',
}) }),
); );
const { data } = await axios.post(BASE_URL, body); const { data } = await axios.post(BASE_URL, body);
@@ -103,7 +121,7 @@ export async function getLatestPrivateMessageListItem() {
}); });
if (data.code && data.code !== 0) { if (data.code && data.code !== 0) {
throw new Error("Failed to get private message list"); throw new Error('Failed to get private message list');
} }
return getLatestMessage(data?.messages); return getLatestMessage(data?.messages);
@@ -118,7 +136,7 @@ export async function getPrivateMessage(messageId: string) {
MessageId: messageId, MessageId: messageId,
}, },
headers: { headers: {
Accept: "application/xml", Accept: 'application/xml',
}, },
}); });
@@ -154,7 +172,7 @@ export async function readPrivateMessageResponse() {
} }
const privateMessageContent = await getPrivateMessage( const privateMessageContent = await getPrivateMessage(
privateMessage.messageId privateMessage.messageId,
); );
if (privateMessageContent) { if (privateMessageContent) {
@@ -168,29 +186,29 @@ export async function readPrivateMessageResponse() {
async function saveAnalysisGroup( async function saveAnalysisGroup(
analysisGroup: UuringuGrupp, analysisGroup: UuringuGrupp,
supabase: SupabaseClient supabase: SupabaseClient,
) { ) {
const { data: insertedAnalysisGroup, error } = await supabase const { data: insertedAnalysisGroup, error } = await supabase
.from("analysis_groups") .from('analysis_groups')
.upsert( .upsert(
{ {
original_id: analysisGroup.UuringuGruppId, original_id: analysisGroup.UuringuGruppId,
name: analysisGroup.UuringuGruppNimi, name: analysisGroup.UuringuGruppNimi,
order: analysisGroup.UuringuGruppJarjekord, order: analysisGroup.UuringuGruppJarjekord,
}, },
{ onConflict: "original_id", ignoreDuplicates: false } { onConflict: 'original_id', ignoreDuplicates: false },
) )
.select("id"); .select('id');
if (error || !insertedAnalysisGroup[0]?.id) { if (error || !insertedAnalysisGroup[0]?.id) {
throw new Error( throw new Error(
`Failed to insert analysis group (id: ${analysisGroup.UuringuGruppId}), error: ${error?.message}` `Failed to insert analysis group (id: ${analysisGroup.UuringuGruppId}), error: ${error?.message}`,
); );
} }
const analysisGroupId = insertedAnalysisGroup[0].id; const analysisGroupId = insertedAnalysisGroup[0].id;
const analysisGroupCodes = toArray(analysisGroup.Kood); const analysisGroupCodes = toArray(analysisGroup.Kood);
const codes: Partial<Tables<"codes">>[] = analysisGroupCodes.map((kood) => ({ const codes: Partial<Tables<'codes'>>[] = analysisGroupCodes.map((kood) => ({
hk_code: kood.HkKood, hk_code: kood.HkKood,
hk_code_multiplier: kood.HkKoodiKordaja, hk_code_multiplier: kood.HkKoodiKordaja,
coefficient: kood.Koefitsient, coefficient: kood.Koefitsient,
@@ -204,7 +222,7 @@ async function saveAnalysisGroup(
const analysisElement = item.UuringuElement; const analysisElement = item.UuringuElement;
const { data: insertedAnalysisElement, error } = await supabase const { data: insertedAnalysisElement, error } = await supabase
.from("analysis_elements") .from('analysis_elements')
.upsert( .upsert(
{ {
analysis_id_oid: analysisElement.UuringIdOID, analysis_id_oid: analysisElement.UuringIdOID,
@@ -216,13 +234,13 @@ async function saveAnalysisGroup(
parent_analysis_group_id: analysisGroupId, parent_analysis_group_id: analysisGroupId,
material_groups: toArray(item.MaterjalideGrupp), material_groups: toArray(item.MaterjalideGrupp),
}, },
{ onConflict: "analysis_id_original", ignoreDuplicates: false } { onConflict: 'analysis_id_original', ignoreDuplicates: false },
) )
.select('id'); .select('id');
if (error || !insertedAnalysisElement[0]?.id) { if (error || !insertedAnalysisElement[0]?.id) {
throw new Error( throw new Error(
`Failed to insert analysis element (id: ${analysisElement.UuringId}), error: ${error?.message}` `Failed to insert analysis element (id: ${analysisElement.UuringId}), error: ${error?.message}`,
); );
} }
@@ -237,7 +255,7 @@ async function saveAnalysisGroup(
coefficient: kood.Koefitsient, coefficient: kood.Koefitsient,
price: kood.Hind, price: kood.Hind,
analysis_element_id: insertedAnalysisElementId, analysis_element_id: insertedAnalysisElementId,
})) })),
); );
} }
@@ -245,7 +263,7 @@ async function saveAnalysisGroup(
if (analyses?.length) { if (analyses?.length) {
for (const analysis of analyses) { for (const analysis of analyses) {
const { data: insertedAnalysis, error } = await supabase const { data: insertedAnalysis, error } = await supabase
.from("analyses") .from('analyses')
.upsert( .upsert(
{ {
analysis_id_oid: analysis.UuringIdOID, analysis_id_oid: analysis.UuringIdOID,
@@ -256,13 +274,13 @@ async function saveAnalysisGroup(
order: analysis.Jarjekord, order: analysis.Jarjekord,
parent_analysis_element_id: insertedAnalysisElementId, parent_analysis_element_id: insertedAnalysisElementId,
}, },
{ onConflict: "analysis_id_original", ignoreDuplicates: false } { onConflict: 'analysis_id_original', ignoreDuplicates: false },
) )
.select('id'); .select('id');
if (error || !insertedAnalysis[0]?.id) { if (error || !insertedAnalysis[0]?.id) {
throw new Error( throw new Error(
`Failed to insert analysis (id: ${analysis.UuringId}) error: ${error?.message}` `Failed to insert analysis (id: ${analysis.UuringId}) error: ${error?.message}`,
); );
} }
@@ -277,7 +295,7 @@ async function saveAnalysisGroup(
coefficient: kood.Koefitsient, coefficient: kood.Koefitsient,
price: kood.Hind, price: kood.Hind,
analysis_id: insertedAnalysisId, analysis_id: insertedAnalysisId,
})) })),
); );
} }
} }
@@ -285,20 +303,21 @@ async function saveAnalysisGroup(
} }
const { error: codesError } = await supabase const { error: codesError } = await supabase
.from("codes") .from('codes')
.upsert(codes, { ignoreDuplicates: false }); .upsert(codes, { ignoreDuplicates: false });
if (codesError?.code) { if (codesError?.code) {
console.error(codesError); // TODO remove this
throw new Error( throw new Error(
`Failed to insert codes (analysis group id: ${analysisGroup.UuringuGruppId})` `Failed to insert codes (analysis group id: ${analysisGroup.UuringuGruppId})`,
); );
} }
} }
export async function syncPublicMessage( export async function syncPublicMessage(
message?: MedipostPublicMessageResponse | null message?: MedipostPublicMessageResponse | null,
) { ) {
const supabase = createClient( const supabase = createCustomClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!, process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!,
{ {
@@ -307,20 +326,20 @@ export async function syncPublicMessage(
autoRefreshToken: false, autoRefreshToken: false,
detectSessionInUrl: false, detectSessionInUrl: false,
}, },
} },
); );
try { try {
const providers = toArray(message?.Saadetis?.Teenused.Teostaja); const providers = toArray(message?.Saadetis?.Teenused.Teostaja);
const analysisGroups = providers.flatMap((provider) => const analysisGroups = providers.flatMap((provider) =>
toArray(provider.UuringuGrupp) toArray(provider.UuringuGrupp),
); );
if (!message || !analysisGroups.length) { if (!message || !analysisGroups.length) {
return supabase.schema("audit").from("sync_entries").insert({ return supabase.schema('audit').from('sync_entries').insert({
operation: "ANALYSES_SYNC", operation: 'ANALYSES_SYNC',
comment: "No data received", comment: 'No data received',
status: SyncStatus.Fail, status: SyncStatus.Fail,
changed_by_role: "service_role", changed_by_role: 'service_role',
}); });
} }
@@ -328,31 +347,170 @@ export async function syncPublicMessage(
await saveAnalysisGroup(analysisGroup, supabase); await saveAnalysisGroup(analysisGroup, supabase);
} }
await supabase.schema("audit").from("sync_entries").insert({ await supabase.schema('audit').from('sync_entries').insert({
operation: "ANALYSES_SYNC", operation: 'ANALYSES_SYNC',
status: SyncStatus.Success, status: SyncStatus.Success,
changed_by_role: "service_role", changed_by_role: 'service_role',
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
await supabase await supabase
.schema("audit") .schema('audit')
.from("sync_entries") .from('sync_entries')
.insert({ .insert({
operation: "ANALYSES_SYNC", operation: 'ANALYSES_SYNC',
status: SyncStatus.Fail, status: SyncStatus.Fail,
comment: JSON.stringify(e), comment: JSON.stringify(e),
changed_by_role: "service_role", changed_by_role: 'service_role',
}); });
} }
} }
// TODO use actual parameters
export async function composeOrderXML(
/* chosenAnalysisElements?: number[],
chosenAnalyses?: number[], */
comment?: string,
) {
const supabase = createCustomClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!,
{
auth: {
persistSession: false,
autoRefreshToken: false,
detectSessionInUrl: false,
},
},
);
// TODO remove dummy when actual implemetation is present
const orderedElements = [1, 75];
const orderedAnalyses = [10, 11, 100];
const createdAnalysisOrder = {
id: 4,
user_id: 'currentUser.user?.id',
analysis_element_ids: orderedElements,
analysis_ids: orderedAnalyses,
status: AnalysisOrderStatus[1],
created_at: new Date(),
};
const { data: analysisElements } = (await supabase
.from('analysis_elements')
.select(`*, analysis_groups(*)`)
.in('id', orderedElements)) as {
data: ({
analysis_groups: Tables<'analysis_groups'>;
} & Tables<'analysis_elements'>)[];
};
const { data: analyses } = (await supabase
.from('analyses')
.select(`*, analysis_elements(*, analysis_groups(*))`)
.in('id', orderedAnalyses)) as {
data: ({
analysis_elements: Tables<'analysis_elements'> & {
analysis_groups: Tables<'analysis_groups'>;
};
} & Tables<'analyses'>)[];
};
const analysisGroups: Tables<'analysis_groups'>[] = uniqBy(
(
analysisElements?.flatMap(({ analysis_groups }) => analysis_groups) ?? []
).concat(
analyses?.flatMap(
({ analysis_elements }) => analysis_elements.analysis_groups,
) ?? [],
),
'id',
);
const specimenSection = [];
const analysisSection = [];
let order = 1;
for (const currentGroup of analysisGroups) {
let relatedAnalysisElement = analysisElements?.find(
(element) => element.analysis_groups.id === currentGroup.id,
);
const relatedAnalyses = analyses?.filter((analysis) => {
return analysis.analysis_elements.analysis_groups.id === currentGroup.id;
});
if (!relatedAnalysisElement) {
relatedAnalysisElement = relatedAnalyses?.find(
(relatedAnalysis) =>
relatedAnalysis.analysis_elements.analysis_groups.id ===
currentGroup.id,
)?.analysis_elements;
}
if (!relatedAnalysisElement || !relatedAnalysisElement.material_groups) {
throw new Error(
`Failed to find related analysis element for group ${currentGroup.name} (id: ${currentGroup.id})`,
);
}
for (const group of relatedAnalysisElement?.material_groups as MaterjalideGrupp[]) {
const materials = toArray(group.Materjal);
const specimenXml = materials.flatMap(
({ MaterjaliNimi, MaterjaliTyyp, MaterjaliTyypOID, Konteiner }) => {
return toArray(Konteiner).map((container) =>
getSpecimen(
MaterjaliTyypOID,
MaterjaliTyyp,
MaterjaliNimi,
order,
container.ProovinouKoodOID,
container.ProovinouKood,
),
);
},
);
specimenSection.push(...specimenXml);
}
const groupXml = getAnalysisGroup(
currentGroup.original_id,
currentGroup.name,
order,
relatedAnalysisElement,
);
order++;
analysisSection.push(groupXml);
}
// TODO get actual data when order creation is implemented
return `<?xml version="1.0" encoding="UTF-8"?>
<Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
${getPais(process.env.MEDIPOST_USER!, process.env.MEDIPOST_RECIPIENT!, createdAnalysisOrder.created_at, createdAnalysisOrder.id)}
<Tellimus cito="EI">
<ValisTellimuseId>${createdAnalysisOrder.id}</ValisTellimuseId>
<!--<TellijaAsutus>-->
${getClientInstitution()}
<!--<TeostajaAsutus>-->
${getProviderInstitution()}
<!--<TellijaIsik>-->
${getClientPerson()}
<!--<SisestajaIsik>-->
${getOrderEnteredByPerson()}
<TellijaMarkused>${comment ?? ''}</TellijaMarkused>
${getPatient(49610230861, 'Surname', 'First Name', '1996-10-23', 'N')}
${getConfidentiality()}
${specimenSection.join('')}
${analysisSection?.join('')}
</Tellimus>
</Saadetis>`;
}
function getLatestMessage(messages?: Message[]) { function getLatestMessage(messages?: Message[]) {
if (!messages?.length) { if (!messages?.length) {
return null; return null;
} }
return messages.reduce((prev, current) => return messages.reduce((prev, current) =>
Number(prev.messageId) > Number(current.messageId) ? prev : current Number(prev.messageId) > Number(current.messageId) ? prev : current,
); );
} }

View File

@@ -0,0 +1,172 @@
import { DATE_TIME_FORMAT } from '@/lib/constants';
import { Tables } from '@/supabase/database.types';
import { format } from 'date-fns';
const isProd = process.env.NODE_ENV === 'production';
export const getPais = (
sender: string,
recipient: string,
createdAt: Date,
messageId: number,
) => {
if (isProd) {
// return correct data
}
return `<Pais>
<Pakett versioon="20">OL</Pakett>
<Saatja>${sender}</Saatja>
<Saaja>${recipient}</Saaja>
<Aeg>${format(createdAt, DATE_TIME_FORMAT)}</Aeg>
<SaadetisId>${messageId}</SaadetisId>
<Email>argo@medreport.ee</Email>
</Pais>`;
};
export const getClientInstitution = () => {
if (isProd) {
// return correct data
}
return `<Asutus tyyp="TELLIJA">
<AsutuseId>16381793</AsutuseId>
<AsutuseNimi>MedReport OÜ</AsutuseNimi>
<AsutuseKood>TSU</AsutuseKood>
<Telefon>+37258871517</Telefon>
</Asutus>`;
};
export const getProviderInstitution = () => {
if (isProd) {
// return correct data
}
return `<Asutus tyyp="TEOSTAJA">
<AsutuseId>11107913</AsutuseId>
<AsutuseNimi>Synlab HTI Tallinn</AsutuseNimi>
<AsutuseKood>SLA</AsutuseKood>
<AllyksuseNimi>Synlab HTI Tallinn</AllyksuseNimi>
<Telefon>+3723417123</Telefon>
</Asutus>`;
};
export const getClientPerson = () => {
if (isProd) {
// return correct data
}
return `<Personal tyyp="TELLIJA" jarjenumber="1">
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<PersonalKood>D07907</PersonalKood>
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
<PersonalEesNimi>Tsvetkov</PersonalEesNimi>
<Telefon>+37258131202</Telefon>
</Personal>`;
};
export const getOrderEnteredPerson = () => {
if (isProd) {
// return correct data
}
return `<Personal tyyp="SISESTAJA" jarjenumber="1">
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<PersonalKood>D07907</PersonalKood>
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
<PersonalEesNimi>Tsvetkov</PersonalEesNimi>
<Telefon>+37258131202</Telefon>
</Personal>`;
};
export const getPatient = (
idCode: number,
surname: string,
firstName: string,
birthDate: string,
genderLetter: string,
) => {
return `<Patsient>
<IsikukoodiOID>1.3.6.1.4.1.28284.6.2.2.1</IsikukoodiOID>
<Isikukood>${idCode}</Isikukood>
<PerekonnaNimi>${surname}</PerekonnaNimi>
<EesNimi>${firstName}</EesNimi>
<SynniAeg>${birthDate}</SynniAeg>
<SuguOID>1.3.6.1.4.1.28284.6.2.3.16.2</SuguOID>
<Sugu>${genderLetter}</Sugu>
</Patsient>`;
};
export const getConfidentiality = () => {
if (isProd) {
// return correct data
}
return `<Konfidentsiaalsus>
<PatsiendileOID>2.16.840.1.113883.5.25</PatsiendileOID>
<Patsiendile>N</Patsiendile>
<ArstileOID>1.3.6.1.4.1.28284.6.2.2.39.1</ArstileOID>
<Arstile>N</Arstile>
<EsindajaleOID>1.3.6.1.4.1.28284.6.2.2.37.1</EsindajaleOID>
<Esindajale>N</Esindajale>
</Konfidentsiaalsus>`;
};
export const getOrderEnteredByPerson = () => {
if (isProd) {
// return correct data
}
return `
<Personal tyyp="SISESTAJA" jarjenumber="1">
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<PersonalKood>D07907</PersonalKood>
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
<PersonalEesNimi>Tsvetkov</PersonalEesNimi>
<Telefon>+37258131202</Telefon>
</Personal>`;
};
export const getSpecimen = (
materialTypeOid: string,
materialGroupId: string,
materialName: string,
order: number,
sampleContainerOid?: string,
sampleContainerId?: string,
) =>
`
<Proov>
${sampleContainerOid ? `<ProovinouIdOID>${sampleContainerOid}</ProovinouIdOID>` : null}
${sampleContainerId ? `<ProovinouId>${sampleContainerId}</ProovinouId>` : null}
<MaterjaliTyypOID>${materialTypeOid}</MaterjaliTyypOID>
<MaterjaliTyyp>${materialGroupId}</MaterjaliTyyp>
<MaterjaliNimi>${materialName}</MaterjaliNimi>
<Jarjenumber>${order}</Jarjenumber>
</Proov>`;
export const getAnalysisElement = (
analysisIdOid: string,
analysisIdOriginal: string,
tehikShortLoinc: string,
tehikLoincName: string,
analysisElementId: number,
analysisNameLab?: string | null,
) => {
return `<UuringuElement>
<UuringIdOID>${analysisIdOid}</UuringIdOID>
<UuringId>${analysisIdOriginal}</UuringId>
<TLyhend>${tehikShortLoinc}</TLyhend>
<KNimetus>${tehikLoincName}</KNimetus>
<UuringNimi>${analysisNameLab ?? tehikLoincName}</UuringNimi>
<TellijaUuringId>${analysisElementId}</TellijaUuringId>
</UuringuElement>`;
};
export const getAnalysisGroup = (
analysisGroupOriginalId: string,
analysisGroupName: string,
specimenOrderNr: number,
analysisElement: Tables<'analysis_elements'>,
) =>
`<UuringuGrupp>
<UuringuGruppId>${analysisGroupOriginalId}</UuringuGruppId>
<UuringuGruppNimi>${analysisGroupName}</UuringuGruppNimi>
<Uuring>
${getAnalysisElement(analysisElement.analysis_id_oid, analysisElement.analysis_id_original, analysisElement.tehik_short_loinc, analysisElement.tehik_loinc_name, analysisElement.id, analysisElement.analysis_name_lab)}
<ProoviJarjenumber>${specimenOrderNr}</ProoviJarjenumber>
</Uuring>
</UuringuGrupp>`;

View File

@@ -74,7 +74,7 @@ export type UuringuElement = {
export type Uuring = { export type Uuring = {
tellitav: "JAH" | "EI"; tellitav: "JAH" | "EI";
UuringuElement: UuringuElement; //1..1 UuringuElement: UuringuElement; //1..1
MaterjalideGrupp?: MaterjalideGrupp[]; //0..n MaterjalideGrupp?: MaterjalideGrupp | MaterjalideGrupp[]; //0..n If this is not present, then the analysis can't be ordered.
}; };
export type UuringuGrupp = { export type UuringuGrupp = {
@@ -86,10 +86,10 @@ export type UuringuGrupp = {
}; };
export type Konteiner = { export type Konteiner = {
ProovinouKoodOID: string; ProovinouKoodOID?: string; //0..1
ProovinouKood: string; ProovinouKood?: string; //0..1
KonteineriNimi: string; KonteineriNimi?: string; //0..1
KonteineriKirjeldus: string; KonteineriKirjeldus: string; //1..1
}; };
export type Materjal = { export type Materjal = {
@@ -98,12 +98,13 @@ export type Materjal = {
MaterjaliNimi: string; MaterjaliNimi: string;
KonteineriOmadus: string; KonteineriOmadus: string;
MaterjaliPaige: { Kohustuslik: "JAH" | "EI" }; //0..1 MaterjaliPaige: { Kohustuslik: "JAH" | "EI" }; //0..1
Konteiner?: Konteiner[]; //0..n MaterjalJarjekord?: number; //0..1
Konteiner?: Konteiner | Konteiner[]; //0..n
}; };
export type MaterjalideGrupp = { export type MaterjalideGrupp = {
vaikimisi: "JAH" | "EI"; vaikimisi: "JAH" | "EI";
Materjal: Materjal; //1..n Materjal: Materjal | Materjal[]; //1..n
}; };
export type Teostaja = { export type Teostaja = {
@@ -139,3 +140,12 @@ export type MedipostPublicMessageResponse = {
}; };
}; };
}; };
export const AnalysisOrderStatus = {
1: "QUEUED",
2: "ON_HOLD",
3: "PROCESSING",
4: "COMPLETED",
5: "REJECTED",
6: "CANCELLED",
} as const;

View File

@@ -63,7 +63,8 @@
"axios": "^1.9.0", "axios": "^1.9.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"fast-xml-parser": "^5.2.3", "fast-xml-parser": "^5.2.5",
"lodash": "^4.17.21",
"lucide-react": "^0.510.0", "lucide-react": "^0.510.0",
"next": "15.3.2", "next": "15.3.2",
"next-sitemap": "^4.2.3", "next-sitemap": "^4.2.3",
@@ -87,6 +88,7 @@
"@types/node": "^22.15.18", "@types/node": "^22.15.18",
"@types/react": "19.1.4", "@types/react": "19.1.4",
"@types/react-dom": "19.1.5", "@types/react-dom": "19.1.5",
"@types/lodash": "^4.17.17",
"babel-plugin-react-compiler": "19.1.0-rc.2", "babel-plugin-react-compiler": "19.1.0-rc.2",
"cssnano": "^7.0.7", "cssnano": "^7.0.7",
"pino-pretty": "^13.0.0", "pino-pretty": "^13.0.0",

8
pnpm-lock.yaml generated
View File

@@ -105,8 +105,11 @@ importers:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.1.0
fast-xml-parser: fast-xml-parser:
specifier: ^5.2.3 specifier: ^5.2.5
version: 5.2.5 version: 5.2.5
lodash:
specifier: ^4.17.21
version: 4.17.21
lucide-react: lucide-react:
specifier: ^0.510.0 specifier: ^0.510.0
version: 0.510.0(react@19.1.0) version: 0.510.0(react@19.1.0)
@@ -159,6 +162,9 @@ importers:
'@tailwindcss/postcss': '@tailwindcss/postcss':
specifier: ^4.1.7 specifier: ^4.1.7
version: 4.1.8 version: 4.1.8
'@types/lodash':
specifier: ^4.17.17
version: 4.17.17
'@types/node': '@types/node':
specifier: ^22.15.18 specifier: ^22.15.18
version: 22.15.30 version: 22.15.30

View File

@@ -50,24 +50,27 @@ export type Database = {
} }
sync_entries: { sync_entries: {
Row: { Row: {
changed_by_role: string | null changed_by_role: string
comment: string | null
created_at: string created_at: string
id: number id: number
operation: string | null operation: string
status: string status: string
} }
Insert: { Insert: {
changed_by_role?: string | null changed_by_role: string
comment?: string | null
created_at?: string created_at?: string
id?: number id?: number
operation?: string | null operation: string
status: string status: string
} }
Update: { Update: {
changed_by_role?: string | null changed_by_role?: string
comment?: string | null
created_at?: string created_at?: string
id?: number id?: number
operation?: string | null operation?: string
status?: string status?: string
} }
Relationships: [] Relationships: []
@@ -224,7 +227,7 @@ export type Database = {
analysis_name_lab: string | null analysis_name_lab: string | null
created_at: string created_at: string
id: number id: number
order: number | null order: number
parent_analysis_element_id: number parent_analysis_element_id: number
tehik_loinc_name: string | null tehik_loinc_name: string | null
tehik_short_loinc: string | null tehik_short_loinc: string | null
@@ -236,7 +239,7 @@ export type Database = {
analysis_name_lab?: string | null analysis_name_lab?: string | null
created_at?: string created_at?: string
id?: number id?: number
order?: number | null order: number
parent_analysis_element_id: number parent_analysis_element_id: number
tehik_loinc_name?: string | null tehik_loinc_name?: string | null
tehik_short_loinc?: string | null tehik_short_loinc?: string | null
@@ -248,7 +251,7 @@ export type Database = {
analysis_name_lab?: string | null analysis_name_lab?: string | null
created_at?: string created_at?: string
id?: number id?: number
order?: number | null order?: number
parent_analysis_element_id?: number parent_analysis_element_id?: number
tehik_loinc_name?: string | null tehik_loinc_name?: string | null
tehik_short_loinc?: string | null tehik_short_loinc?: string | null
@@ -266,42 +269,42 @@ export type Database = {
} }
analysis_elements: { analysis_elements: {
Row: { Row: {
analysis_id_oid: string | null analysis_id_oid: string
analysis_id_original: string analysis_id_original: string
analysis_name_lab: string | null analysis_name_lab: string | null
created_at: string created_at: string
id: number id: number
material_groups: Json[] | null material_groups: Json[] | null
order: number | null order: number
parent_analysis_group_id: number parent_analysis_group_id: number
tehik_loinc_name: string | null tehik_loinc_name: string
tehik_short_loinc: string | null tehik_short_loinc: string
updated_at: string | null updated_at: string | null
} }
Insert: { Insert: {
analysis_id_oid?: string | null analysis_id_oid: string
analysis_id_original: string analysis_id_original: string
analysis_name_lab?: string | null analysis_name_lab?: string | null
created_at?: string created_at?: string
id?: number id?: number
material_groups?: Json[] | null material_groups?: Json[] | null
order?: number | null order: number
parent_analysis_group_id: number parent_analysis_group_id: number
tehik_loinc_name?: string | null tehik_loinc_name: string
tehik_short_loinc?: string | null tehik_short_loinc: string
updated_at?: string | null updated_at?: string | null
} }
Update: { Update: {
analysis_id_oid?: string | null analysis_id_oid?: string
analysis_id_original?: string analysis_id_original?: string
analysis_name_lab?: string | null analysis_name_lab?: string | null
created_at?: string created_at?: string
id?: number id?: number
material_groups?: Json[] | null material_groups?: Json[] | null
order?: number | null order?: number
parent_analysis_group_id?: number parent_analysis_group_id?: number
tehik_loinc_name?: string | null tehik_loinc_name?: string
tehik_short_loinc?: string | null tehik_short_loinc?: string
updated_at?: string | null updated_at?: string | null
} }
Relationships: [ Relationships: [
@@ -318,29 +321,53 @@ export type Database = {
Row: { Row: {
created_at: string created_at: string
id: number id: number
name: string | null name: string
order: number | null order: number
original_id: string original_id: string
updated_at: string | null updated_at: string | null
} }
Insert: { Insert: {
created_at?: string created_at?: string
id?: number id?: number
name?: string | null name: string
order?: number | null order: number
original_id: string original_id: string
updated_at?: string | null updated_at?: string | null
} }
Update: { Update: {
created_at?: string created_at?: string
id?: number id?: number
name?: string | null name?: string
order?: number | null order?: number
original_id?: string original_id?: string
updated_at?: string | null updated_at?: string | null
} }
Relationships: [] Relationships: []
} }
analysis_orders: {
Row: {
analysis_group_ids: number[] | null
created_at: string
id: string
status: Database["public"]["Enums"]["analysis_order_status"]
user_id: string
}
Insert: {
analysis_group_ids?: number[] | null
created_at?: string
id: string
status: Database["public"]["Enums"]["analysis_order_status"]
user_id: string
}
Update: {
analysis_group_ids?: number[] | null
created_at?: string
id?: string
status?: Database["public"]["Enums"]["analysis_order_status"]
user_id?: string
}
Relationships: []
}
billing_customers: { billing_customers: {
Row: { Row: {
account_id: string account_id: string
@@ -1111,6 +1138,13 @@ export type Database = {
} }
} }
Enums: { Enums: {
analysis_order_status:
| "QUEUED"
| "ON_HOLD"
| "PROCESSING"
| "COMPLETED"
| "REJECTED"
| "CANCELLED"
app_permissions: app_permissions:
| "roles.manage" | "roles.manage"
| "billing.manage" | "billing.manage"
@@ -1257,6 +1291,14 @@ export const Constants = {
}, },
public: { public: {
Enums: { Enums: {
analysis_order_status: [
"QUEUED",
"ON_HOLD",
"PROCESSING",
"COMPLETED",
"REJECTED",
"CANCELLED",
],
app_permissions: [ app_permissions: [
"roles.manage", "roles.manage",
"billing.manage", "billing.manage",

View File

@@ -0,0 +1,91 @@
create type "public"."analysis_order_status" as enum ('QUEUED', 'ON_HOLD', 'PROCESSING', 'COMPLETED', 'REJECTED', 'CANCELLED');
create table "public"."analysis_orders" (
"id" uuid not null,
"analysis_element_ids" bigint[],
"analysis_ids" bigint[],
"user_id" uuid not null,
"status" public.analysis_order_status not null,
"created_at" timestamp with time zone not null default now()
);
alter table "public"."analysis_orders" enable row level security;
CREATE UNIQUE INDEX analysis_orders_pkey ON public.analysis_orders USING btree (id);
alter table "public"."analysis_orders" add constraint "analysis_orders_pkey" PRIMARY KEY using index "analysis_orders_pkey";
alter table "public"."analysis_orders" add constraint "analysis_orders_id_fkey" FOREIGN KEY (id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
alter table "public"."analysis_orders" validate constraint "analysis_orders_id_fkey";
grant delete on table "public"."analysis_orders" to "authenticated";
grant insert on table "public"."analysis_orders" to "authenticated";
grant references on table "public"."analysis_orders" to "authenticated";
grant select on table "public"."analysis_orders" to "authenticated";
grant trigger on table "public"."analysis_orders" to "authenticated";
grant truncate on table "public"."analysis_orders" to "authenticated";
grant update on table "public"."analysis_orders" to "authenticated";
grant delete on table "public"."analysis_orders" to "service_role";
grant insert on table "public"."analysis_orders" to "service_role";
grant references on table "public"."analysis_orders" to "service_role";
grant select on table "public"."analysis_orders" to "service_role";
grant trigger on table "public"."analysis_orders" to "service_role";
grant truncate on table "public"."analysis_orders" to "service_role";
grant update on table "public"."analysis_orders" to "service_role";
create policy "analysis_all"
on "public"."analysis_orders"
as permissive
for all
to authenticated, service_role
using (true);
create policy "analysis_select"
on "public"."analyses"
as permissive
for select
to public
using (true);
create policy "analysis_elements_select"
on "public"."analysis_elements"
as permissive
for select
to public
using (true);
create policy "analysis_groups_select"
on "public"."analysis_groups"
as permissive
for select
to public
using (true);
-- Drop previously created unnecessary indices
drop index if exists "public"."analysis_elements_original_id_key";
drop index if exists "public"."analysis_original_id_key";
-- Remove nullable from previously added fields
alter table "public"."analyses" alter column "order" set not null;
alter table "public"."analysis_elements" alter column "analysis_id_oid" set not null;
alter table "public"."analysis_elements" alter column "order" set not null;
alter table "public"."analysis_groups" alter column "name" set not null;
alter table "public"."analysis_groups" alter column "order" set not null;
alter table "public"."analysis_elements" alter column "tehik_loinc_name" set not null;
alter table "public"."analysis_elements" alter column "tehik_short_loinc" set not null;