Merge pull request #46 from MR-medreport/MED-91

Med 91
This commit is contained in:
danelkungla
2025-08-12 15:51:06 +03:00
committed by GitHub
22 changed files with 365 additions and 392 deletions

View File

@@ -106,9 +106,11 @@ To access admin pages follow these steps:
1. Customer adds analysis to cart in **B2B** storefront
2. Customer checks out from cart and is redirected to **Montonio**
3. Customer pays and is redirected back to **B2B** `GET B2B/home/cart/montonio-callback?order-token=$JWT`
- **Medusa** order is created and cart is emptied
- email is sent to customer
- B2B sends order XML as private message to Medipost.
When **Montonio** has confirmed payment, it will call **Medusa** webhook endpoint and **Medusa** will mark order payment as captured.
In background a job will call `POST B2B/api/job/sync-analysis-results` every n minutes and sync private messages with responses from **Medipost**.

View File

@@ -8,8 +8,6 @@ import { ExternalLink } from '@/public/assets/external-link';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { onUpdateAccount } from '@kit/auth/actions/update-account-actions';
import { UpdateAccountSchema } from '@kit/auth/schemas/update-account.schema';
import { Button } from '@kit/ui/button';
import { Checkbox } from '@kit/ui/checkbox';
import {
@@ -23,6 +21,9 @@ import {
import { Input } from '@kit/ui/input';
import { Trans } from '@kit/ui/trans';
import { UpdateAccountSchema } from '../_lib/schemas/update-account.schema';
import { onUpdateAccount } from '../_lib/server/update-account';
export function UpdateAccountForm({ user }: { user: User }) {
const form = useForm({
resolver: zodResolver(UpdateAccountSchema),
@@ -30,16 +31,15 @@ export function UpdateAccountForm({ user }: { user: User }) {
defaultValues: {
firstName: '',
lastName: '',
personalCode: user.user_metadata.personalCode ?? '',
personalCode: '',
email: user.email,
phone: '',
city: '',
weight: user.user_metadata.weight ?? undefined,
height: user.user_metadata.height ?? undefined,
weight: 0,
height: 0,
userConsent: false,
},
});
return (
<Form {...form}>
<form

View File

@@ -4,25 +4,13 @@ import { redirect } from 'next/navigation';
import { updateCustomer } from '@lib/data/customer';
import { createAuthApi } from '@kit/auth/api';
import { AccountSubmitData, createAuthApi } from '@kit/auth/api';
import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import pathsConfig from '~/config/paths.config';
import { UpdateAccountSchema } from '../../schemas/update-account.schema';
export interface AccountSubmitData {
firstName: string;
lastName: string;
personalCode: string;
email: string;
phone?: string;
city?: string;
weight: number | null;
height: number | null;
userConsent: boolean;
}
import { UpdateAccountSchema } from '../schemas/update-account.schema';
export const onUpdateAccount = enhanceAction(
async (params: AccountSubmitData) => {

View File

@@ -42,7 +42,7 @@ const Level = ({
export const AnalysisLevelBarSkeleton = () => {
return (
<div className="mt-4 flex h-3 max-w-[360px] w-[35%] gap-1 sm:mt-0">
<div className="mt-4 flex h-3 w-[35%] max-w-[360px] gap-1 sm:mt-0">
<Level color="gray-200" />
</div>
);
@@ -58,7 +58,7 @@ const AnalysisLevelBar = ({
level: AnalysisResultLevel;
}) => {
return (
<div className="mt-4 flex h-3 max-w-[360px] w-[35%] gap-1 sm:mt-0">
<div className="mt-4 flex h-3 w-[35%] max-w-[360px] gap-1 sm:mt-0">
{normLowerIncluded && (
<>
<Level

View File

@@ -1,16 +1,20 @@
'use client';
import React, { useState } from 'react';
import { format } from 'date-fns';
import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
import { format } from 'date-fns';
import { Info } from 'lucide-react';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import AnalysisLevelBar, { AnalysisLevelBarSkeleton, AnalysisResultLevel } from './analysis-level-bar';
import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
import { AnalysisElement } from '~/lib/services/analysis-element.service';
import { Trans } from '@kit/ui/trans';
import AnalysisLevelBar, {
AnalysisLevelBarSkeleton,
AnalysisResultLevel,
} from './analysis-level-bar';
export enum AnalysisStatus {
NORMAL = 0,
@@ -59,7 +63,7 @@ const Analysis = ({
};
return (
<div className="border-border items-center justify-between rounded-lg border px-5 py-3 sm:h-[65px] flex flex-col sm:flex-row px-12 gap-2 sm:gap-0">
<div className="border-border flex flex-col items-center justify-between gap-2 rounded-lg border px-5 px-12 py-3 sm:h-[65px] sm:flex-row sm:gap-0">
<div className="flex items-center gap-2 font-semibold">
{name}
{results?.response_time && (
@@ -75,7 +79,9 @@ const Analysis = ({
{ block: showTooltip },
)}
>
<Trans i18nKey="analysis-results:analysisDate" />{': '}{format(new Date(results.response_time), 'dd.MM.yyyy HH:mm')}
<Trans i18nKey="analysis-results:analysisDate" />
{': '}
{format(new Date(results.response_time), 'dd.MM.yyyy HH:mm')}
</div>
</div>
)}
@@ -86,7 +92,7 @@ const Analysis = ({
<div className="font-semibold">{value}</div>
<div className="text-muted-foreground text-sm">{unit}</div>
</div>
<div className="text-muted-foreground flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0 mx-8">
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
{normLower} - {normUpper}
<div>
<Trans i18nKey="analysis-results:results.range.normal" />
@@ -105,7 +111,7 @@ const Analysis = ({
<Trans i18nKey="analysis-results:waitingForResults" />
</div>
</div>
<div className="w-[60px] mx-8"></div>
<div className="mx-8 w-[60px]"></div>
<AnalysisLevelBarSkeleton />
</>
)}

View File

@@ -3,7 +3,9 @@
import Link from 'next/link';
import { InfoTooltip } from '@/components/ui/info-tooltip';
import type { AccountWithParams } from '@/packages/features/accounts/src/server/api';
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
import Isikukood from 'isikukood';
import {
Activity,
ChevronRight,
@@ -15,7 +17,6 @@ import {
TrendingUp,
User,
} from 'lucide-react';
import Isikukood from 'isikukood';
import { Button } from '@kit/ui/button';
import {
@@ -28,7 +29,6 @@ import {
} from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import type { AccountWithParams } from '@/packages/features/accounts/src/server/api';
const cards = ({
gender,

View File

@@ -1,5 +1,7 @@
'use client';
import { useState } from 'react';
import { UpdateHealthBenefitSchema } from '@/packages/billing/core/src/schema';
import { Database } from '@/packages/supabase/src/database.types';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -22,32 +24,52 @@ import YearlyExpensesOverview from './yearly-expenses-overview';
const HealthBenefitForm = ({
account,
companyParams,
employeeCount,
}: {
account: Database['medreport']['Tables']['accounts']['Row'];
companyParams: Database['medreport']['Tables']['company_params']['Row'];
employeeCount: number;
}) => {
const [currentCompanyParams, setCurrentCompanyParams] =
useState<Database['medreport']['Tables']['company_params']['Row']>(
companyParams,
);
const [isLoading, setIsLoading] = useState<boolean>(false);
const form = useForm({
resolver: zodResolver(UpdateHealthBenefitSchema),
mode: 'onChange',
defaultValues: {
occurance: companyParams.benefit_occurance || 'yearly',
amount: companyParams.benefit_amount || 0,
occurance: currentCompanyParams.benefit_occurance || 'yearly',
amount: currentCompanyParams.benefit_amount || 0,
},
});
const onSubmit = (data: { occurance: string; amount: number }) => {
const promise = async () => {
setIsLoading(true);
try {
await updateHealthBenefit({ ...data, accountId: account.id });
setCurrentCompanyParams((prev) => ({
...prev,
benefit_amount: data.amount,
benefit_occurance: data.occurance,
}));
} finally {
setIsLoading(false);
}
};
toast.promise(promise, {
success: 'Andmed uuendatud',
error: 'error',
});
};
return (
<Form {...form}>
<form
className="flex flex-col gap-6 px-6 text-left"
onSubmit={form.handleSubmit((data) => {
toast.promise(
() => updateHealthBenefit({ ...data, accountId: account.id }),
{
success: 'success',
error: 'error',
},
);
})}
onSubmit={form.handleSubmit(onSubmit)}
>
<div className="mt-8 flex items-center justify-between">
<div>
@@ -61,13 +83,13 @@ const HealthBenefitForm = ({
<Trans i18nKey="billing:description" />
</p>
</div>
<Button type="submit" className="relative">
{form.formState.isSubmitting && (
<Button type="submit" className="relative" disabled={isLoading}>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<Spinner />
</div>
)}
<div className={cn({ invisible: form.formState.isSubmitting })}>
<div className={cn({ invisible: isLoading })}>
<Trans i18nKey="account:saveChanges" />
</div>
</Button>
@@ -82,7 +104,7 @@ const HealthBenefitForm = ({
<Trans i18nKey="billing:healthBenefitForm.description" />
</p>
<p className="pt-2 text-2xl font-semibold">
{companyParams.benefit_amount || 0}
{currentCompanyParams.benefit_amount || 0}
</p>
</div>
@@ -95,8 +117,8 @@ const HealthBenefitForm = ({
<div className="flex-1">
<YearlyExpensesOverview
employeeCount={22}
companyParams={companyParams}
employeeCount={employeeCount}
companyParams={currentCompanyParams}
/>
<p className="text-muted-foreground mt-2 text-sm">
<Trans i18nKey="billing:healthBenefitForm.info" />

View File

@@ -21,7 +21,7 @@ const YearlyExpensesOverview = ({
case 'yearly':
return (companyParams.benefit_amount / 12).toFixed(2);
case 'quarterly':
return (companyParams.benefit_amount / 4).toFixed(2);
return (companyParams.benefit_amount / 3).toFixed(2);
case 'monthly':
return companyParams.benefit_amount.toFixed(2);
default:
@@ -38,7 +38,7 @@ const YearlyExpensesOverview = ({
case 'yearly':
return companyParams.benefit_amount.toFixed(2);
case 'quarterly':
return (companyParams.benefit_amount * 4).toFixed(2);
return (companyParams.benefit_amount * 3).toFixed(2);
case 'monthly':
return (companyParams.benefit_amount * 12).toFixed(2);
default:
@@ -80,8 +80,15 @@ const YearlyExpensesOverview = ({
</div>
<Separator />
<div className="mt-4 flex justify-between">
<p className="font-semibold">Kokku</p>
<span className="font-semibold">13 200,00 </span>
<p className="font-semibold">
<Trans i18nKey="billing:expensesOverview.sum" />
</p>
<span className="font-semibold">
{companyParams.benefit_amount
? companyParams.benefit_amount * employeeCount
: 0}{' '}
</span>
</div>
</div>
);

View File

@@ -1,48 +0,0 @@
'use client';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
import { useCaptureException } from '@kit/monitoring/hooks';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Button } from '@kit/ui/button';
import { PageBody, PageHeader } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
export default function BillingErrorPage({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useCaptureException(error);
return (
<>
<PageHeader description={<AppBreadcrumbs />} />
<PageBody>
<div className={'flex flex-col space-y-4'}>
<Alert variant={'destructive'}>
<ExclamationTriangleIcon className={'h-4'} />
<AlertTitle>
<Trans i18nKey={'billing:planPickerAlertErrorTitle'} />
</AlertTitle>
<AlertDescription>
<Trans i18nKey={'billing:planPickerAlertErrorDescription'} />
</AlertDescription>
</Alert>
<div>
<Button variant={'outline'} onClick={reset}>
<Trans i18nKey={'common:retry'} />
</Button>
</div>
</div>
</PageBody>
</>
);
}

View File

@@ -1,3 +1,4 @@
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
@@ -23,13 +24,20 @@ export const generateMetadata = async () => {
async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
const accountSlug = (await params).account;
const api = createTeamAccountsApi(getSupabaseServerClient());
const client = getSupabaseServerClient();
const api = createTeamAccountsApi(client);
const account = await api.getTeamAccount(accountSlug);
const companyParams = await api.getTeamAccountParams(account.id);
const accounts = await api.getMembers(accountSlug);
return (
<PageBody>
<HealthBenefitForm account={account} companyParams={companyParams} />
<HealthBenefitForm
account={account}
companyParams={companyParams}
employeeCount={accounts.length}
/>
</PageBody>
);
}

View File

@@ -24,7 +24,8 @@ export const generateMetadata = async () => {
};
async function SelectPackagePage() {
const { analysisPackageElements, analysisPackages, countryCode } = await loadAnalysisPackages();
const { analysisPackageElements, analysisPackages, countryCode } =
await loadAnalysisPackages();
return (
<div className="container mx-auto my-24 flex flex-col items-center space-y-12">
@@ -44,7 +45,10 @@ async function SelectPackagePage() {
}
/>
</div>
<SelectAnalysisPackages analysisPackages={analysisPackages} countryCode={countryCode} />
<SelectAnalysisPackages
analysisPackages={analysisPackages}
countryCode={countryCode}
/>
<div className="flex justify-center">
<Link href={pathsConfig.app.home}>
<Button variant="secondary" className="align-center">

View File

@@ -1,4 +1,7 @@
import withBundleAnalyzer from '@next/bundle-analyzer';
import transpileModules from 'next-transpile-modules';
const withTM = transpileModules(['lucide-react']);
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
@@ -25,7 +28,7 @@ const INTERNAL_PACKAGES = [
];
/** @type {import('next').NextConfig} */
const config = {
const config = withTM({
reactStrictMode: true,
/** Enables hot reloading for local packages without a build step */
transpilePackages: INTERNAL_PACKAGES,
@@ -68,7 +71,7 @@ const config = {
/** We already do linting and typechecking as separate tasks in CI */
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
};
});
export default withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',

View File

@@ -3,6 +3,7 @@ import { BadgeX, Ban, ShieldPlus, VenetianMask } from 'lucide-react';
import { Tables } from '@kit/supabase/database';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createTeamAccountsApi } from '@kit/team-accounts/api';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Badge } from '@kit/ui/badge';
@@ -162,7 +163,8 @@ async function PersonalAccountPage(props: { account: Account }) {
async function TeamAccountPage(props: {
account: Account & { memberships: Membership[] };
}) {
const members = await getMembers(props.account.slug ?? '');
const api = createTeamAccountsApi(getSupabaseServerClient());
const members = await api.getMembers(props.account.slug ?? '');
return (
<>
@@ -386,17 +388,3 @@ async function getMemberships(userId: string) {
return memberships.data;
}
async function getMembers(accountSlug: string) {
const client = getSupabaseServerClient();
const members = await client.schema('medreport').rpc('get_account_members', {
account_slug: accountSlug,
});
if (members.error) {
throw members.error;
}
return members.data;
}

View File

@@ -1,225 +0,0 @@
'use client';
import Link from 'next/link';
import { User } from '@supabase/supabase-js';
import { ExternalLink } from '@/public/assets/external-link';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Button } from '@kit/ui/button';
import { Checkbox } from '@kit/ui/checkbox';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Trans } from '@kit/ui/trans';
import { UpdateAccountSchema } from '../schemas/update-account.schema';
import { onUpdateAccount } from '../server/actions/update-account-actions';
export function UpdateAccountForm({ user }: { user: User }) {
const form = useForm({
resolver: zodResolver(UpdateAccountSchema),
mode: 'onChange',
defaultValues: {
firstName: '',
lastName: '',
personalCode: '',
email: user.email,
phone: '',
city: '',
weight: 0,
height: 0,
userConsent: false,
},
});
return (
<Form {...form}>
<form
className="flex flex-col gap-6 px-6 pt-10 text-left"
onSubmit={form.handleSubmit(onUpdateAccount)}
>
<FormField
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:firstName'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:lastName'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="personalCode"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:personalCode'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:email'} />
</FormLabel>
<FormControl>
<Input {...field} disabled />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:phone'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="city"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:city'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-row justify-between gap-4">
<FormField
name="weight"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:weight'} />
</FormLabel>
<FormControl>
<Input
type="number"
placeholder="kg"
{...field}
value={field.value ?? ''}
onChange={(e) =>
field.onChange(
e.target.value === '' ? null : Number(e.target.value),
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="height"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:height'} />
</FormLabel>
<FormControl>
<Input
placeholder="cm"
type="number"
{...field}
value={field.value ?? ''}
onChange={(e) =>
field.onChange(
e.target.value === '' ? null : Number(e.target.value),
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
name="userConsent"
render={({ field }) => (
<FormItem>
<div className="flex flex-row items-center gap-2 pb-1">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel>
<Trans i18nKey={'account:updateAccount:userConsentLabel'} />
</FormLabel>
</div>
<Link
href={''}
className="flex flex-row items-center gap-2 text-sm hover:underline"
target="_blank"
>
<ExternalLink />
<Trans i18nKey={'account:updateAccount:userConsentUrlTitle'} />
</Link>
</FormItem>
)}
/>
<Button disabled={form.formState.isSubmitting} type="submit">
<Trans i18nKey={'account:updateAccount:button'} />
</Button>
</form>
</Form>
);
}

View File

@@ -2,7 +2,17 @@ import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database';
import { AccountSubmitData } from './actions/update-account-actions';
export interface AccountSubmitData {
firstName: string;
lastName: string;
personalCode: string;
email: string;
phone?: string;
city?: string;
weight: number | null;
height: number | null;
userConsent: boolean;
}
/**
* Class representing an API for interacting with user accounts.

View File

@@ -1,5 +1,5 @@
import Image from 'next/image';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { MedReportLogo } from '@/components/med-report-logo';
@@ -56,10 +56,11 @@ export const SuccessNotification = ({
</div>
)}
{buttonProps && (
<Button className="mt-8 w-full">
<Link href={buttonProps.href}>
<Button
className="mt-8 w-full"
onClick={() => redirect(buttonProps.href)}
>
<Trans i18nKey={buttonProps.buttonTitleKey} />
</Link>
</Button>
)}
</div>

View File

@@ -295,6 +295,20 @@ export class TeamAccountsApi {
return data;
}
async getMembers(accountSlug: string) {
const members = await this.client
.schema('medreport')
.rpc('get_account_members', {
account_slug: accountSlug,
});
if (members.error) {
throw members.error;
}
return members.data;
}
}
export function createTeamAccountsApi(client: SupabaseClient<Database>) {

66
pnpm-lock.yaml generated
View File

@@ -18669,8 +18669,8 @@ snapshots:
'@typescript-eslint/parser': 8.33.1(eslint@8.10.0)(typescript@5.8.3)
eslint: 8.10.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint@8.10.0))(eslint@8.10.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.10.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint@8.10.0)
eslint-plugin-jsx-a11y: 6.10.2(eslint@8.10.0)
eslint-plugin-react: 7.37.5(eslint@8.10.0)
eslint-plugin-react-hooks: 5.2.0(eslint@8.10.0)
@@ -18689,8 +18689,8 @@ snapshots:
'@typescript-eslint/parser': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.28.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@2.4.2))
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.28.0(jiti@2.4.2))
eslint-plugin-react: 7.37.5(eslint@9.28.0(jiti@2.4.2))
eslint-plugin-react-hooks: 5.2.0(eslint@9.28.0(jiti@2.4.2))
@@ -18715,22 +18715,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.1
eslint: 9.28.0(jiti@2.4.2)
get-tsconfig: 4.10.1
is-bun-module: 2.0.0
stable-hash: 0.0.5
tinyglobby: 0.2.14
unrs-resolver: 1.7.11
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2))
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint@8.10.0))(eslint@8.10.0):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@8.10.0):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.1
@@ -18741,33 +18726,48 @@ snapshots:
tinyglobby: 0.2.14
unrs-resolver: 1.7.11
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint@8.10.0)
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@2.4.2)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.1
eslint: 9.28.0(jiti@2.4.2)
get-tsconfig: 4.10.1
is-bun-module: 2.0.0
stable-hash: 0.0.5
tinyglobby: 0.2.14
unrs-resolver: 1.7.11
optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2))
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.10.0):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.33.1(eslint@8.10.0)(typescript@5.8.3)
eslint: 8.10.0
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint@8.10.0))(eslint@8.10.0)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.10.0)
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)):
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.28.0(jiti@2.4.2)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
eslint: 9.28.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.28.0(jiti@2.4.2))
transitivePeerDependencies:
- supports-color
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0):
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -18776,9 +18776,9 @@ snapshots:
array.prototype.flatmap: 1.3.3
debug: 3.2.7
doctrine: 2.1.0
eslint: 8.10.0
eslint: 9.28.0(jiti@2.4.2)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint@8.10.0))(eslint@8.10.0))(eslint@8.10.0)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.28.0(jiti@2.4.2))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -18790,13 +18790,13 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
'@typescript-eslint/parser': 8.33.1(eslint@8.10.0)(typescript@5.8.3)
'@typescript-eslint/parser': 8.32.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)):
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint@8.10.0):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@@ -18805,9 +18805,9 @@ snapshots:
array.prototype.flatmap: 1.3.3
debug: 3.2.7
doctrine: 2.1.0
eslint: 9.28.0(jiti@2.4.2)
eslint: 8.10.0
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.32.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2)))(eslint@9.28.0(jiti@2.4.2))
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.33.1(eslint@8.10.0)(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.10.0)
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3
@@ -18819,7 +18819,7 @@ snapshots:
string.prototype.trimend: 1.0.9
tsconfig-paths: 3.15.0
optionalDependencies:
'@typescript-eslint/parser': 8.33.1(eslint@9.28.0(jiti@2.4.2))(typescript@5.8.3)
'@typescript-eslint/parser': 8.33.1(eslint@8.10.0)(typescript@5.8.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack

View File

@@ -137,6 +137,7 @@
"title": "Kulude ülevaade 2025 aasta raames",
"monthly": "Kulu töötaja kohta kuus *",
"yearly": "Maksimaalne kulu inimese kohta kokku aastas *",
"total": "Maksimaalne kulu {{employeeCount}} töötaja kohta aastas *"
"total": "Maksimaalne kulu {{employeeCount}} töötaja kohta aastas *",
"sum": "Kokku"
}
}

View File

@@ -0,0 +1,3 @@
grant
execute on function medreport.can_action_account_member (uuid, uuid) to authenticated,
service_role;

View File

@@ -0,0 +1,189 @@
CREATE OR REPLACE FUNCTION medreport.insert_company_params_on_new_company()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
IF (TG_OP = 'INSERT' AND NEW.slug IS NOT NULL) THEN
INSERT INTO medreport.company_params (
account_id,
slug,
benefit_occurance,
benefit_amount
) VALUES (
NEW.id,
NEW.slug,
NULL,
NULL
);
END IF;
RETURN NEW;
END;
$function$
;
grant execute on function medreport.insert_company_params_on_new_company() to authenticated,
service_role;
CREATE OR REPLACE FUNCTION log_company_params_changes()
RETURNS trigger AS $$
BEGIN
-- For INSERT operation
IF (TG_OP = 'INSERT') THEN
INSERT INTO audit.log_entries (
schema_name,
table_name,
record_key,
operation,
row_data,
changed_data,
changed_by,
changed_by_role,
changed_at
)
VALUES (
'medreport', -- Schema name
'company_params', -- Table name
NEW.id, -- The ID of the inserted row
'INSERT', -- Operation type
NULL, -- No old data for INSERT
row_to_json(NEW), -- New data (after the INSERT)
auth.uid(), -- The user performing the insert
SESSION_USER, -- The role performing the insert
CURRENT_TIMESTAMP -- Timestamp of the insert
);
-- For UPDATE operation
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO audit.log_entries (
schema_name,
table_name,
record_key,
operation,
row_data,
changed_data,
changed_by,
changed_by_role,
changed_at
)
VALUES (
'medreport', -- Schema name
'company_params', -- Table name
OLD.id, -- The ID of the updated row
'UPDATE', -- Operation type
row_to_json(OLD), -- Old data (before the update)
row_to_json(NEW), -- New data (after the update)
auth.uid(), -- The user performing the update
SESSION_USER, -- The role performing the update
CURRENT_TIMESTAMP -- Timestamp of the update
);
-- For DELETE operation
ELSIF (TG_OP = 'DELETE') THEN
INSERT INTO audit.log_entries (
schema_name,
table_name,
record_key,
operation,
row_data,
changed_data,
changed_by,
changed_by_role,
changed_at
)
VALUES (
'medreport', -- Schema name
'company_params', -- Table name
OLD.id, -- The ID of the deleted row
'DELETE', -- Operation type
row_to_json(OLD), -- Old data (before the delete)
NULL, -- No new data for DELETE
auth.uid(), -- The user performing the delete
SESSION_USER, -- The role performing the delete
CURRENT_TIMESTAMP -- Timestamp of the delete
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER company_params_audit_trigger
AFTER INSERT OR UPDATE OR DELETE ON medreport.company_params
FOR EACH ROW
EXECUTE FUNCTION log_company_params_changes();
create or replace function medreport.create_team_account (
account_name text,
new_personal_code text
) returns medreport.accounts
security definer
set search_path = ''
as $$
declare
existing_account medreport.accounts;
current_user uuid := (select auth.uid())::uuid;
new_account medreport.accounts;
begin
if not medreport.is_set('enable_team_accounts') then
raise exception 'Team accounts are not enabled';
end if;
-- Try to find existing account
select *
into existing_account
from medreport.accounts
where personal_code = new_personal_code
limit 1;
-- If not found, fail
if not found then
raise exception 'No account found with personal_code = %', new_personal_code;
end if;
insert into medreport.accounts(
name,
is_personal_account,
primary_owner_user_id)
values (
account_name,
false,
existing_account.id)
returning
* into new_account;
-- Insert membership
insert into medreport.accounts_memberships (
user_id,
account_id,
account_role,
created_by,
updated_by,
created_at,
updated_at,
has_seen_confirmation
)
values (
existing_account.id,
new_account.id,
'owner',
null,
null,
now(),
now(),
false
)
on conflict do nothing;
return new_account;
end;
$$ language plpgsql;
grant execute on function medreport.create_team_account (text, text) to authenticated, service_role;
ALTER TABLE "medreport"."accounts"
DROP CONSTRAINT "accounts_primary_owner_user_id_fkey";
ALTER TABLE "medreport"."accounts"
ADD CONSTRAINT "accounts_primary_owner_user_id_fkey"
FOREIGN KEY (primary_owner_user_id)
REFERENCES auth.users(id)
ON DELETE CASCADE;