diff --git a/.env.example b/.env.example
index 2cd4fdc..b35abfa 100644
--- a/.env.example
+++ b/.env.example
@@ -2,6 +2,7 @@
# https://app.supabase.com/project/_/settings/api
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
diff --git a/app/(marketing)/page.tsx b/app/(marketing)/page.tsx
index ac7b2ed..8826673 100644
--- a/app/(marketing)/page.tsx
+++ b/app/(marketing)/page.tsx
@@ -9,20 +9,17 @@ import {
import { Trans } from '@kit/ui/trans';
import { withI18n } from '~/lib/i18n/with-i18n';
+import { MedReportTitle } from '@/components/MedReportTitle';
function Home() {
return (
- Med Report
- >
- }
+ title={}
subtitle={
- Lihtne, mugav ja kiire ülevaade Sinu tervise seisundist
+
}
cta={}
@@ -56,8 +53,8 @@ function MainCallToActionButton() {
-
-
+
+
diff --git a/app/(public)/layout.tsx b/app/(public)/layout.tsx
new file mode 100644
index 0000000..b6f3825
--- /dev/null
+++ b/app/(public)/layout.tsx
@@ -0,0 +1,11 @@
+import { withI18n } from '~/lib/i18n/with-i18n';
+
+function SiteLayout(props: React.PropsWithChildren) {
+ return (
+
+ {props.children}
+
+ );
+}
+
+export default withI18n(SiteLayout);
diff --git a/app/(public)/register-company/page.tsx b/app/(public)/register-company/page.tsx
new file mode 100644
index 0000000..e3e54e7
--- /dev/null
+++ b/app/(public)/register-company/page.tsx
@@ -0,0 +1,90 @@
+"use client";
+
+import { MedReportTitle } from "@/components/MedReportTitle";
+import React from "react";
+import { yupResolver } from "@hookform/resolvers/yup";
+import { useForm } from "react-hook-form";
+import { companySchema } from "@/lib/validations/companySchema";
+import { CompanySubmitData } from "@/lib/types/company";
+import { submitCompanyRegistration } from "@/lib/services/register-company.service";
+import { useRouter } from "next/navigation";
+import { Label } from "@kit/ui/label";
+import { Input } from "@kit/ui/input";
+import { SubmitButton } from "@/components/ui/submit-button";
+import { FormItem } from "@kit/ui/form";
+import { Trans } from "@kit/ui/trans";
+
+export default function RegisterCompany() {
+ const router = useRouter();
+ const {
+ register,
+ handleSubmit,
+ formState: { errors, isValid, isSubmitting },
+ } = useForm({
+ resolver: yupResolver(companySchema),
+ mode: "onChange",
+ });
+
+ async function onSubmit(data: CompanySubmitData) {
+ const formData = new FormData();
+ Object.entries(data).forEach(([key, value]) => {
+ if (value !== undefined) formData.append(key, value);
+ });
+
+ try {
+ await submitCompanyRegistration(formData);
+ router.push("/register-company/success");
+ } catch (err: unknown) {
+ if (err instanceof Error) {
+ alert("Server validation error: " + err.message);
+ }
+ alert("Server validation error");
+ }
+ }
+
+ return (
+
+
+
+
Ettevõtte andmed
+
+ Pakkumise saamiseks palun sisesta ettevõtte andmed millega MedReport
+ kasutada kavatsed.
+
+
+
+
+
+
+ );
+}
diff --git a/app/(public)/register-company/success/page.tsx b/app/(public)/register-company/success/page.tsx
new file mode 100644
index 0000000..688dbb0
--- /dev/null
+++ b/app/(public)/register-company/success/page.tsx
@@ -0,0 +1,26 @@
+import { MedReportTitle } from "@/components/MedReportTitle";
+import { Button } from "@/packages/ui/src/shadcn/button";
+import Image from "next/image";
+import Link from "next/link";
+
+export default function CompanyRegistrationSuccess() {
+ return (
+
+
+
+
+
Päring edukalt saadetud!
+
Saadame teile esimesel võimalusel vastuse
+
+
+
+ );
+}
diff --git a/app/(public)/sign-in/page.tsx b/app/(public)/sign-in/page.tsx
new file mode 100644
index 0000000..aca9ead
--- /dev/null
+++ b/app/(public)/sign-in/page.tsx
@@ -0,0 +1,22 @@
+import { Button } from '@kit/ui/button';
+import Link from "next/link";
+import React from "react";
+
+export default async function SignIn() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/app/icon.ico b/app/icon.ico
new file mode 100644
index 0000000..ad7ea1f
Binary files /dev/null and b/app/icon.ico differ
diff --git a/components/MedReportTitle.tsx b/components/MedReportTitle.tsx
new file mode 100644
index 0000000..a010e11
--- /dev/null
+++ b/components/MedReportTitle.tsx
@@ -0,0 +1,10 @@
+import { MedReportSmallLogo } from "@/public/assets/MedReportSmallLogo";
+
+export const MedReportTitle = () => (
+
+
+
+ MedReport
+
+
+);
diff --git a/components/header-auth.tsx b/components/header-auth.tsx
new file mode 100644
index 0000000..aab2ec7
--- /dev/null
+++ b/components/header-auth.tsx
@@ -0,0 +1,67 @@
+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 { createClient } from "@/utils/supabase/server";
+
+export default async function AuthButton() {
+ const supabase = await createClient();
+
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+
+ if (!hasEnvVars) {
+ return (
+ <>
+
+
+
+ Please update .env.local file with anon key and url
+
+
+
+
+
+
+
+ >
+ );
+ }
+ return user ? (
+
+ Hey, {user.email}!
+
+
+ ) : (
+
+
+
+ );
+}
diff --git a/components/ui/submit-button.tsx b/components/ui/submit-button.tsx
new file mode 100644
index 0000000..93cf1a1
--- /dev/null
+++ b/components/ui/submit-button.tsx
@@ -0,0 +1,23 @@
+"use client";
+
+import { Button } from "@kit/ui/button";
+import { type ComponentProps } from "react";
+import { useFormStatus } from "react-dom";
+
+type Props = ComponentProps
& {
+ pendingText?: string;
+};
+
+export function SubmitButton({
+ children,
+ pendingText = "Submitting...",
+ ...props
+}: Props) {
+ const { pending } = useFormStatus();
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/fonts/InterDisplay-Medium.woff2 b/fonts/InterDisplay-Medium.woff2
new file mode 100644
index 0000000..f6157fa
Binary files /dev/null and b/fonts/InterDisplay-Medium.woff2 differ
diff --git a/fonts/InterDisplay-Regular.woff2 b/fonts/InterDisplay-Regular.woff2
new file mode 100644
index 0000000..b5a45e8
Binary files /dev/null and b/fonts/InterDisplay-Regular.woff2 differ
diff --git a/lib/actions/sign-out.tsx b/lib/actions/sign-out.tsx
new file mode 100644
index 0000000..0137a93
--- /dev/null
+++ b/lib/actions/sign-out.tsx
@@ -0,0 +1,10 @@
+"use server";
+
+import { createClient } from "@/utils/supabase/server";
+import { redirect } from "next/navigation";
+
+export const signOutAction = async () => {
+ const supabase = await createClient();
+ await supabase.auth.signOut();
+ return redirect("/sign-in");
+};
diff --git a/lib/fonts.ts b/lib/fonts.ts
index bdf7195..a9ec3df 100644
--- a/lib/fonts.ts
+++ b/lib/fonts.ts
@@ -1,4 +1,5 @@
-import { Inter as SansFont } from 'next/font/google';
+import { Geist as HeadingFont } from 'next/font/google';
+import SansFont from 'next/font/local';
import { cn } from '@kit/ui/utils';
@@ -8,18 +9,32 @@ import { cn } from '@kit/ui/utils';
* By default, it uses the Inter font from Google Fonts.
*/
const sans = SansFont({
- subsets: ['latin'],
variable: '--font-sans',
- fallback: ['system-ui', 'Helvetica Neue', 'Helvetica', 'Arial'],
- preload: true,
- weight: ['300', '400', '500', '600', '700'],
+ src: [
+ {
+ path: '../fonts/InterDisplay-Regular.woff2',
+ weight: '400',
+ style: 'normal',
+ },
+ {
+ path: '../fonts/InterDisplay-Medium.woff2',
+ weight: '500',
+ style: 'medium',
+ },
+ ],
+
});
/**
* @heading
* @description Define here the heading font.
*/
-const heading = sans;
+const heading = HeadingFont({
+ variable: '--font-heading',
+ fallback: ['system-ui', 'Helvetica Neue', 'Helvetica', 'Arial'],
+ preload: true,
+ weight: ['300', '400', '500', '600', '700'],
+});
// we export these fonts into the root layout
export { sans, heading };
diff --git a/lib/i18n/i18n.settings.ts b/lib/i18n/i18n.settings.ts
index 4be9cf3..bb70925 100644
--- a/lib/i18n/i18n.settings.ts
+++ b/lib/i18n/i18n.settings.ts
@@ -12,7 +12,7 @@ const defaultLanguage = process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? 'en';
* By default, only the default language is supported.
* Add more languages here if needed.
*/
-export const languages: string[] = [defaultLanguage];
+export const languages: string[] = [defaultLanguage, 'en', 'ru'];
/**
* The name of the cookie that stores the selected language.
diff --git a/lib/services/medipost.service.ts b/lib/services/medipost.service.ts
index 703241f..f6f1b9c 100644
--- a/lib/services/medipost.service.ts
+++ b/lib/services/medipost.service.ts
@@ -1,10 +1,16 @@
import {
GetMessageListResponse,
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 { xml2json } from "xml-js";
+import { XMLParser } from "fast-xml-parser";
+import { SyncStatus } from "@/lib/types/audit";
+import { toArray } from "@/lib/utils";
const BASE_URL = process.env.MEDIPOST_URL!;
const USER = process.env.MEDIPOST_USER!;
@@ -15,9 +21,10 @@ export async function getMessages() {
const publicMessage = await getLatestPublicMessageListItem();
if (!publicMessage) {
- return [];
+ return null;
}
+ //Teenused tuleb mappida kokku MedReport teenustega. alusel
return getPublicMessage(publicMessage.messageId);
} catch (error) {
console.error(error);
@@ -55,18 +62,13 @@ export async function getPublicMessage(messageId: string) {
Accept: "application/xml",
},
});
+ const parser = new XMLParser({ ignoreAttributes: false });
+ const parsed: MedipostPublicMessageResponse = parser.parse(data);
- if (data.code && data.code !== 0) {
+ if (parsed.ANSWER?.CODE && parsed.ANSWER?.CODE !== 0) {
throw new Error(`Failed to get public message (id: ${messageId})`);
}
- const parsed = JSON.parse(
- xml2json(data, {
- compact: true,
- spaces: 2,
- })
- );
-
return parsed;
}
@@ -124,14 +126,8 @@ export async function getPrivateMessage(messageId: string) {
throw new Error(`Failed to get private message (id: ${messageId})`);
}
- const parsed = JSON.parse(
- xml2json(data, {
- compact: true,
- spaces: 2,
- })
- );
-
- return parsed;
+ const parser = new XMLParser({ ignoreAttributes: false });
+ return parser.parse(data);
}
export async function deletePrivateMessage(messageId: string) {
@@ -170,6 +166,187 @@ export async function readPrivateMessageResponse() {
}
}
+async function saveAnalysisGroup(
+ analysisGroup: UuringuGrupp,
+ supabase: SupabaseClient
+) {
+ const { data: insertedAnalysisGroup, error } = await supabase
+ .from("analysis_groups")
+ .upsert(
+ {
+ original_id: analysisGroup.UuringuGruppId,
+ name: analysisGroup.UuringuGruppNimi,
+ order: analysisGroup.UuringuGruppJarjekord,
+ },
+ { onConflict: "original_id", ignoreDuplicates: false }
+ )
+ .select("id");
+
+ if (error || !insertedAnalysisGroup[0]?.id) {
+ throw new Error(
+ `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) => ({
+ hk_code: kood.HkKood,
+ hk_code_multiplier: kood.HkKoodiKordaja,
+ coefficient: kood.Koefitsient,
+ price: kood.Hind,
+ analysis_group_id: analysisGroupId,
+ }));
+
+ const analysisGroupItems = toArray(analysisGroup.Uuring);
+
+ for (const item of analysisGroupItems) {
+ const analysisElement = item.UuringuElement;
+
+ const { data: insertedAnalysisElement, error } = await supabase
+ .from("analysis_elements")
+ .upsert(
+ {
+ analysis_id_oid: analysisElement.UuringIdOID,
+ analysis_id_original: analysisElement.UuringId,
+ tehik_short_loinc: analysisElement.TLyhend,
+ tehik_loinc_name: analysisElement.KNimetus,
+ analysis_name_lab: analysisElement.UuringNimi,
+ order: analysisElement.Jarjekord,
+ parent_analysis_group_id: analysisGroupId,
+ material_groups: toArray(item.MaterjalideGrupp),
+ },
+ { 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}`
+ );
+ }
+
+ const insertedAnalysisElementId = insertedAnalysisElement[0].id;
+
+ if (analysisElement.Kood) {
+ const analysisElementCodes = toArray(analysisElement.Kood);
+ codes.push(
+ ...analysisElementCodes.map((kood) => ({
+ hk_code: kood.HkKood,
+ hk_code_multiplier: kood.HkKoodiKordaja,
+ coefficient: kood.Koefitsient,
+ price: kood.Hind,
+ analysis_element_id: insertedAnalysisElementId,
+ }))
+ );
+ }
+
+ const analyses = analysisElement.UuringuElement;
+ if (analyses?.length) {
+ for (const analysis of analyses) {
+ const { data: insertedAnalysis, error } = await supabase
+ .from("analyses")
+ .upsert(
+ {
+ analysis_id_oid: analysis.UuringIdOID,
+ analysis_id_original: analysis.UuringId,
+ tehik_short_loinc: analysis.TLyhend,
+ tehik_loinc_name: analysis.KNimetus,
+ analysis_name_lab: analysis.UuringNimi,
+ order: analysis.Jarjekord,
+ parent_analysis_element_id: insertedAnalysisElementId,
+ },
+ { 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}`
+ );
+ }
+
+ const insertedAnalysisId = insertedAnalysis[0].id;
+ if (analysisElement.Kood) {
+ const analysisCodes = toArray(analysis.Kood);
+
+ codes.push(
+ ...analysisCodes.map((kood) => ({
+ hk_code: kood.HkKood,
+ hk_code_multiplier: kood.HkKoodiKordaja,
+ coefficient: kood.Koefitsient,
+ price: kood.Hind,
+ analysis_id: insertedAnalysisId,
+ }))
+ );
+ }
+ }
+ }
+ }
+
+ const { error: codesError } = await supabase
+ .from("codes")
+ .upsert(codes, { ignoreDuplicates: false });
+
+ if (codesError?.code) {
+ throw new Error(
+ `Failed to insert codes (analysis group id: ${analysisGroup.UuringuGruppId})`
+ );
+ }
+}
+
+export async function syncPublicMessage(
+ message?: MedipostPublicMessageResponse | null
+) {
+ const supabase = createClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY!,
+ {
+ auth: {
+ persistSession: false,
+ autoRefreshToken: false,
+ detectSessionInUrl: false,
+ },
+ }
+ );
+
+ try {
+ const providers = toArray(message?.Saadetis?.Teenused.Teostaja);
+ const analysisGroups = providers.flatMap((provider) =>
+ toArray(provider.UuringuGrupp)
+ );
+ if (!message || !analysisGroups.length) {
+ return supabase.schema("audit").from("sync_entries").insert({
+ operation: "ANALYSES_SYNC",
+ comment: "No data received",
+ status: SyncStatus.Fail,
+ changed_by_role: "service_role",
+ });
+ }
+
+ for (const analysisGroup of analysisGroups) {
+ await saveAnalysisGroup(analysisGroup, supabase);
+ }
+
+ await supabase.schema("audit").from("sync_entries").insert({
+ operation: "ANALYSES_SYNC",
+ status: SyncStatus.Success,
+ changed_by_role: "service_role",
+ });
+ } catch (e) {
+ console.error(e);
+ await supabase
+ .schema("audit")
+ .from("sync_entries")
+ .insert({
+ operation: "ANALYSES_SYNC",
+ status: SyncStatus.Fail,
+ comment: JSON.stringify(e),
+ changed_by_role: "service_role",
+ });
+ }
+}
+
function getLatestMessage(messages?: Message[]) {
if (!messages?.length) {
return null;
diff --git a/lib/services/register-company.service.ts b/lib/services/register-company.service.ts
new file mode 100644
index 0000000..c030462
--- /dev/null
+++ b/lib/services/register-company.service.ts
@@ -0,0 +1,31 @@
+"use server";
+
+import * as yup from "yup";
+import { companySchema } from "@/lib/validations/companySchema";
+
+export async function submitCompanyRegistration(formData: FormData) {
+ const data = {
+ companyName: formData.get("companyName")?.toString() || "",
+ contactPerson: formData.get("contactPerson")?.toString() || "",
+ email: formData.get("email")?.toString() || "",
+ phone: formData.get("phone")?.toString() || "",
+ };
+
+ try {
+ await companySchema.validate(data, { abortEarly: false });
+
+ console.log("Valid data:", data);
+ } catch (validationError) {
+ if (validationError instanceof yup.ValidationError) {
+ const errors = validationError.inner.map((err) => ({
+ path: err.path,
+ message: err.message,
+ }));
+ throw new Error(
+ "Validation failed: " +
+ errors.map((e) => `${e.path}: ${e.message}`).join(", ")
+ );
+ }
+ throw validationError;
+ }
+}
diff --git a/lib/types/audit.ts b/lib/types/audit.ts
new file mode 100644
index 0000000..f8eed24
--- /dev/null
+++ b/lib/types/audit.ts
@@ -0,0 +1,4 @@
+export enum SyncStatus {
+ Success = "SUCCESS",
+ Fail = "FAIL",
+}
diff --git a/lib/types/company.ts b/lib/types/company.ts
new file mode 100644
index 0000000..7fbc4d6
--- /dev/null
+++ b/lib/types/company.ts
@@ -0,0 +1,6 @@
+export interface CompanySubmitData {
+ companyName: string;
+ contactPerson: string;
+ email: string;
+ phone?: string;
+}
diff --git a/lib/types/medipost.ts b/lib/types/medipost.ts
index 7f94da1..cdb5931 100644
--- a/lib/types/medipost.ts
+++ b/lib/types/medipost.ts
@@ -20,3 +20,122 @@ export enum MedipostAction {
GetPrivateMessage = "GetPrivateMessage",
DeletePrivateMessage = "DeletePrivateMessage",
}
+
+export type VoimalikVaartus = {
+ VaartusId: string;
+ Vaartus: "jah" | "ei";
+ VaartusJarjekord: number;
+};
+
+export type Sisendparameeter = {
+ "@_VastuseTyyp"?: "ARV" | "VABATEKST" | "KODEERITUD" | "AJAHETK";
+ "@_VastuseKoodistikuOID"?: string;
+ "@_VastuseKoodistikuNimi"?: string;
+ "@_URL"?: string;
+
+ UuringIdOID: string;
+ UuringId: string;
+ TLyhend: string;
+ KNimetus: string;
+ UuringNimi: string;
+ Jarjekord: number;
+
+ VoimalikVaartus: VoimalikVaartus[];
+};
+
+export type Kood = {
+ HkKood: string;
+ HkKoodiKordaja: number;
+ Koefitsient: number; // float
+ Hind: number; // float
+};
+
+export type UuringuAlamElement = {
+ UuringIdOID: string;
+ UuringId: string;
+ TLyhend: string;
+ KNimetus: string;
+ UuringNimi: string;
+ Jarjekord: string;
+ Kood?: Kood[];
+};
+
+export type UuringuElement = {
+ UuringIdOID: string;
+ UuringId: string;
+ TLyhend: string;
+ KNimetus: string;
+ UuringNimi: string;
+ Jarjekord: string;
+ Kood?: Kood[];
+ UuringuElement?: UuringuAlamElement[];
+};
+
+export type Uuring = {
+ tellitav: "JAH" | "EI";
+ UuringuElement: UuringuElement; //1..1
+ MaterjalideGrupp?: MaterjalideGrupp[]; //0..n
+};
+
+export type UuringuGrupp = {
+ UuringuGruppId: string;
+ UuringuGruppNimi: string;
+ UuringuGruppJarjekord: number;
+ Uuring: Uuring | Uuring[]; //1..n
+ Kood?: Kood | Kood[]; //0..n
+};
+
+export type Konteiner = {
+ ProovinouKoodOID: string;
+ ProovinouKood: string;
+ KonteineriNimi: string;
+ KonteineriKirjeldus: string;
+};
+
+export type Materjal = {
+ MaterjaliTyypOID: string;
+ MaterjaliTyyp: string;
+ MaterjaliNimi: string;
+ KonteineriOmadus: string;
+ MaterjaliPaige: { Kohustuslik: "JAH" | "EI" }; //0..1
+ Konteiner?: Konteiner[]; //0..n
+};
+
+export type MaterjalideGrupp = {
+ vaikimisi: "JAH" | "EI";
+ Materjal: Materjal; //1..n
+};
+
+export type Teostaja = {
+ UuringuGrupp?: UuringuGrupp | UuringuGrupp[]; //0...n
+ Asutus: {
+ AsutuseId: string;
+ AsutuseNimi: string;
+ AsutuseKood: string;
+ AllyksuseNimi: string;
+ Telefon: string;
+ Aadress: string;
+ };
+ Sisendparameeter?: Sisendparameeter | Sisendparameeter[]; //0...n
+};
+
+export type MedipostPublicMessageResponse = {
+ "?xml": {
+ "@_version": string;
+ "@_encoding": "UTF-8";
+ "@_standalone"?: "yes" | "no";
+ };
+ ANSWER?: { CODE: number };
+ Saadetis?: {
+ Pais: {
+ Pakett: { "#text": "SL" | "OL" | "AL" | "ME" }; // SL - Teenused, OL - Tellimus (meie poolt saadetav saatekiri), AL - Vastus (saatekirja vastus), ME - Teade
+ Saatja: string;
+ Saaja: string;
+ Aeg: string;
+ SaadetisId: string;
+ };
+ Teenused: {
+ Teostaja: Teostaja | Teostaja[]; //1..n
+ };
+ };
+};
diff --git a/lib/utils.ts b/lib/utils.ts
index a5ef193..405dd01 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -4,3 +4,8 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+
+export function toArray(input?: T | T[] | null): T[] {
+ if (!input) return [];
+ return Array.isArray(input) ? input : [input];
+}
diff --git a/lib/validations/companySchema.ts b/lib/validations/companySchema.ts
new file mode 100644
index 0000000..b265f81
--- /dev/null
+++ b/lib/validations/companySchema.ts
@@ -0,0 +1,8 @@
+import * as yup from "yup";
+
+export const companySchema = yup.object({
+ companyName: yup.string().required("Company name is required"),
+ contactPerson: yup.string().required("Contact person is required"),
+ email: yup.string().email("Invalid email").required("Email is required"),
+ phone: yup.string().optional(),
+});
diff --git a/package.json b/package.json
index 43a3321..82a583a 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,7 @@
"@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",
"lucide-react": "^0.510.0",
"next": "15.3.2",
@@ -73,10 +74,10 @@
"recharts": "2.15.3",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0",
- "xml-js": "^1.6.11",
"zod": "^3.24.4"
},
"devDependencies": {
+ "@hookform/resolvers": "^5.0.1",
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
@@ -85,6 +86,7 @@
"@types/node": "^22.15.18",
"@types/react": "19.1.4",
"@types/react-dom": "19.1.5",
+ "react-hook-form": "^7.57.0",
"babel-plugin-react-compiler": "19.1.0-rc.2",
"cssnano": "^7.0.7",
"pino-pretty": "^13.0.0",
@@ -92,7 +94,8 @@
"supabase": "^2.22.12",
"tailwindcss": "4.1.7",
"tailwindcss-animate": "^1.0.7",
- "typescript": "^5.8.3"
+ "typescript": "^5.8.3",
+ "yup": "^1.6.1"
},
"prettier": "@kit/prettier-config",
"browserslist": [
@@ -100,4 +103,4 @@
"> 0.7%",
"not dead"
]
-}
+}
\ No newline at end of file
diff --git a/packages/ui/src/makerkit/marketing/header.tsx b/packages/ui/src/makerkit/marketing/header.tsx
index fcba508..1489ef2 100644
--- a/packages/ui/src/makerkit/marketing/header.tsx
+++ b/packages/ui/src/makerkit/marketing/header.tsx
@@ -1,5 +1,5 @@
import { cn } from '../../lib/utils';
-
+import { LanguageSelector } from '@kit/ui/language-selector';
interface HeaderProps extends React.HTMLAttributes {
logo?: React.ReactNode;
navigation?: React.ReactNode;
@@ -25,7 +25,8 @@ export const Header: React.FC = function ({
{logo}
{navigation}
-
{actions}
+
+
diff --git a/packages/ui/src/shadcn/button.tsx b/packages/ui/src/shadcn/button.tsx
index 85ee15c..6210a54 100644
--- a/packages/ui/src/shadcn/button.tsx
+++ b/packages/ui/src/shadcn/button.tsx
@@ -12,7 +12,7 @@ const buttonVariants = cva(
variants: {
variant: {
default:
- 'bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs',
+ 'bg-primary text-primary-foreground font-medium hover:bg-primary/90 shadow-xs',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-xs',
outline:
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4cc46d5..e84ce96 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -101,6 +101,9 @@ importers:
date-fns:
specifier: ^4.1.0
version: 4.1.0
+ fast-xml-parser:
+ specifier: ^5.2.3
+ version: 5.2.5
lucide-react:
specifier: ^0.510.0
version: 0.510.0(react@19.1.0)
@@ -134,9 +137,6 @@ importers:
tailwind-merge:
specifier: ^3.3.0
version: 3.3.0
- xml-js:
- specifier: ^1.6.11
- version: 1.6.11
zod:
specifier: ^3.24.4
version: 3.25.56
@@ -189,6 +189,9 @@ importers:
typescript:
specifier: ^5.8.3
version: 5.8.3
+ yup:
+ specifier: ^1.6.1
+ version: 1.6.1
packages/analytics:
devDependencies:
@@ -5362,6 +5365,10 @@ packages:
fast-uri@3.0.6:
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
+ fast-xml-parser@5.2.5:
+ resolution: {integrity: sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==}
+ hasBin: true
+
fastq@1.19.1:
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
@@ -6727,6 +6734,9 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+ property-expr@2.0.6:
+ resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==}
+
prosemirror-commands@1.7.1:
resolution: {integrity: sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==}
@@ -6976,9 +6986,6 @@ packages:
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
engines: {node: '>=10'}
- sax@1.4.1:
- resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
-
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
@@ -7176,6 +7183,9 @@ packages:
'@types/node':
optional: true
+ strnum@2.1.1:
+ resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==}
+
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
@@ -7273,6 +7283,9 @@ packages:
thread-stream@3.1.0:
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
+ tiny-case@1.0.3:
+ resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
+
tiny-invariant@1.0.6:
resolution: {integrity: sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA==}
@@ -7290,6 +7303,9 @@ packages:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
+ toposort@2.0.2:
+ resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==}
+
totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
@@ -7354,6 +7370,10 @@ packages:
resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==}
engines: {node: '>=8'}
+ type-fest@2.19.0:
+ resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
+ engines: {node: '>=12.20'}
+
typed-array-buffer@1.0.3:
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
engines: {node: '>= 0.4'}
@@ -7579,10 +7599,6 @@ packages:
utf-8-validate:
optional: true
- xml-js@1.6.11:
- resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
- hasBin: true
-
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
@@ -7639,6 +7655,9 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ yup@1.6.1:
+ resolution: {integrity: sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==}
+
zod@3.25.56:
resolution: {integrity: sha512-rd6eEF3BTNvQnR2e2wwolfTmUTnp70aUTqr0oaGbHifzC3BKJsoV+Gat8vxUMR1hwOKBs6El+qWehrHbCpW6SQ==}
@@ -12494,6 +12513,10 @@ snapshots:
fast-uri@3.0.6: {}
+ fast-xml-parser@5.2.5:
+ dependencies:
+ strnum: 2.1.1
+
fastq@1.19.1:
dependencies:
reusify: 1.1.0
@@ -13960,6 +13983,8 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
+ property-expr@2.0.6: {}
+
prosemirror-commands@1.7.1:
dependencies:
prosemirror-model: 1.25.1
@@ -14266,8 +14291,6 @@ snapshots:
safe-stable-stringify@2.5.0: {}
- sax@1.4.1: {}
-
scheduler@0.26.0: {}
schema-utils@4.3.2:
@@ -14537,6 +14560,8 @@ snapshots:
optionalDependencies:
'@types/node': 22.15.30
+ strnum@2.1.1: {}
+
styled-jsx@5.1.6(@babel/core@7.27.4)(react@19.1.0):
dependencies:
client-only: 0.0.1
@@ -14626,6 +14651,8 @@ snapshots:
dependencies:
real-require: 0.2.0
+ tiny-case@1.0.3: {}
+
tiny-invariant@1.0.6: {}
tiny-invariant@1.3.3: {}
@@ -14641,6 +14668,8 @@ snapshots:
dependencies:
is-number: 7.0.0
+ toposort@2.0.2: {}
+
totalist@3.0.1: {}
tr46@0.0.3: {}
@@ -14693,6 +14722,8 @@ snapshots:
type-fest@0.7.1: {}
+ type-fest@2.19.0: {}
+
typed-array-buffer@1.0.3:
dependencies:
call-bound: 1.0.4
@@ -15006,10 +15037,6 @@ snapshots:
ws@8.18.2: {}
- xml-js@1.6.11:
- dependencies:
- sax: 1.4.1
-
xtend@4.0.2: {}
y-prosemirror@1.3.5(prosemirror-model@1.25.1)(prosemirror-state@1.4.3)(prosemirror-view@1.40.0)(y-protocols@1.0.6(yjs@13.6.27))(yjs@13.6.27):
@@ -15056,6 +15083,13 @@ snapshots:
yocto-queue@0.1.0: {}
+ yup@1.6.1:
+ dependencies:
+ property-expr: 2.0.6
+ tiny-case: 1.0.3
+ toposort: 2.0.2
+ type-fest: 2.19.0
+
zod@3.25.56: {}
zwitch@2.0.4: {}
diff --git a/public/assets/MedReportSmallLogo.tsx b/public/assets/MedReportSmallLogo.tsx
new file mode 100644
index 0000000..8d5ec33
--- /dev/null
+++ b/public/assets/MedReportSmallLogo.tsx
@@ -0,0 +1,16 @@
+export const MedReportSmallLogo = () => {
+ return (
+