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,10 +106,12 @@ 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.
- **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**.
@@ -121,7 +123,7 @@ In local dev environment, you can import products from B2B to Medusa with this A
- `POST /api/job/sync-analysis-groups-store`
- Syncs required data of `analyses`, `analysis_elements` data from **B2B** to **Medusa** and creates relevant products and categories.
If product or category already exists, then it is not recreated. Old entries are not deleted either currently.
If product or category already exists, then it is not recreated. Old entries are not deleted either currently.
## Jobs

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}>
<Trans i18nKey={buttonProps.buttonTitleKey} />
</Link>
<Button
className="mt-8 w-full"
onClick={() => redirect(buttonProps.href)}
>
<Trans i18nKey={buttonProps.buttonTitleKey} />
</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;