add health benefit form

fix super admin
This commit is contained in:
Danel Kungla
2025-07-23 16:33:24 +03:00
parent 2db67b7f20
commit 86b86c6752
43 changed files with 1329 additions and 561 deletions

View File

@@ -5,3 +5,4 @@ export * from './cancel-subscription-params.schema';
export * from './report-billing-usage.schema';
export * from './update-subscription-params.schema';
export * from './query-billing-usage.schema';
export * from './update-health-benefit.schema';

View File

@@ -0,0 +1,10 @@
import { z } from 'zod';
export const UpdateHealthBenefitSchema = z.object({
occurance: z
.string({
required_error: 'Occurance is required',
})
.nonempty(),
amount: z.number({ required_error: 'Amount is required' }),
});

View File

@@ -6,14 +6,9 @@ import Link from 'next/link';
import type { User } from '@supabase/supabase-js';
import {
ChevronsUpDown,
Home,
LogOut,
UserCircle,
Shield,
} from 'lucide-react';
import { ChevronsUpDown, Home, LogOut, Shield, UserCircle } from 'lucide-react';
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar';
import {
DropdownMenu,
DropdownMenuContent,
@@ -26,11 +21,11 @@ import { SubMenuModeToggle } from '@kit/ui/mode-toggle';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
import { usePersonalAccountData } from '../hooks/use-personal-account-data';
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar';
import { toTitleCase } from '~/lib/utils';
import { usePersonalAccountData } from '../hooks/use-personal-account-data';
const PERSONAL_ACCOUNT_SLUG = 'personal';
export function PersonalAccountDropdown({
@@ -41,7 +36,7 @@ export function PersonalAccountDropdown({
paths,
features,
account,
accounts = []
accounts = [],
}: {
user: User;
@@ -104,7 +99,8 @@ export function PersonalAccountDropdown({
className ?? '',
{
['active:bg-secondary/50 items-center gap-4 rounded-md' +
' hover:bg-secondary m-0 transition-colors border-1 rounded-md px-4 py-1 h-10']: showProfileName,
' hover:bg-secondary m-0 h-10 rounded-md border-1 px-4 py-1 transition-colors']:
showProfileName,
},
)}
>
@@ -127,7 +123,6 @@ export function PersonalAccountDropdown({
>
{toTitleCase(displayName)}
</span>
</div>
<ChevronsUpDown
@@ -171,7 +166,7 @@ export function PersonalAccountDropdown({
<DropdownMenuSeparator />
<If condition={accounts.length > 0}>
<span className='px-2 text-muted-foreground text-xs'>
<span className="text-muted-foreground px-2 text-xs">
<Trans
i18nKey={'teams:yourTeams'}
values={{ teamsCount: accounts.length }}
@@ -185,12 +180,15 @@ export function PersonalAccountDropdown({
href={`/home/${account.value}`}
>
<div className={'flex items-center'}>
<Avatar className={'rounded-xs h-5 w-5 ' + account.image}>
<AvatarImage {...(account.image && { src: account.image })} />
<Avatar className={'h-5 w-5 rounded-xs ' + account.image}>
<AvatarImage
{...(account.image && { src: account.image })}
/>
<AvatarFallback
className={cn('rounded-md', {
['bg-background']: PERSONAL_ACCOUNT_SLUG === account.value,
['bg-background']:
PERSONAL_ACCOUNT_SLUG === account.value,
['group-hover:bg-background']:
PERSONAL_ACCOUNT_SLUG !== account.value,
})}
@@ -199,9 +197,7 @@ export function PersonalAccountDropdown({
</AvatarFallback>
</Avatar>
<span className={'pl-3'}>
{account.label}
</span>
<span className={'pl-3'}>{account.label}</span>
</div>
</Link>
</DropdownMenuItem>

View File

@@ -3,11 +3,6 @@ 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 {
AccountInvitationsTable,
AccountMembersTable,
InviteMembersDialogContainer,
} from '@kit/team-accounts/components';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Badge } from '@kit/ui/badge';

View File

@@ -22,9 +22,11 @@ class CreateTeamAccountService {
logger.info(ctx, `Creating new company account...`);
const { error, data } = await this.client.rpc('create_team_account', {
account_name: params.name,
});
const { error, data } = await this.client
.schema('medreport')
.rpc('create_team_account', {
account_name: params.name,
});
if (error) {
logger.error(
@@ -35,7 +37,7 @@ class CreateTeamAccountService {
`Error creating company account`,
);
throw new Error('Error creating company account');
throw new Error('Error creating company account: ' + error);
}
logger.info(ctx, `Company account created successfully`);

View File

@@ -9,7 +9,9 @@ import { Database } from '@kit/supabase/database';
*/
export async function isSuperAdmin(client: SupabaseClient<Database>) {
try {
const { data, error } = await client.rpc('is_super_admin');
const { data, error } = await client
.schema('medreport')
.rpc('is_super_admin');
if (error) {
throw error;

View File

@@ -18,7 +18,9 @@
"./captcha/server": "./src/captcha/server/index.ts",
"./resend-email-link": "./src/components/resend-auth-link-form.tsx",
"./lib/utils/*": "./src/lib/utils/*.ts",
"./api": "./src/server/api.ts"
"./api": "./src/server/api.ts",
"./schemas/*": "./src/schemas/*.ts",
"./actions/*": "./src/server/actions/*.ts"
},
"devDependencies": {
"@hookform/resolvers": "^5.0.1",

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,60 @@
'use server';
import { redirect } from 'next/navigation';
import { updateCustomer } from '@lib/data/customer';
import { 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;
}
export const onUpdateAccount = enhanceAction(
async (params: AccountSubmitData) => {
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);
}
await updateCustomer({
first_name: params.firstName,
last_name: params.lastName,
phone: params.phone,
});
const hasUnseenMembershipConfirmation =
await api.hasUnseenMembershipConfirmation();
if (hasUnseenMembershipConfirmation) {
redirect(pathsConfig.auth.membershipConfirmation);
} else {
redirect(pathsConfig.app.selectPackage);
}
},
{
schema: UpdateAccountSchema,
},
);

View File

@@ -2,6 +2,8 @@ import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database';
import { UpdateHealthBenefitData } from './types';
/**
* Class representing an API for interacting with team accounts.
* @constructor
@@ -254,6 +256,45 @@ export class TeamAccountsApi {
return invitation;
}
/**
* @name updateHealthBenefit
* @description Update health benefits for the team account
*/
async updateHealthBenefit(data: UpdateHealthBenefitData) {
const { error } = await this.client
.schema('medreport')
.from('company_params')
.update({
benefit_occurance: data.occurance,
benefit_amount: data.amount,
updated_at: new Date().toISOString(),
})
.eq('account_id', data.accountId);
if (error) {
throw error;
}
}
/**
* @name getTeamAccountParams
* @description Get health benefits for the team account
*/
async getTeamAccountParams(accountId: string) {
const { data, error } = await this.client
.schema('medreport')
.from('company_params')
.select('*')
.eq('account_id', accountId)
.single();
if (error) {
throw error;
}
return data;
}
}
export function createTeamAccountsApi(client: SupabaseClient<Database>) {

View File

@@ -0,0 +1,5 @@
export interface UpdateHealthBenefitData {
accountId: string;
occurance: string;
amount: number;
}

View File

@@ -7,11 +7,16 @@ import { Database } from '@kit/supabase/database';
* @description Check if the current user is a super admin.
* @param client
*/
export async function isCompanyAdmin(client: SupabaseClient<Database>, accountSlug: string) {
export async function isCompanyAdmin(
client: SupabaseClient<Database>,
accountSlug: string,
) {
try {
const { data, error } = await client.rpc('is_company_admin', {
account_slug: accountSlug,
});
const { data, error } = await client
.schema('medreport')
.rpc('is_company_admin', {
account_slug: accountSlug,
});
if (error) {
throw error;

View File

@@ -9,6 +9,39 @@ export type Json =
export type Database = {
audit: {
Tables: {
cart_entries: {
Row: {
account_id: string
cart_id: string
changed_by: string
comment: string | null
created_at: string
id: number
operation: string
variant_id: string | null
}
Insert: {
account_id: string
cart_id: string
changed_by: string
comment?: string | null
created_at?: string
id?: number
operation: string
variant_id?: string | null
}
Update: {
account_id?: string
cart_id?: string
changed_by?: string
comment?: string | null
created_at?: string
id?: number
operation?: string
variant_id?: string | null
}
Relationships: []
}
log_entries: {
Row: {
changed_at: string
@@ -116,29 +149,7 @@ export type Database = {
status?: string
}
Relationships: []
},
cart_entries: {
Row: {
id: number
account_id: string
cart_id: string
operation: string
variant_id: string
comment: string | null
created_at: string
changed_by: string | null
}
Insert: {
id: number
account_id: string
cart_id: string
operation: string
variant_id: string
comment: string | null
created_at: string
changed_by: string | null
}
},
}
}
Views: {
[_ in never]: never
@@ -685,6 +696,58 @@ export type Database = {
},
]
}
company_params: {
Row: {
account_id: string | null
benefit_amount: number | null
benefit_occurance: string | null
created_at: string | null
id: string
slug: string | null
updated_at: string | null
}
Insert: {
account_id?: string | null
benefit_amount?: number | null
benefit_occurance?: string | null
created_at?: string | null
id?: string
slug?: string | null
updated_at?: string | null
}
Update: {
account_id?: string | null
benefit_amount?: number | null
benefit_occurance?: string | null
created_at?: string | null
id?: string
slug?: string | null
updated_at?: string | null
}
Relationships: [
{
foreignKeyName: "company_params_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "company_params_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "company_params_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
]
}
config: {
Row: {
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
@@ -1628,6 +1691,10 @@ export type Database = {
Args: { target_account_id: string }
Returns: boolean
}
is_company_admin: {
Args: { account_slug: string }
Returns: boolean
}
is_mfa_compliant: {
Args: Record<PropertyKey, never>
Returns: boolean