Merge branch 'main' into B2B-30

This commit is contained in:
devmc-ee
2025-07-01 23:27:59 +03:00
95 changed files with 2343 additions and 2297 deletions

View File

@@ -2,4 +2,4 @@
This package is responsible for managing email templates using the react.email library.
Here you can define email templates using React components and export them as a function that returns the email content.
Here you can define email templates using React components and export them as a function that returns the email content.

View File

@@ -0,0 +1,90 @@
import {
Body,
Head,
Html,
Preview,
Tailwind,
Text,
render,
} from '@react-email/components';
import { BodyStyle } from '../components/body-style';
import { EmailContent } from '../components/content';
import { EmailHeader } from '../components/header';
import { EmailHeading } from '../components/heading';
import { EmailWrapper } from '../components/wrapper';
import { initializeEmailI18n } from '../lib/i18n';
export async function renderCompanyOfferEmail({
language,
companyData,
}: {
language?: string;
companyData: {
companyName: string;
contactPerson: string;
email: string;
phone?: string;
};
}) {
const namespace = 'company-offer-email';
const { t } = await initializeEmailI18n({
language,
namespace,
});
const to = process.env.CONTACT_EMAIL || '';
const previewText = t(`${namespace}:previewText`, {
companyName: companyData.companyName,
});
const subject = t(`${namespace}:subject`, {
companyName: companyData.companyName,
});
const html = await render(
<Html>
<Head>
<BodyStyle />
</Head>
<Preview>{previewText}</Preview>
<Tailwind>
<Body>
<EmailWrapper>
<EmailHeader>
<EmailHeading>{previewText}</EmailHeading>
</EmailHeader>
<EmailContent>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:companyName`)} {companyData.companyName}
</Text>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:contactPerson`)} {companyData.contactPerson}
</Text>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:email`)} {companyData.email}
</Text>
<Text className="text-[16px] leading-[24px] text-[#242424]">
{t(`${namespace}:phone`)} {companyData.phone || 'N/A'}
</Text>
</EmailContent>
</EmailWrapper>
</Body>
</Tailwind>
</Html>,
);
return {
html,
subject,
to,
};
}

View File

@@ -1,3 +1,4 @@
export * from './emails/invite.email';
export * from './emails/account-delete.email';
export * from './emails/otp.email';
export * from './emails/company-offer.email';

View File

@@ -0,0 +1,8 @@
{
"subject": "Uus ettevõtte liitumispäring",
"previewText": "Ettevõte {{companyName}} soovib pakkumist",
"companyName": "Ettevõtte nimi:",
"contactPerson": "Kontaktisik:",
"email": "E-mail:",
"phone": "Telefon:"
}

View File

@@ -26,7 +26,8 @@ export function usePersonalAccountData(
`
id,
name,
picture_url
picture_url,
last_name
`,
)
.eq('primary_owner_user_id', userId)

View File

@@ -215,7 +215,6 @@ async function TeamAccountPage(props: {
<div>
<div className={'flex flex-col gap-y-8'}>
<div className={'flex flex-col gap-y-2.5'}>
<Heading level={6}>Company Members</Heading>

View File

@@ -4,6 +4,8 @@ import { useRouter } from 'next/navigation';
import type { Provider } from '@supabase/supabase-js';
import { useSupabase } from '@/packages/supabase/src/hooks/use-supabase';
import { isBrowser } from '@kit/shared/utils';
import { If } from '@kit/ui/if';
import { Separator } from '@kit/ui/separator';
@@ -20,6 +22,7 @@ export function SignInMethodsContainer(props: {
callback: string;
joinTeam: string;
returnPath: string;
updateAccount: string;
};
providers: {
@@ -28,13 +31,14 @@ export function SignInMethodsContainer(props: {
oAuth: Provider[];
};
}) {
const client = useSupabase();
const router = useRouter();
const redirectUrl = isBrowser()
? new URL(props.paths.callback, window?.location.origin).toString()
: '';
const onSignIn = () => {
const onSignIn = async (userId?: string) => {
// if the user has an invite token, we should join the team
if (props.inviteToken) {
const searchParams = new URLSearchParams({
@@ -45,8 +49,28 @@ export function SignInMethodsContainer(props: {
router.replace(joinTeamPath);
} else {
// otherwise, we should redirect to the return path
router.replace(props.paths.returnPath);
if (!userId) {
router.replace(props.paths.callback);
return;
}
try {
const { data: hasPersonalCode } = await client.rpc(
'has_personal_code',
{
account_id: userId,
},
);
if (hasPersonalCode) {
router.replace(props.paths.returnPath);
} else {
router.replace(props.paths.updateAccount);
}
} catch {
router.replace(props.paths.callback);
return;
}
}
};

View File

@@ -17,6 +17,7 @@ export function SignUpMethodsContainer(props: {
paths: {
callback: string;
appHome: string;
updateAccount: string;
};
providers: {

View File

@@ -0,0 +1,225 @@
'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

@@ -0,0 +1,44 @@
import { z } from 'zod';
export const UpdateAccountSchema = z.object({
firstName: z
.string({
required_error: 'First name is required',
})
.nonempty(),
lastName: z
.string({
required_error: 'Last name is required',
})
.nonempty(),
personalCode: z
.string({
required_error: 'Personal code is required',
})
.nonempty(),
email: z.string().email({
message: 'Email is required',
}),
phone: z
.string({
required_error: 'Phone number is required',
})
.nonempty(),
city: z.string().optional(),
weight: z
.number({
required_error: 'Weight is required',
invalid_type_error: 'Weight must be a number',
})
.gt(0, { message: 'Weight must be greater than 0' }),
height: z
.number({
required_error: 'Height is required',
invalid_type_error: 'Height must be a number',
})
.gt(0, { message: 'Height must be greater than 0' }),
userConsent: z.boolean().refine((val) => val === true, {
message: 'Must be true',
}),
});

View File

@@ -0,0 +1,44 @@
'use server';
import { redirect } from 'next/navigation';
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';
import { createAuthApi } from '../api';
export interface AccountSubmitData {
firstName: string;
lastName: string;
personalCode: string;
email: string;
phone?: string;
city?: string;
weight: number | null;
height: number | null;
userConsent: boolean;
}
export const onUpdateAccount = enhanceAction(
async (params) => {
const client = getSupabaseServerClient();
const api = createAuthApi(client);
try {
await api.updateAccount(params);
console.log('SUCCESS', pathsConfig.auth.updateAccountSuccess);
} catch (err: unknown) {
if (err instanceof Error) {
console.warn('On update account error: ' + err.message);
}
console.warn('On update account error: ', err);
}
redirect(pathsConfig.auth.updateAccountSuccess);
},
{
schema: UpdateAccountSchema,
},
);

View File

@@ -0,0 +1,93 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database';
import { AccountSubmitData } from './actions/update-account-actions';
/**
* Class representing an API for interacting with user accounts.
* @constructor
* @param {SupabaseClient<Database>} client - The Supabase client instance.
*/
class AuthApi {
constructor(private readonly client: SupabaseClient<Database>) {}
/**
* @name hasPersonalCode
* @description Check if given account ID has added personal code.
* @param id
*/
async hasPersonalCode(id: string) {
const { data, error } = await this.client.rpc('has_personal_code', {
account_id: id,
});
if (error) {
throw error;
}
return data;
}
/**
* @name updateAccount
* @description Update required fields for the account.
* @param data
*/
async updateAccount(data: AccountSubmitData) {
const {
data: { user },
} = await this.client.auth.getUser();
if (!user) {
throw new Error('User not authenticated');
}
const { error } = await this.client.rpc('update_account', {
p_name: data.firstName,
p_last_name: data.lastName,
p_personal_code: data.personalCode,
p_phone: data.phone || '',
p_city: data.city || '',
p_has_consent_personal_data: data.userConsent,
p_uid: user.id,
});
if (error) {
throw error;
}
if (data.height || data.weight) {
await this.updateAccountParams(data);
}
}
/**
* @name updateAccountParams
* @description Update account parameters.
* @param data
*/
async updateAccountParams(data: AccountSubmitData) {
const {
data: { user },
} = await this.client.auth.getUser();
if (!user) {
throw new Error('User not authenticated');
}
console.log('test', user, data);
const response = await this.client.from('account_params').insert({
account_id: user.id,
height: data.height,
weight: data.weight,
});
if (response.error) {
throw response.error;
}
}
}
export function createAuthApi(client: SupabaseClient<Database>) {
return new AuthApi(client);
}

View File

@@ -1,2 +1,3 @@
export * from './components/sign-up-methods-container';
export * from './schemas/password-sign-up.schema';
export * from './components/update-account-form';

View File

@@ -1,5 +1,5 @@
{
"extends": "@kit/tsconfig/base.json",
"extends": "../../../tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},

View File

@@ -1 +1,3 @@
export * from './notifications-popover';
export * from './success-notification';
export * from './update-account-success-notification';

View File

@@ -126,7 +126,7 @@ export function NotificationsPopover(params: {
<span
className={cn(
`fade-in animate-in zoom-in absolute right-1 top-1 mt-0 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-red-500 text-[0.65rem] text-white`,
`fade-in animate-in zoom-in absolute top-1 right-1 mt-0 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-red-500 text-[0.65rem] text-white`,
{
hidden: !notifications.length,
},
@@ -186,7 +186,7 @@ export function NotificationsPopover(params: {
<div
key={notification.id.toString()}
className={cn(
'min-h-18 flex flex-col items-start justify-center gap-y-1 px-3 py-2',
'flex min-h-18 flex-col items-start justify-center gap-y-1 px-3 py-2',
)}
onClick={() => {
if (params.onClick) {

View File

@@ -0,0 +1,50 @@
import Image from 'next/image';
import Link from 'next/link';
import { MedReportLogo } from '@/components/med-report-logo';
import { Button } from '@kit/ui/button';
import { Trans } from '@kit/ui/trans';
export const SuccessNotification = ({
showLogo = true,
title,
titleKey,
descriptionKey,
buttonProps,
}: {
showLogo?: boolean;
title?: string;
titleKey?: string;
descriptionKey?: string;
buttonProps?: {
buttonTitleKey: string;
href: string;
};
}) => {
return (
<div className="border-border rounded-3xl border px-16 pt-4 pb-12">
{showLogo && <MedReportLogo />}
<div className="flex flex-col items-center px-4">
<Image
src="/assets/success.png"
alt="Success"
className="pt-6 pb-8"
width={326}
height={195}
/>
<h1 className="pb-2">{title || <Trans i18nKey={titleKey} />}</h1>
<p className="text-muted-foreground text-sm">
<Trans i18nKey={descriptionKey} />
</p>
</div>
{buttonProps && (
<Button className="mt-8 w-full">
<Link href={buttonProps.href}>
<Trans i18nKey={buttonProps.buttonTitleKey} />
</Link>
</Button>
)}
</div>
);
};

View File

@@ -0,0 +1,39 @@
'use client';
import { redirect } from 'next/navigation';
import pathsConfig from '@/config/paths.config';
import { useTranslation } from 'react-i18next';
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { SuccessNotification } from './success-notification';
export const UpdateAccountSuccessNotification = ({
userId,
}: {
userId?: string;
}) => {
const { t } = useTranslation('account');
if (!userId) {
redirect(pathsConfig.app.home);
}
const { data: accountData } = usePersonalAccountData(userId);
return (
<SuccessNotification
showLogo={false}
title={t('account:updateAccount:successTitle', {
firstName: accountData?.name,
lastName: accountData?.last_name,
})}
descriptionKey="account:updateAccount:successDescription"
buttonProps={{
buttonTitleKey: 'account:updateAccount:successButton',
href: pathsConfig.app.home,
}}
/>
);
};

View File

@@ -1,5 +1,5 @@
{
"extends": "@kit/tsconfig/base.json",
"extends": "../../../tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},

View File

@@ -5,7 +5,7 @@ export const MailerSchema = z
to: z.string().email(),
// this is not necessarily formatted
// as an email so we type it loosely
from: z.string().min(1),
from: z.string().min(1).optional(),
subject: z.string(),
})
.and(

View File

@@ -48,6 +48,48 @@ export type Database = {
}
Relationships: []
}
request_entries: {
Row: {
comment: string | null
created_at: string
id: number
personal_code: number | null
request_api: string
request_api_method: string
requested_end_date: string | null
requested_start_date: string | null
service_id: number | null
service_provider_id: number | null
status: Database["audit"]["Enums"]["request_status"]
}
Insert: {
comment?: string | null
created_at?: string
id?: number
personal_code?: number | null
request_api: string
request_api_method: string
requested_end_date?: string | null
requested_start_date?: string | null
service_id?: number | null
service_provider_id?: number | null
status: Database["audit"]["Enums"]["request_status"]
}
Update: {
comment?: string | null
created_at?: string
id?: number
personal_code?: number | null
request_api?: string
request_api_method?: string
requested_end_date?: string | null
requested_start_date?: string | null
service_id?: number | null
service_provider_id?: number | null
status?: Database["audit"]["Enums"]["request_status"]
}
Relationships: []
}
sync_entries: {
Row: {
changed_by_role: string
@@ -83,6 +125,7 @@ export type Database = {
[_ in never]: never
}
Enums: {
request_status: "SUCCESS" | "FAIL"
sync_status: "SUCCESS" | "FAIL"
}
CompositeTypes: {
@@ -116,15 +159,43 @@ export type Database = {
}
public: {
Tables: {
account_params: {
Row: {
account_id: string
height: number | null
id: string
recorded_at: string
weight: number | null
}
Insert: {
account_id?: string
height?: number | null
id?: string
recorded_at?: string
weight?: number | null
}
Update: {
account_id?: string
height?: number | null
id?: string
recorded_at?: string
weight?: number | null
}
Relationships: []
}
accounts: {
Row: {
city: string | null
created_at: string | null
created_by: string | null
email: string | null
has_consent_personal_data: boolean | null
id: string
is_personal_account: boolean
last_name: string | null
name: string
personal_code: string | null
phone: string | null
picture_url: string | null
primary_owner_user_id: string
public_data: Json
@@ -133,13 +204,17 @@ export type Database = {
updated_by: string | null
}
Insert: {
city?: string | null
created_at?: string | null
created_by?: string | null
email?: string | null
has_consent_personal_data?: boolean | null
id?: string
is_personal_account?: boolean
last_name?: string | null
name: string
personal_code?: string | null
phone?: string | null
picture_url?: string | null
primary_owner_user_id?: string
public_data?: Json
@@ -148,13 +223,17 @@ export type Database = {
updated_by?: string | null
}
Update: {
city?: string | null
created_at?: string | null
created_by?: string | null
email?: string | null
has_consent_personal_data?: boolean | null
id?: string
is_personal_account?: boolean
last_name?: string | null
name?: string
personal_code?: string | null
phone?: string | null
picture_url?: string | null
primary_owner_user_id?: string
public_data?: Json
@@ -200,20 +279,6 @@ export type Database = {
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "accounts_memberships_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "accounts_memberships_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_personal_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "accounts_memberships_account_id_fkey"
columns: ["account_id"]
@@ -515,20 +580,6 @@ export type Database = {
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "billing_customers_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "billing_customers_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_personal_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "billing_customers_account_id_fkey"
columns: ["account_id"]
@@ -627,6 +678,158 @@ export type Database = {
}
Relationships: []
}
connected_online_providers: {
Row: {
can_select_worker: boolean
created_at: string
email: string | null
id: number
name: string
personal_code_required: boolean
phone_number: string | null
updated_at: string | null
}
Insert: {
can_select_worker: boolean
created_at?: string
email?: string | null
id: number
name: string
personal_code_required: boolean
phone_number?: string | null
updated_at?: string | null
}
Update: {
can_select_worker?: boolean
created_at?: string
email?: string | null
id?: number
name?: string
personal_code_required?: boolean
phone_number?: string | null
updated_at?: string | null
}
Relationships: []
}
connected_online_reservation: {
Row: {
booking_code: string
clinic_id: number
comments: string | null
created_at: string
discount_code: string | null
id: number
lang: string
requires_payment: boolean
service_id: number
service_user_id: number | null
start_time: string
sync_user_id: number
updated_at: string | null
user_id: string
}
Insert: {
booking_code: string
clinic_id: number
comments?: string | null
created_at?: string
discount_code?: string | null
id?: number
lang: string
requires_payment: boolean
service_id: number
service_user_id?: number | null
start_time: string
sync_user_id: number
updated_at?: string | null
user_id: string
}
Update: {
booking_code?: string
clinic_id?: number
comments?: string | null
created_at?: string
discount_code?: string | null
id?: number
lang?: string
requires_payment?: boolean
service_id?: number
service_user_id?: number | null
start_time?: string
sync_user_id?: number
updated_at?: string | null
user_id?: string
}
Relationships: []
}
connected_online_services: {
Row: {
clinic_id: number
code: string
created_at: string
description: string | null
display: string | null
duration: number
has_free_codes: boolean
id: number
name: string
neto_duration: number | null
online_hide_duration: number | null
online_hide_price: number | null
price: number
price_periods: string | null
requires_payment: boolean
sync_id: number
updated_at: string | null
}
Insert: {
clinic_id: number
code: string
created_at?: string
description?: string | null
display?: string | null
duration: number
has_free_codes: boolean
id: number
name: string
neto_duration?: number | null
online_hide_duration?: number | null
online_hide_price?: number | null
price: number
price_periods?: string | null
requires_payment: boolean
sync_id: number
updated_at?: string | null
}
Update: {
clinic_id?: number
code?: string
created_at?: string
description?: string | null
display?: string | null
duration?: number
has_free_codes?: boolean
id?: number
name?: string
neto_duration?: number | null
online_hide_duration?: number | null
online_hide_price?: number | null
price?: number
price_periods?: string | null
requires_payment?: boolean
sync_id?: number
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "connected_online_services_clinic_id_fkey"
columns: ["clinic_id"]
isOneToOne: false
referencedRelation: "connected_online_providers"
referencedColumns: ["id"]
},
]
}
invitations: {
Row: {
account_id: string
@@ -672,20 +875,6 @@ export type Database = {
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "invitations_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "invitations_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_personal_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "invitations_account_id_fkey"
columns: ["account_id"]
@@ -709,6 +898,129 @@ export type Database = {
},
]
}
medreport_product_groups: {
Row: {
created_at: string
id: number
name: string
updated_at: string | null
}
Insert: {
created_at?: string
id?: number
name: string
updated_at?: string | null
}
Update: {
created_at?: string
id?: number
name?: string
updated_at?: string | null
}
Relationships: []
}
medreport_products: {
Row: {
created_at: string
id: number
name: string
product_group_id: number | null
updated_at: string | null
}
Insert: {
created_at?: string
id?: number
name: string
product_group_id?: number | null
updated_at?: string | null
}
Update: {
created_at?: string
id?: number
name?: string
product_group_id?: number | null
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "medreport_products_product_groups_id_fkey"
columns: ["product_group_id"]
isOneToOne: false
referencedRelation: "medreport_product_groups"
referencedColumns: ["id"]
},
]
}
medreport_products_analyses_relations: {
Row: {
analysis_element_id: number | null
analysis_id: number | null
product_id: number
}
Insert: {
analysis_element_id?: number | null
analysis_id?: number | null
product_id: number
}
Update: {
analysis_element_id?: number | null
analysis_id?: number | null
product_id?: number
}
Relationships: [
{
foreignKeyName: "medreport_products_analyses_analysis_element_id_fkey"
columns: ["analysis_element_id"]
isOneToOne: true
referencedRelation: "analysis_elements"
referencedColumns: ["id"]
},
{
foreignKeyName: "medreport_products_analyses_analysis_id_fkey"
columns: ["analysis_id"]
isOneToOne: true
referencedRelation: "analyses"
referencedColumns: ["id"]
},
{
foreignKeyName: "medreport_products_analyses_product_id_fkey"
columns: ["product_id"]
isOneToOne: true
referencedRelation: "medreport_products"
referencedColumns: ["id"]
},
]
}
medreport_products_external_services_relations: {
Row: {
connected_online_service_id: number
product_id: number
}
Insert: {
connected_online_service_id: number
product_id: number
}
Update: {
connected_online_service_id?: number
product_id?: number
}
Relationships: [
{
foreignKeyName: "medreport_products_connected_online_services_id_fkey"
columns: ["connected_online_service_id"]
isOneToOne: true
referencedRelation: "connected_online_services"
referencedColumns: ["id"]
},
{
foreignKeyName: "medreport_products_connected_online_services_product_id_fkey"
columns: ["product_id"]
isOneToOne: false
referencedRelation: "medreport_products"
referencedColumns: ["id"]
},
]
}
nonces: {
Row: {
client_token: string
@@ -808,20 +1120,6 @@ export type Database = {
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "notifications_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "notifications_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_personal_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "notifications_account_id_fkey"
columns: ["account_id"]
@@ -921,20 +1219,6 @@ export type Database = {
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "orders_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "orders_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_personal_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "orders_account_id_fkey"
columns: ["account_id"]
@@ -1106,20 +1390,6 @@ export type Database = {
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "subscriptions_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "subscriptions_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "invitations_with_personal_accounts"
referencedColumns: ["account_id"]
},
{
foreignKeyName: "subscriptions_account_id_fkey"
columns: ["account_id"]
@@ -1145,23 +1415,6 @@ export type Database = {
}
}
Views: {
invitations_with_accounts: {
Row: {
account_id: string | null
invite_token: string | null
personal_code: string | null
}
Relationships: []
}
invitations_with_personal_accounts: {
Row: {
account_id: string | null
account_slug: string | null
invite_token: string | null
personal_code: string | null
}
Relationships: []
}
user_account_workspace: {
Row: {
id: string | null
@@ -1241,13 +1494,17 @@ export type Database = {
create_team_account: {
Args: { account_name: string }
Returns: {
city: string | null
created_at: string | null
created_by: string | null
email: string | null
has_consent_personal_data: boolean | null
id: string
is_personal_account: boolean
last_name: string | null
name: string
personal_code: string | null
phone: string | null
picture_url: string | null
primary_owner_user_id: string
public_data: Json
@@ -1329,6 +1586,10 @@ export type Database = {
}
Returns: boolean
}
has_personal_code: {
Args: { account_id: string }
Returns: boolean
}
has_role_on_account: {
Args: { account_id: string; account_role?: string }
Returns: boolean
@@ -1395,6 +1656,18 @@ export type Database = {
Args: { target_account_id: string; new_owner_id: string }
Returns: undefined
}
update_account: {
Args: {
p_name: string
p_last_name: string
p_personal_code: string
p_phone: string
p_city: string
p_has_consent_personal_data: boolean
p_uid: string
}
Returns: undefined
}
upsert_order: {
Args: {
target_account_id: string
@@ -1611,6 +1884,7 @@ export type CompositeTypes<
export const Constants = {
audit: {
Enums: {
request_status: ["SUCCESS", "FAIL"],
sync_status: ["SUCCESS", "FAIL"],
},
},

View File

@@ -29,7 +29,7 @@ const RouteChild = z.object({
});
const RouteGroup = z.object({
label: z.string(),
label: z.string().optional(),
collapsible: z.boolean().optional(),
collapsed: z.boolean().optional(),
children: z.array(RouteChild),
@@ -37,12 +37,8 @@ const RouteGroup = z.object({
});
export const NavigationConfigSchema = z.object({
style: z.enum(['custom', 'sidebar', 'header']).default('sidebar'),
sidebarCollapsed: z
.enum(['false', 'true'])
.default('true')
.optional()
.transform((value) => value === `true`),
style: z.enum(['custom', 'sidebar', 'header']).default('custom'),
sidebarCollapsed: z.boolean().optional(),
sidebarCollapsedStyle: z.enum(['offcanvas', 'icon', 'none']).default('icon'),
routes: z.array(z.union([RouteGroup, Divider])),
});

View File

@@ -14,10 +14,6 @@ type PageProps = React.PropsWithChildren<{
sticky?: boolean;
}>;
const ENABLE_SIDEBAR_TRIGGER = process.env.NEXT_PUBLIC_ENABLE_SIDEBAR_TRIGGER
? process.env.NEXT_PUBLIC_ENABLE_SIDEBAR_TRIGGER === 'true'
: true;
export function Page(props: PageProps) {
switch (props.style) {
case 'header':
@@ -79,7 +75,7 @@ function PageWithHeader(props: PageProps) {
const { Navigation, Children, MobileNavigation } = getSlotsFromPage(props);
return (
<div className={cn('flex h-screen flex-1 flex-col', props.className)}>
<div className={cn('flex h-screen flex-1 flex-col z-1000', props.className)}>
<div
className={
props.contentContainerClassName ?? 'flex flex-1 flex-col space-y-4'
@@ -87,9 +83,9 @@ function PageWithHeader(props: PageProps) {
>
<div
className={cn(
'bg-bg-background border-1 light:border-border dark:border-border dark:shadow-primary/10 flex h-15 items-center justify-between px-4 py-1 lg:justify-start lg:shadow-xs',
'bg-bg-background border-1 light:border-border dark:border-border dark:shadow-primary/10 flex h-15 items-center justify-between px-4 py-1 lg:justify-start lg:shadow-xs border-b',
{
'sticky top-0 z-10 backdrop-blur-md': props.sticky ?? true,
'sticky top-0 z-1000 backdrop-blur-md': props.sticky ?? true,
},
)}
>
@@ -113,7 +109,10 @@ export function PageBody(
className?: string;
}>,
) {
const className = cn('flex w-full flex-1 flex-col lg:px-4', props.className);
const className = cn(
'flex w-full flex-1 flex-col space-y-6 lg:px-4',
props.className,
);
return <div className={className}>{props.children}</div>;
}
@@ -125,7 +124,7 @@ export function PageNavigation(props: React.PropsWithChildren) {
export function PageDescription(props: React.PropsWithChildren) {
return (
<div className={'flex h-6 items-center'}>
<div className={'text-muted-foreground text-xs leading-none font-normal'}>
<div className={'text-muted-foreground text-sm leading-none font-normal'}>
{props.children}
</div>
</div>
@@ -153,7 +152,7 @@ export function PageHeader({
title,
description,
className,
displaySidebarTrigger = ENABLE_SIDEBAR_TRIGGER,
displaySidebarTrigger = false,
}: React.PropsWithChildren<{
className?: string;
title?: string | React.ReactNode;

View File

@@ -70,7 +70,7 @@ export function VersionUpdater(props: { intervalTimeInSecond?: number }) {
setDismissed(true);
}}
>
<Trans i18nKey="common:back" />
<Trans i18nKey="common:goBack" />
</Button>
<Button onClick={() => window.location.reload()}>

View File

@@ -7,7 +7,7 @@ import type { VariantProps } from 'class-variance-authority';
import { cn } from '../lib/utils';
const buttonVariants = cva(
'focus-visible:ring-ring inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50',
'focus-visible:ring-ring gap-1 inline-flex items-center justify-center rounded-md text-sm font-medium whitespace-nowrap transition-colors focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {

View File

@@ -1,15 +1,31 @@
import * as React from 'react';
import { cn } from '../lib/utils';
import { VariantProps, cva } from 'class-variance-authority';
import { cn } from '.';
const Card: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
className,
...props
}) => (
<div
className={cn('bg-card text-card-foreground rounded-xl border', className)}
{...props}
/>
const cardVariants = cva('text-card-foreground rounded-xl border', {
variants: {
variant: {
default: 'bg-card',
'gradient-warning':
'from-warning/30 via-warning/10 to-background bg-gradient-to-t',
'gradient-destructive':
'from-destructive/30 via-destructive/10 to-background bg-gradient-to-t',
'gradient-success':
'from-success/30 via-success/10 to-background bg-gradient-to-t',
},
},
defaultVariants: {
variant: 'default',
},
});
export interface CardProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof cardVariants> {}
const Card: React.FC<CardProps> = ({ className, variant, ...props }) => (
<div className={cn(cardVariants({ variant, className }))} {...props} />
);
Card.displayName = 'Card';

View File

@@ -12,7 +12,7 @@ const Checkbox: React.FC<
> = ({ className, ...props }) => (
<CheckboxPrimitive.Root
className={cn(
'peer border-primary focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground h-4 w-4 shrink-0 rounded-xs border shadow-xs focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
'peer border-primary focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground h-4 w-4 shrink-0 rounded-sm border shadow-xs focus-visible:ring-1 focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}

View File

@@ -0,0 +1,3 @@
export const SIDEBAR_WIDTH = '16rem';
export const SIDEBAR_WIDTH_MOBILE = '18rem';
export const SIDEBAR_WIDTH_ICON = '4rem';

View File

@@ -21,6 +21,11 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from './collapsible';
import {
SIDEBAR_WIDTH,
SIDEBAR_WIDTH_ICON,
SIDEBAR_WIDTH_MOBILE,
} from './constants';
import { Input } from './input';
import { Separator } from './separator';
import { Sheet, SheetContent } from './sheet';
@@ -34,9 +39,6 @@ import {
const SIDEBAR_COOKIE_NAME = 'sidebar:state';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
const SIDEBAR_WIDTH = '16rem';
const SIDEBAR_WIDTH_MOBILE = '18rem';
const SIDEBAR_WIDTH_ICON = '4rem';
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
const SIDEBAR_MINIMIZED_WIDTH = SIDEBAR_WIDTH_ICON;
@@ -276,7 +278,7 @@ const Sidebar: React.FC<
<div
data-sidebar="sidebar"
className={cn(
'bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm',
'bg-sidebar group-data-[variant=floating]:border-sidebar-border ml-3 flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm',
{
'bg-transparent': variant === 'ghost',
},
@@ -908,7 +910,7 @@ export function SidebarNavigation({
tooltip={child.label}
>
<Link
className={cn('flex items-center', {
className={cn('flex items-center font-medium', {
'mx-auto w-full gap-0! [&>svg]:flex-1': !open,
})}
href={path}
@@ -916,7 +918,7 @@ export function SidebarNavigation({
{child.Icon}
<span
className={cn(
'w-auto transition-opacity duration-300',
'text-md w-auto font-medium transition-opacity duration-300',
{
'w-0 opacity-0': !open,
},

View File

@@ -1,5 +1,5 @@
{
"extends": "@kit/tsconfig/base.json",
"extends": "../../tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"paths": {
@@ -13,4 +13,4 @@
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}
}