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; + +