diff --git a/.env.example b/.env.example
index b35abfa..3686882 100644
--- a/.env.example
+++ b/.env.example
@@ -4,6 +4,7 @@ NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
-MEDIPOST_URL=your-medpost-url
-MEDIPOST_USER=your-medpost-user
-MEDIPOST_PASSWORD=your-medpost-password
\ No newline at end of file
+MEDIPOST_URL=your-medipost-url
+MEDIPOST_USER=your-medipost-user
+MEDIPOST_PASSWORD=your-medipost-password
+MEDIPOST_RECIPIENT=your-medipost-recipient
\ No newline at end of file
diff --git a/app/(public)/register-company/page.tsx b/app/(public)/register-company/page.tsx
index e3e54e7..5099e36 100644
--- a/app/(public)/register-company/page.tsx
+++ b/app/(public)/register-company/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { MedReportTitle } from "@/components/MedReportTitle";
+import { MedReportTitle } from "@/components/med-report-title";
import React from "react";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form";
diff --git a/app/(public)/register-company/success/page.tsx b/app/(public)/register-company/success/page.tsx
index 688dbb0..9682cff 100644
--- a/app/(public)/register-company/success/page.tsx
+++ b/app/(public)/register-company/success/page.tsx
@@ -1,5 +1,4 @@
-import { MedReportTitle } from "@/components/MedReportTitle";
-import { Button } from "@/packages/ui/src/shadcn/button";
+import { MedReportTitle } from "@/components/med-report-title";
import Image from "next/image";
import Link from "next/link";
diff --git a/components/header-auth.tsx b/components/header-auth.tsx
index aab2ec7..d560200 100644
--- a/components/header-auth.tsx
+++ b/components/header-auth.tsx
@@ -1,8 +1,8 @@
import { signOutAction } from "@/lib/actions/sign-out";
import { hasEnvVars } from "@/utils/supabase/check-env-vars";
import Link from "next/link";
-import { Badge } from "./ui/badge";
-import { Button } from "./ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
import { createClient } from "@/utils/supabase/server";
export default async function AuthButton() {
diff --git a/components/MedReportTitle.tsx b/components/med-report-title.tsx
similarity index 75%
rename from components/MedReportTitle.tsx
rename to components/med-report-title.tsx
index a010e11..e8398cb 100644
--- a/components/MedReportTitle.tsx
+++ b/components/med-report-title.tsx
@@ -1,4 +1,4 @@
-import { MedReportSmallLogo } from "@/public/assets/MedReportSmallLogo";
+import { MedReportSmallLogo } from "@/public/assets/med-report-small-logo";
export const MedReportTitle = () => (
diff --git a/lib/constants.ts b/lib/constants.ts
new file mode 100644
index 0000000..7092724
--- /dev/null
+++ b/lib/constants.ts
@@ -0,0 +1,2 @@
+export const DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+export const DATE_FORMAT = "yyyy-mm-dd";
diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts
index f6f1b9c..3f1007f 100644
--- a/lib/services/medipost.service.ts
+++ b/lib/services/medipost.service.ts
@@ -1,16 +1,34 @@
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,
+ MaterjalideGrupp,
MedipostAction,
MedipostPublicMessageResponse,
Message,
UuringuGrupp,
-} from "@/lib/types/medipost";
-import { Tables } from "@/supabase/database.types";
-import { createClient, SupabaseClient } from "@supabase/supabase-js";
-import axios from "axios";
-import { XMLParser } from "fast-xml-parser";
-import { SyncStatus } from "@/lib/types/audit";
-import { toArray } from "@/lib/utils";
+} from '@/lib/types/medipost';
+import { toArray } from '@/lib/utils';
+import { Tables } from '@/supabase/database.types';
+import axios from 'axios';
+import { XMLParser } from 'fast-xml-parser';
+import { uniqBy } from 'lodash';
const BASE_URL = process.env.MEDIPOST_URL!;
const USER = process.env.MEDIPOST_USER!;
@@ -37,14 +55,14 @@ export async function getLatestPublicMessageListItem() {
Action: MedipostAction.GetPublicMessageList,
User: USER,
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
// MessageType check only for messages of certain type
},
});
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);
@@ -59,7 +77,7 @@ export async function getPublicMessage(messageId: string) {
MessageId: messageId,
},
headers: {
- Accept: "application/xml",
+ Accept: 'application/xml',
},
});
const parser = new XMLParser({ ignoreAttributes: false });
@@ -74,16 +92,16 @@ export async function getPublicMessage(messageId: string) {
export async function sendPrivateMessage(messageXml: string, receiver: string) {
const body = new FormData();
- body.append("Action", MedipostAction.SendPrivateMessage);
- body.append("User", USER);
- body.append("Password", PASSWORD);
- body.append("Receiver", receiver);
- body.append("MessageType", "Tellimus");
+ body.append('Action', MedipostAction.SendPrivateMessage);
+ body.append('User', USER);
+ body.append('Password', PASSWORD);
+ body.append('Receiver', receiver);
+ body.append('MessageType', 'Tellimus');
body.append(
- "Message",
+ 'Message',
new Blob([messageXml], {
- type: "text/xml; charset=UTF-8",
- })
+ type: 'text/xml; charset=UTF-8',
+ }),
);
const { data } = await axios.post(BASE_URL, body);
@@ -103,7 +121,7 @@ export async function getLatestPrivateMessageListItem() {
});
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);
@@ -118,7 +136,7 @@ export async function getPrivateMessage(messageId: string) {
MessageId: messageId,
},
headers: {
- Accept: "application/xml",
+ Accept: 'application/xml',
},
});
@@ -154,7 +172,7 @@ export async function readPrivateMessageResponse() {
}
const privateMessageContent = await getPrivateMessage(
- privateMessage.messageId
+ privateMessage.messageId,
);
if (privateMessageContent) {
@@ -168,29 +186,29 @@ export async function readPrivateMessageResponse() {
async function saveAnalysisGroup(
analysisGroup: UuringuGrupp,
- supabase: SupabaseClient
+ supabase: SupabaseClient,
) {
const { data: insertedAnalysisGroup, error } = await supabase
- .from("analysis_groups")
+ .from('analysis_groups')
.upsert(
{
original_id: analysisGroup.UuringuGruppId,
name: analysisGroup.UuringuGruppNimi,
order: analysisGroup.UuringuGruppJarjekord,
},
- { onConflict: "original_id", ignoreDuplicates: false }
+ { onConflict: 'original_id', ignoreDuplicates: false },
)
- .select("id");
+ .select('id');
if (error || !insertedAnalysisGroup[0]?.id) {
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 analysisGroupCodes = toArray(analysisGroup.Kood);
- const codes: Partial
>[] = analysisGroupCodes.map((kood) => ({
+ const codes: Partial>[] = analysisGroupCodes.map((kood) => ({
hk_code: kood.HkKood,
hk_code_multiplier: kood.HkKoodiKordaja,
coefficient: kood.Koefitsient,
@@ -204,7 +222,7 @@ async function saveAnalysisGroup(
const analysisElement = item.UuringuElement;
const { data: insertedAnalysisElement, error } = await supabase
- .from("analysis_elements")
+ .from('analysis_elements')
.upsert(
{
analysis_id_oid: analysisElement.UuringIdOID,
@@ -216,13 +234,13 @@ async function saveAnalysisGroup(
parent_analysis_group_id: analysisGroupId,
material_groups: toArray(item.MaterjalideGrupp),
},
- { onConflict: "analysis_id_original", ignoreDuplicates: false }
+ { onConflict: 'analysis_id_original', ignoreDuplicates: false },
)
.select('id');
if (error || !insertedAnalysisElement[0]?.id) {
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,
price: kood.Hind,
analysis_element_id: insertedAnalysisElementId,
- }))
+ })),
);
}
@@ -245,7 +263,7 @@ async function saveAnalysisGroup(
if (analyses?.length) {
for (const analysis of analyses) {
const { data: insertedAnalysis, error } = await supabase
- .from("analyses")
+ .from('analyses')
.upsert(
{
analysis_id_oid: analysis.UuringIdOID,
@@ -256,13 +274,13 @@ async function saveAnalysisGroup(
order: analysis.Jarjekord,
parent_analysis_element_id: insertedAnalysisElementId,
},
- { onConflict: "analysis_id_original", ignoreDuplicates: false }
+ { onConflict: 'analysis_id_original', ignoreDuplicates: false },
)
.select('id');
if (error || !insertedAnalysis[0]?.id) {
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,
price: kood.Hind,
analysis_id: insertedAnalysisId,
- }))
+ })),
);
}
}
@@ -285,20 +303,21 @@ async function saveAnalysisGroup(
}
const { error: codesError } = await supabase
- .from("codes")
+ .from('codes')
.upsert(codes, { ignoreDuplicates: false });
if (codesError?.code) {
+ console.error(codesError); // TODO remove this
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(
- message?: MedipostPublicMessageResponse | null
+ message?: MedipostPublicMessageResponse | null,
) {
- const supabase = createClient(
+ const supabase = createCustomClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!,
{
@@ -307,20 +326,20 @@ export async function syncPublicMessage(
autoRefreshToken: false,
detectSessionInUrl: false,
},
- }
+ },
);
try {
const providers = toArray(message?.Saadetis?.Teenused.Teostaja);
const analysisGroups = providers.flatMap((provider) =>
- toArray(provider.UuringuGrupp)
+ toArray(provider.UuringuGrupp),
);
if (!message || !analysisGroups.length) {
- return supabase.schema("audit").from("sync_entries").insert({
- operation: "ANALYSES_SYNC",
- comment: "No data received",
+ return supabase.schema('audit').from('sync_entries').insert({
+ operation: 'ANALYSES_SYNC',
+ comment: 'No data received',
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 supabase.schema("audit").from("sync_entries").insert({
- operation: "ANALYSES_SYNC",
+ await supabase.schema('audit').from('sync_entries').insert({
+ operation: 'ANALYSES_SYNC',
status: SyncStatus.Success,
- changed_by_role: "service_role",
+ changed_by_role: 'service_role',
});
} catch (e) {
console.error(e);
await supabase
- .schema("audit")
- .from("sync_entries")
+ .schema('audit')
+ .from('sync_entries')
.insert({
- operation: "ANALYSES_SYNC",
+ operation: 'ANALYSES_SYNC',
status: SyncStatus.Fail,
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 `
+
+ ${getPais(process.env.MEDIPOST_USER!, process.env.MEDIPOST_RECIPIENT!, createdAnalysisOrder.created_at, createdAnalysisOrder.id)}
+
+ ${createdAnalysisOrder.id}
+
+ ${getClientInstitution()}
+
+ ${getProviderInstitution()}
+
+ ${getClientPerson()}
+
+ ${getOrderEnteredByPerson()}
+ ${comment ?? ''}
+ ${getPatient(49610230861, 'Surname', 'First Name', '1996-10-23', 'N')}
+ ${getConfidentiality()}
+ ${specimenSection.join('')}
+ ${analysisSection?.join('')}
+
+`;
+}
+
function getLatestMessage(messages?: Message[]) {
if (!messages?.length) {
return null;
}
return messages.reduce((prev, current) =>
- Number(prev.messageId) > Number(current.messageId) ? prev : current
+ Number(prev.messageId) > Number(current.messageId) ? prev : current,
);
}
diff --git a/lib/templates/medipost-order.ts b/lib/templates/medipost-order.ts
new file mode 100644
index 0000000..9b045e6
--- /dev/null
+++ b/lib/templates/medipost-order.ts
@@ -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 `
+ OL
+ ${sender}
+ ${recipient}
+ ${format(createdAt, DATE_TIME_FORMAT)}
+ ${messageId}
+ argo@medreport.ee
+ `;
+};
+
+export const getClientInstitution = () => {
+ if (isProd) {
+ // return correct data
+ }
+ return `
+ 16381793
+ MedReport OÜ
+ TSU
+ +37258871517
+ `;
+};
+
+export const getProviderInstitution = () => {
+ if (isProd) {
+ // return correct data
+ }
+ return `
+ 11107913
+ Synlab HTI Tallinn
+ SLA
+ Synlab HTI Tallinn
+ +3723417123
+ `;
+};
+
+export const getClientPerson = () => {
+ if (isProd) {
+ // return correct data
+ }
+ return `
+ 1.3.6.1.4.1.28284.6.2.4.9
+ D07907
+ Eduard
+ Tsvetkov
+ +37258131202
+ `;
+};
+
+export const getOrderEnteredPerson = () => {
+ if (isProd) {
+ // return correct data
+ }
+ return `
+ 1.3.6.1.4.1.28284.6.2.4.9
+ D07907
+ Eduard
+ Tsvetkov
+ +37258131202
+ `;
+};
+
+export const getPatient = (
+ idCode: number,
+ surname: string,
+ firstName: string,
+ birthDate: string,
+ genderLetter: string,
+) => {
+ return `
+ 1.3.6.1.4.1.28284.6.2.2.1
+ ${idCode}
+ ${surname}
+ ${firstName}
+ ${birthDate}
+ 1.3.6.1.4.1.28284.6.2.3.16.2
+ ${genderLetter}
+ `;
+};
+
+export const getConfidentiality = () => {
+ if (isProd) {
+ // return correct data
+ }
+ return `
+ 2.16.840.1.113883.5.25
+ N
+ 1.3.6.1.4.1.28284.6.2.2.39.1
+ N
+ 1.3.6.1.4.1.28284.6.2.2.37.1
+ N
+ `;
+};
+
+export const getOrderEnteredByPerson = () => {
+ if (isProd) {
+ // return correct data
+ }
+ return `
+
+ 1.3.6.1.4.1.28284.6.2.4.9
+ D07907
+ Eduard
+ Tsvetkov
+ +37258131202
+ `;
+};
+
+export const getSpecimen = (
+ materialTypeOid: string,
+ materialGroupId: string,
+ materialName: string,
+ order: number,
+ sampleContainerOid?: string,
+ sampleContainerId?: string,
+) =>
+ `
+
+ ${sampleContainerOid ? `${sampleContainerOid}` : null}
+ ${sampleContainerId ? `${sampleContainerId}` : null}
+ ${materialTypeOid}
+ ${materialGroupId}
+ ${materialName}
+ ${order}
+ `;
+
+export const getAnalysisElement = (
+ analysisIdOid: string,
+ analysisIdOriginal: string,
+ tehikShortLoinc: string,
+ tehikLoincName: string,
+ analysisElementId: number,
+ analysisNameLab?: string | null,
+) => {
+ return `
+ ${analysisIdOid}
+ ${analysisIdOriginal}
+ ${tehikShortLoinc}
+ ${tehikLoincName}
+ ${analysisNameLab ?? tehikLoincName}
+ ${analysisElementId}
+ `;
+};
+
+export const getAnalysisGroup = (
+ analysisGroupOriginalId: string,
+ analysisGroupName: string,
+ specimenOrderNr: number,
+ analysisElement: Tables<'analysis_elements'>,
+) =>
+ `
+ ${analysisGroupOriginalId}
+ ${analysisGroupName}
+
+ ${getAnalysisElement(analysisElement.analysis_id_oid, analysisElement.analysis_id_original, analysisElement.tehik_short_loinc, analysisElement.tehik_loinc_name, analysisElement.id, analysisElement.analysis_name_lab)}
+ ${specimenOrderNr}
+
+ `;
diff --git a/lib/types/medipost.ts b/lib/types/medipost.ts
index cdb5931..60d116d 100644
--- a/lib/types/medipost.ts
+++ b/lib/types/medipost.ts
@@ -74,7 +74,7 @@ export type UuringuElement = {
export type Uuring = {
tellitav: "JAH" | "EI";
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 = {
@@ -86,10 +86,10 @@ export type UuringuGrupp = {
};
export type Konteiner = {
- ProovinouKoodOID: string;
- ProovinouKood: string;
- KonteineriNimi: string;
- KonteineriKirjeldus: string;
+ ProovinouKoodOID?: string; //0..1
+ ProovinouKood?: string; //0..1
+ KonteineriNimi?: string; //0..1
+ KonteineriKirjeldus: string; //1..1
};
export type Materjal = {
@@ -98,12 +98,13 @@ export type Materjal = {
MaterjaliNimi: string;
KonteineriOmadus: string;
MaterjaliPaige: { Kohustuslik: "JAH" | "EI" }; //0..1
- Konteiner?: Konteiner[]; //0..n
+ MaterjalJarjekord?: number; //0..1
+ Konteiner?: Konteiner | Konteiner[]; //0..n
};
export type MaterjalideGrupp = {
vaikimisi: "JAH" | "EI";
- Materjal: Materjal; //1..n
+ Materjal: Materjal | Materjal[]; //1..n
};
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;
diff --git a/package.json b/package.json
index 4d8d360..0dbb8ef 100644
--- a/package.json
+++ b/package.json
@@ -61,8 +61,9 @@
"@tanstack/react-table": "^8.21.3",
"axios": "^1.9.0",
"clsx": "^2.1.1",
- "fast-xml-parser": "^5.2.3",
"date-fns": "^4.1.0",
+ "fast-xml-parser": "^5.2.5",
+ "lodash": "^4.17.21",
"lucide-react": "^0.510.0",
"next": "15.3.2",
"next-sitemap": "^4.2.3",
@@ -86,11 +87,12 @@
"@types/node": "^22.15.18",
"@types/react": "19.1.4",
"@types/react-dom": "19.1.5",
- "react-hook-form": "^7.57.0",
+ "@types/lodash": "^4.17.17",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"cssnano": "^7.0.7",
"pino-pretty": "^13.0.0",
"prettier": "^3.5.3",
+ "react-hook-form": "^7.57.0",
"supabase": "^2.22.12",
"tailwindcss": "4.1.7",
"tailwindcss-animate": "^1.0.7",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e84ce96..c1fb37f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -102,8 +102,11 @@ importers:
specifier: ^4.1.0
version: 4.1.0
fast-xml-parser:
- specifier: ^5.2.3
+ specifier: ^5.2.5
version: 5.2.5
+ lodash:
+ specifier: ^4.17.21
+ version: 4.17.21
lucide-react:
specifier: ^0.510.0
version: 0.510.0(react@19.1.0)
@@ -156,6 +159,9 @@ importers:
'@tailwindcss/postcss':
specifier: ^4.1.7
version: 4.1.8
+ '@types/lodash':
+ specifier: ^4.17.17
+ version: 4.17.17
'@types/node':
specifier: ^22.15.18
version: 22.15.30
diff --git a/public/assets/MedReportSmallLogo.tsx b/public/assets/med-report-small-logo.tsx
similarity index 100%
rename from public/assets/MedReportSmallLogo.tsx
rename to public/assets/med-report-small-logo.tsx
diff --git a/supabase/database.types.ts b/supabase/database.types.ts
index bb836be..c432b1d 100644
--- a/supabase/database.types.ts
+++ b/supabase/database.types.ts
@@ -50,24 +50,27 @@ export type Database = {
}
sync_entries: {
Row: {
- changed_by_role: string | null
+ changed_by_role: string
+ comment: string | null
created_at: string
id: number
- operation: string | null
+ operation: string
status: string
}
Insert: {
- changed_by_role?: string | null
+ changed_by_role: string
+ comment?: string | null
created_at?: string
id?: number
- operation?: string | null
+ operation: string
status: string
}
Update: {
- changed_by_role?: string | null
+ changed_by_role?: string
+ comment?: string | null
created_at?: string
id?: number
- operation?: string | null
+ operation?: string
status?: string
}
Relationships: []
@@ -224,7 +227,7 @@ export type Database = {
analysis_name_lab: string | null
created_at: string
id: number
- order: number | null
+ order: number
parent_analysis_element_id: number
tehik_loinc_name: string | null
tehik_short_loinc: string | null
@@ -236,7 +239,7 @@ export type Database = {
analysis_name_lab?: string | null
created_at?: string
id?: number
- order?: number | null
+ order: number
parent_analysis_element_id: number
tehik_loinc_name?: string | null
tehik_short_loinc?: string | null
@@ -248,7 +251,7 @@ export type Database = {
analysis_name_lab?: string | null
created_at?: string
id?: number
- order?: number | null
+ order?: number
parent_analysis_element_id?: number
tehik_loinc_name?: string | null
tehik_short_loinc?: string | null
@@ -266,42 +269,42 @@ export type Database = {
}
analysis_elements: {
Row: {
- analysis_id_oid: string | null
+ analysis_id_oid: string
analysis_id_original: string
analysis_name_lab: string | null
created_at: string
id: number
material_groups: Json[] | null
- order: number | null
+ order: number
parent_analysis_group_id: number
- tehik_loinc_name: string | null
- tehik_short_loinc: string | null
+ tehik_loinc_name: string
+ tehik_short_loinc: string
updated_at: string | null
}
Insert: {
- analysis_id_oid?: string | null
+ analysis_id_oid: string
analysis_id_original: string
analysis_name_lab?: string | null
created_at?: string
id?: number
material_groups?: Json[] | null
- order?: number | null
+ order: number
parent_analysis_group_id: number
- tehik_loinc_name?: string | null
- tehik_short_loinc?: string | null
+ tehik_loinc_name: string
+ tehik_short_loinc: string
updated_at?: string | null
}
Update: {
- analysis_id_oid?: string | null
+ analysis_id_oid?: string
analysis_id_original?: string
analysis_name_lab?: string | null
created_at?: string
id?: number
material_groups?: Json[] | null
- order?: number | null
+ order?: number
parent_analysis_group_id?: number
- tehik_loinc_name?: string | null
- tehik_short_loinc?: string | null
+ tehik_loinc_name?: string
+ tehik_short_loinc?: string
updated_at?: string | null
}
Relationships: [
@@ -318,29 +321,53 @@ export type Database = {
Row: {
created_at: string
id: number
- name: string | null
- order: number | null
+ name: string
+ order: number
original_id: string
updated_at: string | null
}
Insert: {
created_at?: string
id?: number
- name?: string | null
- order?: number | null
+ name: string
+ order: number
original_id: string
updated_at?: string | null
}
Update: {
created_at?: string
id?: number
- name?: string | null
- order?: number | null
+ name?: string
+ order?: number
original_id?: string
updated_at?: string | null
}
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: {
Row: {
account_id: string
@@ -1111,6 +1138,13 @@ export type Database = {
}
}
Enums: {
+ analysis_order_status:
+ | "QUEUED"
+ | "ON_HOLD"
+ | "PROCESSING"
+ | "COMPLETED"
+ | "REJECTED"
+ | "CANCELLED"
app_permissions:
| "roles.manage"
| "billing.manage"
@@ -1257,6 +1291,14 @@ export const Constants = {
},
public: {
Enums: {
+ analysis_order_status: [
+ "QUEUED",
+ "ON_HOLD",
+ "PROCESSING",
+ "COMPLETED",
+ "REJECTED",
+ "CANCELLED",
+ ],
app_permissions: [
"roles.manage",
"billing.manage",
diff --git a/supabase/migrations/20250605150146_add_analysis_orders.sql b/supabase/migrations/20250605150146_add_analysis_orders.sql
new file mode 100644
index 0000000..98870a6
--- /dev/null
+++ b/supabase/migrations/20250605150146_add_analysis_orders.sql
@@ -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;
+
+