Merge pull request #105 from MR-medreport/MED-97

feat(MED-97): create company benefits tables; company, superadmin view fixes
This commit is contained in:
2025-09-24 12:57:42 +03:00
committed by GitHub
36 changed files with 900 additions and 209 deletions

View File

@@ -0,0 +1,27 @@
import { AppLogo } from '@kit/shared/components/app-logo';
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
import { SIDEBAR_WIDTH_PROPERTY } from '@/packages/ui/src/shadcn/constants';
import type { UserWorkspace } from '../../home/(user)/_lib/server/load-user-workspace';
export function AdminMenuNavigation(props: {
workspace: UserWorkspace;
}) {
const { accounts } = props.workspace;
return (
<div className={'flex w-full flex-1 items-center justify-between gap-3'}>
<div className={`flex items-center ${SIDEBAR_WIDTH_PROPERTY}`}>
<AppLogo href={'/admin'} />
</div>
<div className="flex items-center justify-end gap-3">
<div>
<ProfileAccountDropdownContainer
accounts={accounts}
/>
</div>
</div>
</div>
);
}

View File

@@ -1,72 +1,25 @@
'use client'; import { adminNavigationConfig } from '@kit/shared/config';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
import { LayoutDashboard, Users } from 'lucide-react';
import { AppLogo } from '@kit/shared/components/app-logo';
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader, SidebarHeader,
SidebarMenu, SidebarNavigation,
SidebarMenuButton,
useSidebar,
} from '@kit/ui/shadcn-sidebar'; } from '@kit/ui/shadcn-sidebar';
export function AdminSidebar({ export function AdminSidebar() {
accounts, const collapsible = adminNavigationConfig.sidebarCollapsedStyle;
}: {
accounts: UserWorkspace['accounts'];
}) {
const path = usePathname();
const { open } = useSidebar();
return ( return (
<Sidebar collapsible="icon"> <Sidebar collapsible={collapsible}>
<SidebarHeader className={'m-2'}> <SidebarHeader className="h-24 justify-center">
<AppLogo href={'/admin'} className="max-w-full" compact={!open} /> <div className="mt-24 flex items-center">
<h5>Superadmin</h5>
</div>
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
<SidebarGroup> <SidebarNavigation config={adminNavigationConfig} />
<SidebarGroupLabel>Super Admin</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuButton isActive={path === '/admin'} asChild>
<Link className={'flex gap-2.5'} href={'/admin'}>
<LayoutDashboard className={'h-4'} />
<span>Dashboard</span>
</Link>
</SidebarMenuButton>
<SidebarMenuButton
isActive={path.includes('/admin/accounts')}
asChild
>
<Link
className={'flex size-full gap-2.5'}
href={'/admin/accounts'}
>
<Users className={'h-4'} />
<span>Accounts</span>
</Link>
</SidebarMenuButton>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent> </SidebarContent>
<SidebarFooter>
<ProfileAccountDropdownContainer accounts={accounts} />
</SidebarFooter>
</Sidebar> </Sidebar>
); );
} }

View File

@@ -4,6 +4,7 @@ import { AdminAccountPage } from '@kit/admin/components/admin-account-page';
import { AdminGuard } from '@kit/admin/components/admin-guard'; import { AdminGuard } from '@kit/admin/components/admin-guard';
import { getAccount } from '~/lib/services/account.service'; import { getAccount } from '~/lib/services/account.service';
import { withI18n } from '~/lib/i18n/with-i18n';
interface Params { interface Params {
params: Promise<{ params: Promise<{
@@ -27,6 +28,6 @@ async function AccountPage(props: Params) {
return <AdminAccountPage account={account} />; return <AdminAccountPage account={account} />;
} }
export default AdminGuard(AccountPage); export default withI18n(AdminGuard(AccountPage));
const loadAccount = cache(getAccount); const loadAccount = cache(getAccount);

View File

@@ -8,6 +8,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { PageBody, PageHeader } from '@kit/ui/page'; import { PageBody, PageHeader } from '@kit/ui/page';
import { withI18n } from '~/lib/i18n/with-i18n';
interface SearchParams { interface SearchParams {
page?: string; page?: string;
@@ -30,7 +31,7 @@ async function AccountsPage(props: AdminAccountsPageProps) {
return ( return (
<> <>
<PageHeader description={<AppBreadcrumbs />}> <PageHeader description={<AppBreadcrumbs />} title="Accounts">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<AdminCreateUserDialog> <AdminCreateUserDialog>
<Button data-test="admin-create-user-button"> <Button data-test="admin-create-user-button">
@@ -84,4 +85,4 @@ async function AccountsPage(props: AdminAccountsPageProps) {
); );
} }
export default AdminGuard(AccountsPage); export default withI18n(AdminGuard(AccountsPage));

View File

@@ -6,6 +6,7 @@ import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar'; import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
import { AdminSidebar } from '~/admin/_components/admin-sidebar'; import { AdminSidebar } from '~/admin/_components/admin-sidebar';
import { AdminMenuNavigation } from '~/admin/_components/admin-menu-navigation';
import { AdminMobileNavigation } from '~/admin/_components/mobile-navigation'; import { AdminMobileNavigation } from '~/admin/_components/mobile-navigation';
import { loadUserWorkspace } from '../home/(user)/_lib/server/load-user-workspace'; import { loadUserWorkspace } from '../home/(user)/_lib/server/load-user-workspace';
@@ -21,19 +22,24 @@ export default function AdminLayout(props: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace()); const workspace = use(loadUserWorkspace());
return ( return (
<SidebarProvider defaultOpen={state.open}> <Page style={'header'}>
<Page style={'sidebar'}>
<PageNavigation> <PageNavigation>
<AdminSidebar accounts={workspace.accounts} /> <AdminMenuNavigation workspace={workspace} />
</PageNavigation> </PageNavigation>
<PageMobileNavigation> <PageMobileNavigation>
<AdminMobileNavigation /> <AdminMobileNavigation />
</PageMobileNavigation> </PageMobileNavigation>
<SidebarProvider defaultOpen={state.open}>
<Page style={'sidebar'}>
<PageNavigation>
<AdminSidebar />
</PageNavigation>
{props.children} {props.children}
</Page> </Page>
</SidebarProvider> </SidebarProvider>
</Page>
); );
} }

View File

@@ -1,11 +1,12 @@
import { AdminDashboard } from '@kit/admin/components/admin-dashboard'; import { AdminDashboard } from '@kit/admin/components/admin-dashboard';
import { AdminGuard } from '@kit/admin/components/admin-guard'; import { AdminGuard } from '@kit/admin/components/admin-guard';
import { PageBody, PageHeader } from '@kit/ui/page'; import { PageBody, PageHeader } from '@kit/ui/page';
import { withI18n } from '~/lib/i18n/with-i18n';
function AdminPage() { function AdminPage() {
return ( return (
<> <>
<PageHeader description={`Super Admin`} /> <PageHeader title={`Super Admin`} />
<PageBody> <PageBody>
<AdminDashboard /> <AdminDashboard />
@@ -14,4 +15,4 @@ function AdminPage() {
); );
} }
export default AdminGuard(AdminPage); export default withI18n(AdminGuard(AdminPage));

View File

@@ -1,12 +0,0 @@
import { z } from 'zod';
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
import { SidebarNavigation } from '@kit/ui/shadcn-sidebar';
export function TeamAccountLayoutSidebarNavigation({
config,
}: React.PropsWithChildren<{
config: z.infer<typeof NavigationConfigSchema>;
}>) {
return <SidebarNavigation config={config} />;
}

View File

@@ -1,20 +1,12 @@
import type { User } from '@supabase/supabase-js';
import { ApplicationRole } from '@kit/accounts/types/accounts'; import { ApplicationRole } from '@kit/accounts/types/accounts';
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
import { getTeamAccountSidebarConfig } from '@kit/shared/config'; import { getTeamAccountSidebarConfig } from '@kit/shared/config';
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
SidebarFooter,
SidebarHeader, SidebarHeader,
SidebarNavigation,
} from '@kit/ui/shadcn-sidebar'; } from '@kit/ui/shadcn-sidebar';
import { TeamAccountNotifications } from '~/home/[account]/_components/team-account-notifications';
import { TeamAccountAccountsSelector } from '../_components/team-account-accounts-selector';
import { TeamAccountLayoutSidebarNavigation } from './team-account-layout-sidebar-navigation';
type AccountModel = { type AccountModel = {
label: string | null; label: string | null;
value: string | null; value: string | null;
@@ -26,14 +18,12 @@ export function TeamAccountLayoutSidebar(props: {
account: string; account: string;
accountId: string; accountId: string;
accounts: AccountModel[]; accounts: AccountModel[];
user: User;
}) { }) {
return ( return (
<SidebarContainer <SidebarContainer
account={props.account} account={props.account}
accountId={props.accountId} accountId={props.accountId}
accounts={props.accounts} accounts={props.accounts}
user={props.user}
/> />
); );
} }
@@ -42,45 +32,27 @@ function SidebarContainer(props: {
account: string; account: string;
accountId: string; accountId: string;
accounts: AccountModel[]; accounts: AccountModel[];
user: User;
}) { }) {
const { account, accounts, user } = props; const { account, accounts } = props;
const userId = user.id;
const config = getTeamAccountSidebarConfig(account); const config = getTeamAccountSidebarConfig(account);
const collapsible = config.sidebarCollapsedStyle; const collapsible = config.sidebarCollapsedStyle;
const selectedAccount = accounts.find(({ value }) => value === account);
const accountName = selectedAccount?.label || account;
return ( return (
<Sidebar collapsible={collapsible}> <Sidebar collapsible={collapsible}>
<SidebarHeader className="h-16 justify-center"> <SidebarHeader className="h-24 justify-center">
<div className="flex items-center justify-between gap-x-3"> <div className="mt-24 flex items-center">
<TeamAccountAccountsSelector <h5>{accountName}</h5>
userId={userId}
selectedAccount={account}
accounts={accounts}
/>
<div className="group-data-[minimized=true]:hidden">
<TeamAccountNotifications
userId={userId}
accountId={props.accountId}
/>
</div>
</div> </div>
</SidebarHeader> </SidebarHeader>
<SidebarContent className={`mt-5 h-[calc(100%-160px)] overflow-y-auto`}> <SidebarContent>
<TeamAccountLayoutSidebarNavigation config={config} /> <SidebarNavigation config={config} />
</SidebarContent> </SidebarContent>
<SidebarFooter>
<SidebarContent>
<ProfileAccountDropdownContainer
user={props.user}
accounts={accounts}
/>
</SidebarContent>
</SidebarFooter>
</Sidebar> </Sidebar>
); );
} }

View File

@@ -20,7 +20,7 @@ const HealthBenefitFields = () => {
return ( return (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<FormField <FormField
name="occurance" name="occurrence"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
@@ -30,20 +30,20 @@ const HealthBenefitFields = () => {
<Select {...field} onValueChange={field.onChange}> <Select {...field} onValueChange={field.onChange}>
<SelectTrigger> <SelectTrigger>
<SelectValue <SelectValue
placeholder={<Trans i18nKey="common:formField:occurance" />} placeholder={<Trans i18nKey="common:formField:occurrence" />}
/> />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
<SelectItem value="yearly"> <SelectItem value="yearly">
<Trans i18nKey="billing:occurance.yearly" /> <Trans i18nKey="billing:occurrence.yearly" />
</SelectItem> </SelectItem>
<SelectItem value="quarterly"> <SelectItem value="quarterly">
<Trans i18nKey="billing:occurance.quarterly" /> <Trans i18nKey="billing:occurrence.quarterly" />
</SelectItem> </SelectItem>
<SelectItem value="monthly"> <SelectItem value="monthly">
<Trans i18nKey="billing:occurance.monthly" /> <Trans i18nKey="billing:occurrence.monthly" />
</SelectItem> </SelectItem>
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>

View File

@@ -39,13 +39,13 @@ const HealthBenefitForm = ({
resolver: zodResolver(UpdateHealthBenefitSchema), resolver: zodResolver(UpdateHealthBenefitSchema),
mode: 'onChange', mode: 'onChange',
defaultValues: { defaultValues: {
occurance: currentCompanyParams.benefit_occurance || 'yearly', occurrence: currentCompanyParams.benefit_occurance || 'yearly',
amount: currentCompanyParams.benefit_amount || 0, amount: currentCompanyParams.benefit_amount || 0,
}, },
}); });
const isDirty = form.formState.isDirty; const isDirty = form.formState.isDirty;
const onSubmit = (data: { occurance: string; amount: number }) => { const onSubmit = (data: { occurrence: string; amount: number }) => {
const promise = async () => { const promise = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
@@ -53,7 +53,7 @@ const HealthBenefitForm = ({
setCurrentCompanyParams((prev) => ({ setCurrentCompanyParams((prev) => ({
...prev, ...prev,
benefit_amount: data.amount, benefit_amount: data.amount,
benefit_occurance: data.occurance, benefit_occurance: data.occurrence,
})); }));
} finally { } finally {
form.reset(data); form.reset(data);
@@ -78,7 +78,7 @@ const HealthBenefitForm = ({
<h4> <h4>
<Trans <Trans
i18nKey="billing:pageTitle" i18nKey="billing:pageTitle"
values={{ companyName: account.slug }} values={{ companyName: account.name }}
/> />
</h4> </h4>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">

View File

@@ -15,7 +15,6 @@ import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
import { withI18n } from '~/lib/i18n/with-i18n'; import { withI18n } from '~/lib/i18n/with-i18n';
// local imports
import { TeamAccountLayoutMobileNavigation } from './_components/team-account-layout-mobile-navigation'; import { TeamAccountLayoutMobileNavigation } from './_components/team-account-layout-mobile-navigation';
import { TeamAccountLayoutSidebar } from './_components/team-account-layout-sidebar'; import { TeamAccountLayoutSidebar } from './_components/team-account-layout-sidebar';
import { TeamAccountNavigationMenu } from './_components/team-account-navigation-menu'; import { TeamAccountNavigationMenu } from './_components/team-account-navigation-menu';
@@ -57,13 +56,12 @@ function SidebarLayout({
return ( return (
<TeamAccountWorkspaceContextProvider value={data}> <TeamAccountWorkspaceContextProvider value={data}>
<SidebarProvider defaultOpen={state.open}> <SidebarProvider defaultOpen={state.open}>
<Page style={'sidebar'}> <Page style={'header'}>
<PageNavigation> <PageNavigation>
<TeamAccountLayoutSidebar <TeamAccountLayoutSidebar
account={account} account={account}
accountId={data.account.id} accountId={data.account.id}
accounts={accounts} accounts={accounts}
user={data.user}
/> />
</PageNavigation> </PageNavigation>
@@ -129,23 +127,8 @@ function HeaderLayout({
account={account} account={account}
accountId={data.account.id} accountId={data.account.id}
accounts={accounts} accounts={accounts}
user={data.user}
/> />
</PageNavigation> </PageNavigation>
<PageMobileNavigation
className={'flex items-center justify-between'}
>
<AppLogo href={pathsConfig.app.home} />
<div className={'flex space-x-4'}>
<TeamAccountLayoutMobileNavigation
userId={data.user.id}
accounts={accounts}
account={account}
/>
</div>
</PageMobileNavigation>
{children} {children}
</Page> </Page>
</SidebarProvider> </SidebarProvider>

View File

@@ -22,7 +22,6 @@ import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n'; import { withI18n } from '~/lib/i18n/with-i18n';
// local imports
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header'; import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
import { loadMembersPageData } from './_lib/server/members-page.loader'; import { loadMembersPageData } from './_lib/server/members-page.loader';
@@ -56,8 +55,7 @@ async function TeamAccountMembersPage({ params }: TeamAccountMembersPageProps) {
<> <>
<TeamAccountLayoutPageHeader <TeamAccountLayoutPageHeader
title={<Trans i18nKey={'common:routes.members'} />} title={<Trans i18nKey={'common:routes.members'} />}
description={<AppBreadcrumbs />} description={<AppBreadcrumbs values={{ [account.slug]: account.name }}/>}
account={account.slug}
/> />
<PageBody> <PageBody>

View File

@@ -8,7 +8,6 @@ import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
// local imports
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header'; import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
export const generateMetadata = async () => { export const generateMetadata = async () => {
@@ -48,9 +47,8 @@ async function TeamAccountSettingsPage(props: TeamAccountSettingsPageProps) {
return ( return (
<> <>
<TeamAccountLayoutPageHeader <TeamAccountLayoutPageHeader
account={account.slug}
title={<Trans i18nKey={'teams:settings.pageTitle'} />} title={<Trans i18nKey={'teams:settings.pageTitle'} />}
description={<AppBreadcrumbs />} description={<AppBreadcrumbs values={{ [account.slug]: account.name }} />}
/> />
<PageBody> <PageBody>

View File

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

View File

@@ -170,6 +170,7 @@ async function TeamAccountPage(props: {
<> <>
<PageHeader <PageHeader
className="border-b" className="border-b"
title={'Account'}
description={ description={
<AppBreadcrumbs <AppBreadcrumbs
values={{ values={{

View File

@@ -18,8 +18,8 @@ export async function AdminDashboard() {
' xl:grid-cols-4' ' xl:grid-cols-4'
} }
> >
<Card> <Card className="flex flex-col">
<CardHeader> <CardHeader className="flex-1">
<CardTitle>Users</CardTitle> <CardTitle>Users</CardTitle>
<CardDescription> <CardDescription>
@@ -34,8 +34,8 @@ export async function AdminDashboard() {
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card className="flex flex-col">
<CardHeader> <CardHeader className="flex-1">
<CardTitle>Company Accounts</CardTitle> <CardTitle>Company Accounts</CardTitle>
<CardDescription> <CardDescription>

View File

@@ -7,6 +7,7 @@ import { ColumnDef } from '@tanstack/react-table';
import { Database } from '@kit/supabase/database'; import { Database } from '@kit/supabase/database';
import { DataTable } from '@kit/ui/enhanced-data-table'; import { DataTable } from '@kit/ui/enhanced-data-table';
import { ProfileAvatar } from '@kit/ui/profile-avatar'; import { ProfileAvatar } from '@kit/ui/profile-avatar';
import { formatDateAndTime } from '@kit/shared/utils';
type Memberships = type Memberships =
Database['medreport']['Functions']['get_account_members']['Returns'][number]; Database['medreport']['Functions']['get_account_members']['Returns'][number];
@@ -17,10 +18,6 @@ export function AdminMembersTable(props: { members: Memberships[] }) {
function getColumns(): ColumnDef<Memberships>[] { function getColumns(): ColumnDef<Memberships>[] {
return [ return [
{
header: 'User ID',
accessorKey: 'user_id',
},
{ {
header: 'Name', header: 'Name',
cell: ({ row }) => { cell: ({ row }) => {
@@ -58,10 +55,16 @@ function getColumns(): ColumnDef<Memberships>[] {
{ {
header: 'Created At', header: 'Created At',
accessorKey: 'created_at', accessorKey: 'created_at',
cell: ({ row }) => {
return formatDateAndTime(row.original.created_at);
},
}, },
{ {
header: 'Updated At', header: 'Updated At',
accessorKey: 'updated_at', accessorKey: 'updated_at',
cell: ({ row }) => {
return formatDateAndTime(row.original.updated_at);
},
}, },
]; ];
} }

View File

@@ -267,7 +267,7 @@ export class TeamAccountsApi {
.schema('medreport') .schema('medreport')
.from('company_params') .from('company_params')
.update({ .update({
benefit_occurance: data.occurance, benefit_occurance: data.occurrence,
benefit_amount: data.amount, benefit_amount: data.amount,
updated_at: new Date().toISOString(), updated_at: new Date().toISOString(),
}) })

View File

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

View File

@@ -0,0 +1,33 @@
import { LayoutDashboard, Users } from 'lucide-react';
import { z } from 'zod';
import { pathsConfig } from '@kit/shared/config';
import { NavigationConfigSchema } from '@kit/ui/navigation-schema';
const iconClasses = 'w-4 stroke-[1.5px]';
const routes = [
{
children: [
{
label: 'Dashboard',
path: pathsConfig.app.admin,
Icon: <LayoutDashboard className={iconClasses} />,
end: true,
},
{
label: 'Accounts',
path: `${pathsConfig.app.admin}/accounts`,
Icon: <Users className={iconClasses} />,
end: true,
},
],
},
] satisfies z.infer<typeof NavigationConfigSchema>['routes'];
export const adminNavigationConfig = NavigationConfigSchema.parse({
routes,
style: 'custom',
sidebarCollapsed: false,
sidebarCollapsedStyle: 'icon',
});

View File

@@ -1,6 +1,7 @@
import appConfig from './app.config'; import appConfig from './app.config';
import authConfig from './auth.config'; import authConfig from './auth.config';
import billingConfig from './billing.config'; import billingConfig from './billing.config';
import { adminNavigationConfig } from './admin-navigation.config';
import { import {
DynamicAuthConfig, DynamicAuthConfig,
getCachedAuthConfig, getCachedAuthConfig,
@@ -15,6 +16,7 @@ import {
} from './team-account-navigation.config'; } from './team-account-navigation.config';
export { export {
adminNavigationConfig,
appConfig, appConfig,
authConfig, authConfig,
billingConfig, billingConfig,

View File

@@ -7,7 +7,6 @@ const iconClasses = 'w-4';
const getRoutes = (account: string) => [ const getRoutes = (account: string) => [
{ {
label: 'common:routes.application',
children: [ children: [
{ {
label: 'common:routes.dashboard', label: 'common:routes.dashboard',
@@ -15,12 +14,6 @@ const getRoutes = (account: string) => [
Icon: <LayoutDashboard className={iconClasses} />, Icon: <LayoutDashboard className={iconClasses} />,
end: true, end: true,
}, },
],
},
{
label: 'common:routes.settings',
collapsible: false,
children: [
{ {
label: 'common:routes.settings', label: 'common:routes.settings',
path: createPath(pathsConfig.app.accountSettings, account), path: createPath(pathsConfig.app.accountSettings, account),

View File

@@ -41,15 +41,19 @@ const CardHeader: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
); );
CardHeader.displayName = 'CardHeader'; CardHeader.displayName = 'CardHeader';
const CardTitle: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = ({ const CardTitle: React.FC<React.HTMLAttributes<HTMLHeadingElement> & { size?: 'h3' | 'h4' | 'h5' }> = ({
className, className,
size = 'h3',
...props ...props
}) => ( }) => {
<h3 const Component = size;
return (
<Component
className={cn('leading-none font-semibold tracking-tight', className)} className={cn('leading-none font-semibold tracking-tight', className)}
{...props} {...props}
/> />
); );
};
CardTitle.displayName = 'CardTitle'; CardTitle.displayName = 'CardTitle';
const CardDescription: React.FC<React.HTMLAttributes<HTMLParagraphElement>> = ({ const CardDescription: React.FC<React.HTMLAttributes<HTMLParagraphElement>> = ({

View File

@@ -119,5 +119,25 @@
}, },
"cart": { "cart": {
"label": "Cart ({{ items }})" "label": "Cart ({{ items }})"
},
"pageTitle": "{{companyName}} budget",
"description": "Configure company budget..",
"saveChanges": "Save changes",
"healthBenefitForm": {
"title": "Health benefit form",
"description": "Company health benefit for employees",
"info": "* Taxes are added to the prices"
},
"occurrence": {
"yearly": "Yearly",
"quarterly": "Quarterly",
"monthly": "Monthly"
},
"expensesOverview": {
"title": "Expenses overview 2025",
"monthly": "Expense per employee per month *",
"yearly": "Maximum expense per employee per year *",
"total": "Maximum expense per {{employeeCount}} employee(s) per year *",
"sum": "Total"
} }
} }

View File

@@ -80,10 +80,11 @@
"dashboard": "Dashboard", "dashboard": "Dashboard",
"settings": "Settings", "settings": "Settings",
"profile": "Profile", "profile": "Profile",
"application": "Application",
"pickTime": "Pick time", "pickTime": "Pick time",
"preferences": "Preferences", "preferences": "Preferences",
"security": "Security" "security": "Security",
"admin": "Admin",
"accounts": "Accounts"
}, },
"roles": { "roles": {
"owner": { "owner": {
@@ -124,7 +125,7 @@
"city": "City", "city": "City",
"weight": "Weight", "weight": "Weight",
"height": "Height", "height": "Height",
"occurance": "Support frequency", "occurrence": "Support frequency",
"amount": "Amount", "amount": "Amount",
"selectDate": "Select date" "selectDate": "Select date"
}, },

View File

@@ -1,9 +1,9 @@
{ {
"home": { "home": {
"pageTitle": "Dashboard" "pageTitle": "Company Dashboard"
}, },
"settings": { "settings": {
"pageTitle": "Settings", "pageTitle": "Company Settings",
"pageDescription": "Manage your Company details", "pageDescription": "Manage your Company details",
"teamLogo": "Company Logo", "teamLogo": "Company Logo",
"teamLogoDescription": "Update your company's logo to make it easier to identify", "teamLogoDescription": "Update your company's logo to make it easier to identify",
@@ -13,10 +13,10 @@
"dangerZoneDescription": "This section contains actions that are irreversible" "dangerZoneDescription": "This section contains actions that are irreversible"
}, },
"members": { "members": {
"pageTitle": "Members" "pageTitle": "Company Members"
}, },
"billing": { "billing": {
"pageTitle": "Billing" "pageTitle": "Company Billing"
}, },
"yourTeams": "Your Companies ({{teamsCount}})", "yourTeams": "Your Companies ({{teamsCount}})",
"createTeam": "Create a Company", "createTeam": "Create a Company",

View File

@@ -121,16 +121,16 @@
"label": "Cart ({{ items }})" "label": "Cart ({{ items }})"
}, },
"pageTitle": "{{companyName}} eelarve", "pageTitle": "{{companyName}} eelarve",
"description": "Vali kalendrist sobiv kuupäev ja broneeri endale vastuvõtuaeg.", "description": "Muuda ettevõtte eelarve seadistusi.",
"saveChanges": "Salvesta muudatused", "saveChanges": "Salvesta muudatused",
"healthBenefitForm": { "healthBenefitForm": {
"title": "Tervisetoetuse vorm", "title": "Tervisetoetuse vorm",
"description": "Ettevõtte Tervisekassa toetus töötajale", "description": "Ettevõtte Tervisekassa toetus töötajale",
"info": "* Hindadele lisanduvad riigipoolsed maksud" "info": "* Hindadele lisanduvad riigipoolsed maksud"
}, },
"occurance": { "occurrence": {
"yearly": "Kord aastas", "yearly": "Kord aastas",
"quarterly": "kord kvartalis", "quarterly": "Kord kvartalis",
"monthly": "Kord kuus" "monthly": "Kord kuus"
}, },
"expensesOverview": { "expensesOverview": {

View File

@@ -80,10 +80,11 @@
"dashboard": "Ülevaade", "dashboard": "Ülevaade",
"settings": "Seaded", "settings": "Seaded",
"profile": "Profiil", "profile": "Profiil",
"application": "Rakendus",
"pickTime": "Vali aeg", "pickTime": "Vali aeg",
"preferences": "Eelistused", "preferences": "Eelistused",
"security": "Turvalisus" "security": "Turvalisus",
"admin": "Admin",
"accounts": "Kontod"
}, },
"roles": { "roles": {
"owner": { "owner": {
@@ -124,7 +125,7 @@
"city": "Linn", "city": "Linn",
"weight": "Kaal", "weight": "Kaal",
"height": "Pikkus", "height": "Pikkus",
"occurance": "Toetuse sagedus", "occurrence": "Toetuse sagedus",
"amount": "Summa", "amount": "Summa",
"selectDate": "Vali kuupäev" "selectDate": "Vali kuupäev"
}, },

View File

@@ -1,6 +1,6 @@
{ {
"home": { "home": {
"pageTitle": "Ülevaade", "pageTitle": "Ettevõtte ülevaade",
"headerTitle": "{{companyName}} Tervisekassa kokkuvõte", "headerTitle": "{{companyName}} Tervisekassa kokkuvõte",
"healthDetails": "Ettevõtte terviseandmed", "healthDetails": "Ettevõtte terviseandmed",
"membersSettingsButtonTitle": "Halda töötajaid", "membersSettingsButtonTitle": "Halda töötajaid",
@@ -9,7 +9,7 @@
"membersBillingButtonDescription": "Vali kuidas soovid eelarvet töötajate vahel jagada." "membersBillingButtonDescription": "Vali kuidas soovid eelarvet töötajate vahel jagada."
}, },
"settings": { "settings": {
"pageTitle": "Seaded", "pageTitle": "Ettevõtte seaded",
"pageDescription": "Halda oma ettevõtte andmeid", "pageDescription": "Halda oma ettevõtte andmeid",
"teamLogo": "Ettevõtte logo", "teamLogo": "Ettevõtte logo",
"teamLogoDescription": "Uuenda oma ettevõtte logo, et seda oleks lihtsam tuvastada", "teamLogoDescription": "Uuenda oma ettevõtte logo, et seda oleks lihtsam tuvastada",
@@ -19,10 +19,10 @@
"dangerZoneDescription": "See osa sisaldab pöördumatuid toiminguid" "dangerZoneDescription": "See osa sisaldab pöördumatuid toiminguid"
}, },
"members": { "members": {
"pageTitle": "Töötajad" "pageTitle": "Ettevõtte töötajad"
}, },
"billing": { "billing": {
"pageTitle": "Arveldamine" "pageTitle": "Ettevõtte arveldamine"
}, },
"benefitStatistics": { "benefitStatistics": {
"budget": { "budget": {

View File

@@ -121,14 +121,14 @@
"label": "Корзина ({{ items }})" "label": "Корзина ({{ items }})"
}, },
"pageTitle": "Бюджет {{companyName}}", "pageTitle": "Бюджет {{companyName}}",
"description": "Выберите подходящую дату в календаре и запишитесь на прием.", "description": "Измените настройки бюджета компании.",
"saveChanges": "Сохранить изменения", "saveChanges": "Сохранить изменения",
"healthBenefitForm": { "healthBenefitForm": {
"title": "Форма здоровья", "title": "Форма здоровья",
"description": "Поддержка сотрудника из корпоративного фонда здоровья", "description": "Поддержка сотрудника из корпоративного фонда здоровья",
"info": "* К ценам добавляются государственные налоги" "info": "* К ценам добавляются государственные налоги"
}, },
"occurance": { "occurrence": {
"yearly": "Раз в год", "yearly": "Раз в год",
"quarterly": "Раз в квартал", "quarterly": "Раз в квартал",
"monthly": "Раз в месяц" "monthly": "Раз в месяц"

View File

@@ -80,10 +80,11 @@
"dashboard": "Обзор", "dashboard": "Обзор",
"settings": "Настройки", "settings": "Настройки",
"profile": "Профиль", "profile": "Профиль",
"application": "Приложение",
"pickTime": "Выбрать время", "pickTime": "Выбрать время",
"preferences": "Предпочтения", "preferences": "Предпочтения",
"security": "Безопасность" "security": "Безопасность",
"admin": "Админ",
"accounts": "Аккаунты"
}, },
"roles": { "roles": {
"owner": { "owner": {
@@ -124,7 +125,7 @@
"city": "Город", "city": "Город",
"weight": "Вес", "weight": "Вес",
"height": "Рост", "height": "Рост",
"occurance": "Частота поддержки", "occurrence": "Частота поддержки",
"amount": "Сумма", "amount": "Сумма",
"selectDate": "Выберите дату" "selectDate": "Выберите дату"
}, },

View File

@@ -1,6 +1,6 @@
{ {
"home": { "home": {
"pageTitle": "Обзор", "pageTitle": "Обзор компании",
"headerTitle": "Обзор Tervisekassa {{companyName}}", "headerTitle": "Обзор Tervisekassa {{companyName}}",
"healthDetails": "Данные о здоровье компании", "healthDetails": "Данные о здоровье компании",
"membersSettingsButtonTitle": "Управление сотрудниками", "membersSettingsButtonTitle": "Управление сотрудниками",
@@ -9,7 +9,7 @@
"membersBillingButtonDescription": "Выберите, как распределять бюджет между сотрудниками." "membersBillingButtonDescription": "Выберите, как распределять бюджет между сотрудниками."
}, },
"settings": { "settings": {
"pageTitle": "Настройки", "pageTitle": "Настройки компании",
"pageDescription": "Управление данными вашей компании", "pageDescription": "Управление данными вашей компании",
"teamLogo": "Логотип компании", "teamLogo": "Логотип компании",
"teamLogoDescription": "Обновите логотип вашей компании для упрощения идентификации", "teamLogoDescription": "Обновите логотип вашей компании для упрощения идентификации",
@@ -19,10 +19,10 @@
"dangerZoneDescription": "Этот раздел содержит действия, которые невозможно отменить" "dangerZoneDescription": "Этот раздел содержит действия, которые невозможно отменить"
}, },
"members": { "members": {
"pageTitle": "Сотрудники" "pageTitle": "Сотрудники компании"
}, },
"billing": { "billing": {
"pageTitle": "Биллинг" "pageTitle": "Биллинг компании"
}, },
"benefitStatistics": { "benefitStatistics": {
"budget": { "budget": {

View File

@@ -0,0 +1,239 @@
-- Create account_balance_entries table to track individual account balances
create table "medreport"."account_balance_entries" (
"id" uuid not null default gen_random_uuid(),
"account_id" uuid not null,
"amount" numeric not null,
"entry_type" text not null, -- 'benefit', 'purchase', 'refund', etc.
"description" text,
"source_company_id" uuid, -- Company that provided the benefit
"reference_id" text, -- Reference to related record (e.g., analysis order ID)
"created_at" timestamp with time zone not null default now(),
"created_by" uuid,
"expires_at" timestamp with time zone, -- When the balance expires (for benefits)
"is_active" boolean not null default true
);
-- Add constraints
alter table "medreport"."account_balance_entries" add constraint "account_balance_entries_pkey" primary key (id);
alter table "medreport"."account_balance_entries" add constraint "account_balance_entries_account_id_fkey"
foreign key (account_id) references medreport.accounts(id) on delete cascade;
alter table "medreport"."account_balance_entries" add constraint "account_balance_entries_source_company_id_fkey"
foreign key (source_company_id) references medreport.accounts(id) on delete set null;
-- Add indexes for performance
create index "ix_account_balance_entries_account_id" on "medreport"."account_balance_entries" (account_id);
create index "ix_account_balance_entries_entry_type" on "medreport"."account_balance_entries" (entry_type);
create index "ix_account_balance_entries_created_at" on "medreport"."account_balance_entries" (created_at);
create index "ix_account_balance_entries_active" on "medreport"."account_balance_entries" (is_active) where is_active = true;
-- Enable RLS
alter table "medreport"."account_balance_entries" enable row level security;
-- Create RLS policies
create policy "Users can view their own balance entries"
on "medreport"."account_balance_entries"
for select
using (
account_id in (
select account_id
from medreport.accounts_memberships
where user_id = auth.uid()
)
);
create policy "Service role can manage all balance entries"
on "medreport"."account_balance_entries"
for all
to service_role
using (true)
with check (true);
-- Grant permissions
grant select, insert, update, delete on table "medreport"."account_balance_entries" to authenticated, service_role;
-- Create function to get account balance
create or replace function medreport.get_account_balance(p_account_id uuid)
returns numeric
language plpgsql
security definer
as $$
declare
total_balance numeric := 0;
begin
select coalesce(sum(amount), 0)
into total_balance
from medreport.account_balance_entries
where account_id = p_account_id
and is_active = true
and (expires_at is null or expires_at > now());
return total_balance;
end;
$$;
-- Grant execute permission
grant execute on function medreport.get_account_balance(uuid) to authenticated, service_role;
-- Create function to distribute health benefits to all company members
create or replace function medreport.distribute_health_benefits(
p_company_id uuid,
p_benefit_amount numeric,
p_benefit_occurrence text default 'yearly'
)
returns void
language plpgsql
security definer
as $$
declare
member_record record;
benefit_entry_id uuid;
expires_date timestamp with time zone;
begin
-- Calculate expiration date based on occurrence
case p_benefit_occurrence
when 'yearly' then
expires_date := now() + interval '1 year';
when 'monthly' then
expires_date := now() + interval '1 month';
when 'quarterly' then
expires_date := now() + interval '3 months';
else
expires_date := now() + interval '1 year'; -- default to yearly
end case;
-- Get all personal accounts that are members of this company
for member_record in
select distinct a.id as personal_account_id
from medreport.accounts a
join medreport.accounts_memberships am on a.id = am.user_id
where am.account_id = p_company_id
and a.is_personal_account = true
loop
-- Insert balance entry for each personal account
insert into medreport.account_balance_entries (
account_id,
amount,
entry_type,
description,
source_company_id,
created_by,
expires_at
) values (
member_record.personal_account_id,
p_benefit_amount,
'benefit',
'Health benefit from company',
p_company_id,
auth.uid(),
expires_date
);
end loop;
end;
$$;
-- Grant execute permission
grant execute on function medreport.distribute_health_benefits(uuid, numeric, text) to authenticated, service_role;
-- Create trigger to automatically distribute benefits when company params are updated
create or replace function medreport.trigger_distribute_benefits()
returns trigger
language plpgsql
as $$
begin
-- Only distribute if benefit_amount is set and greater than 0
if new.benefit_amount is not null and new.benefit_amount > 0 then
-- Distribute benefits to all company members
perform medreport.distribute_health_benefits(
new.account_id,
new.benefit_amount,
coalesce(new.benefit_occurance, 'yearly')
);
end if;
return new;
end;
$$;
-- Create the trigger
create trigger trigger_distribute_benefits_on_update
after update on medreport.company_params
for each row
when (old.benefit_amount is distinct from new.benefit_amount or old.benefit_occurance is distinct from new.benefit_occurance)
execute function medreport.trigger_distribute_benefits();
-- Create function to consume balance (for purchases)
create or replace function medreport.consume_account_balance(
p_account_id uuid,
p_amount numeric,
p_description text,
p_reference_id text default null
)
returns boolean
language plpgsql
security definer
as $$
declare
current_balance numeric;
remaining_amount numeric := p_amount;
entry_record record;
consumed_amount numeric;
begin
-- Get current balance
current_balance := medreport.get_account_balance(p_account_id);
-- Check if sufficient balance
if current_balance < p_amount then
return false;
end if;
-- Consume balance using FIFO (First In, First Out) with expiration priority
for entry_record in
select id, amount
from medreport.account_balance_entries
where account_id = p_account_id
and is_active = true
and (expires_at is null or expires_at > now())
order by
case when expires_at is not null then expires_at else '9999-12-31'::timestamp end,
created_at
loop
if remaining_amount <= 0 then
exit;
end if;
consumed_amount := least(entry_record.amount, remaining_amount);
-- Update the entry
update medreport.account_balance_entries
set amount = amount - consumed_amount,
is_active = case when amount - consumed_amount <= 0 then false else true end
where id = entry_record.id;
remaining_amount := remaining_amount - consumed_amount;
end loop;
-- Record the consumption
insert into medreport.account_balance_entries (
account_id,
amount,
entry_type,
description,
reference_id,
created_by
) values (
p_account_id,
-p_amount,
'purchase',
p_description,
p_reference_id,
auth.uid()
);
return true;
end;
$$;
-- Grant execute permission
grant execute on function medreport.consume_account_balance(uuid, numeric, text, text) to authenticated, service_role;

View File

@@ -0,0 +1,303 @@
-- Fix the trigger to actually call the distribution function
-- First, drop the existing trigger and function
drop trigger if exists trigger_distribute_benefits_on_update on medreport.company_params;
drop function if exists medreport.trigger_distribute_benefits();
-- Create a new trigger function that actually distributes benefits
create or replace function medreport.trigger_distribute_benefits()
returns trigger
language plpgsql
as $$
begin
-- Only distribute if benefit_amount is set and greater than 0
if new.benefit_amount is not null and new.benefit_amount > 0 then
-- Distribute benefits to all company members
perform medreport.distribute_health_benefits(
new.account_id,
new.benefit_amount,
coalesce(new.benefit_occurance, 'yearly')
);
end if;
return new;
end;
$$;
-- Recreate the trigger
create trigger trigger_distribute_benefits_on_update
after update on medreport.company_params
for each row
when (old.benefit_amount is distinct from new.benefit_amount or old.benefit_occurance is distinct from new.benefit_occurance)
execute function medreport.trigger_distribute_benefits();
-- Create a table to track periodic benefit distributions
create table "medreport"."benefit_distribution_schedule" (
"id" uuid not null default gen_random_uuid(),
"company_id" uuid not null,
"benefit_amount" numeric not null,
"benefit_occurrence" text not null,
"last_distributed_at" timestamp with time zone,
"next_distribution_at" timestamp with time zone not null,
"is_active" boolean not null default true,
"created_at" timestamp with time zone not null default now(),
"updated_at" timestamp with time zone not null default now()
);
-- Add constraints
alter table "medreport"."benefit_distribution_schedule" add constraint "benefit_distribution_schedule_pkey" primary key (id);
alter table "medreport"."benefit_distribution_schedule" add constraint "benefit_distribution_schedule_company_id_fkey"
foreign key (company_id) references medreport.accounts(id) on delete cascade;
-- Add unique constraint on company_id for upsert functionality
create unique index "ix_benefit_distribution_schedule_company_id_unique"
on "medreport"."benefit_distribution_schedule" (company_id)
where is_active = true;
-- Add indexes
create index "ix_benefit_distribution_schedule_next_distribution" on "medreport"."benefit_distribution_schedule" (next_distribution_at) where is_active = true;
create index "ix_benefit_distribution_schedule_company_id" on "medreport"."benefit_distribution_schedule" (company_id);
-- Enable RLS
alter table "medreport"."benefit_distribution_schedule" enable row level security;
-- Create RLS policies
create policy "Service role can manage all distribution schedules"
on "medreport"."benefit_distribution_schedule"
for all
to service_role
using (true)
with check (true);
create policy "Users can view distribution schedules for their companies"
on "medreport"."benefit_distribution_schedule"
for select
to authenticated
using (
company_id in (
select account_id
from medreport.accounts_memberships
where user_id = auth.uid()
)
);
-- Grant permissions
grant select, insert, update, delete on table "medreport"."benefit_distribution_schedule" to service_role, authenticated;
-- Function to calculate next distribution date
create or replace function medreport.calculate_next_distribution_date(
p_occurrence text,
p_current_date timestamp with time zone default now()
)
returns timestamp with time zone
language plpgsql
as $$
declare
next_date timestamp with time zone;
current_year integer;
current_month integer;
current_quarter integer;
begin
case p_occurrence
when 'yearly' then
-- First day of next year
current_year := extract(year from p_current_date);
next_date := make_date(current_year + 1, 1, 1);
return next_date;
when 'monthly' then
-- First day of next month
current_year := extract(year from p_current_date);
current_month := extract(month from p_current_date);
if current_month = 12 then
next_date := make_date(current_year + 1, 1, 1);
else
next_date := make_date(current_year, current_month + 1, 1);
end if;
return next_date;
when 'quarterly' then
-- First day of next quarter
current_year := extract(year from p_current_date);
current_month := extract(month from p_current_date);
current_quarter := ((current_month - 1) / 3) + 1;
if current_quarter = 4 then
next_date := make_date(current_year + 1, 1, 1);
else
next_date := make_date(current_year, (current_quarter * 3) + 1, 1);
end if;
return next_date;
else
-- Default to yearly
current_year := extract(year from p_current_date);
next_date := make_date(current_year + 1, 1, 1);
return next_date;
end case;
end;
$$;
-- Grant execute permission
grant execute on function medreport.calculate_next_distribution_date(text, timestamp with time zone) to authenticated, service_role;
-- Function to process periodic benefit distributions
create or replace function medreport.process_periodic_benefit_distributions()
returns void
language plpgsql
as $$
declare
schedule_record record;
next_distribution_date timestamp with time zone;
begin
-- Get all active schedules that are due for distribution
for schedule_record in
select *
from medreport.benefit_distribution_schedule
where is_active = true
and next_distribution_at <= now()
loop
-- Distribute benefits
perform medreport.distribute_health_benefits(
schedule_record.company_id,
schedule_record.benefit_amount,
schedule_record.benefit_occurrence
);
-- Calculate next distribution date
next_distribution_date := medreport.calculate_next_distribution_date(
schedule_record.benefit_occurrence,
now()
);
-- Update the schedule
update medreport.benefit_distribution_schedule
set
last_distributed_at = now(),
next_distribution_at = next_distribution_date,
updated_at = now()
where id = schedule_record.id;
end loop;
end;
$$;
-- Grant execute permission
grant execute on function medreport.process_periodic_benefit_distributions() to service_role;
-- Function to create or update a benefit distribution schedule
create or replace function medreport.upsert_benefit_distribution_schedule(
p_company_id uuid,
p_benefit_amount numeric,
p_benefit_occurrence text
)
returns void
language plpgsql
as $$
declare
next_distribution_date timestamp with time zone;
existing_record_id uuid;
begin
-- Calculate next distribution date
next_distribution_date := medreport.calculate_next_distribution_date(p_benefit_occurrence);
-- Check if there's an existing record for this company
select id into existing_record_id
from medreport.benefit_distribution_schedule
where company_id = p_company_id
limit 1;
if existing_record_id is not null then
-- Update existing record
update medreport.benefit_distribution_schedule
set
benefit_amount = p_benefit_amount,
benefit_occurrence = p_benefit_occurrence,
next_distribution_at = next_distribution_date,
is_active = true,
updated_at = now()
where id = existing_record_id;
else
-- Insert new record
insert into medreport.benefit_distribution_schedule (
company_id,
benefit_amount,
benefit_occurrence,
next_distribution_at
) values (
p_company_id,
p_benefit_amount,
p_benefit_occurrence,
next_distribution_date
);
end if;
end;
$$;
-- Grant execute permission
grant execute on function medreport.upsert_benefit_distribution_schedule(uuid, numeric, text) to service_role, authenticated;
grant execute on function medreport.distribute_health_benefits(uuid, numeric, text) to service_role, authenticated;
grant execute on function medreport.calculate_next_distribution_date(text, timestamp with time zone) to service_role, authenticated;
-- Also grant permissions to the original functions from the first migration
grant execute on function medreport.get_account_balance(uuid) to service_role, authenticated;
grant execute on function medreport.consume_account_balance(uuid, numeric, text, text) to service_role, authenticated;
-- Update the trigger to also create/update the distribution schedule
create or replace function medreport.trigger_distribute_benefits()
returns trigger
language plpgsql
security definer
as $$
begin
-- Only distribute if benefit_amount is set and greater than 0
if new.benefit_amount is not null and new.benefit_amount > 0 then
-- Distribute benefits to all company members immediately
perform medreport.distribute_health_benefits(
new.account_id,
new.benefit_amount,
coalesce(new.benefit_occurance, 'yearly')
);
-- Create or update the distribution schedule for future distributions
perform medreport.upsert_benefit_distribution_schedule(
new.account_id,
new.benefit_amount,
coalesce(new.benefit_occurance, 'yearly')
);
else
-- If benefit_amount is 0 or null, deactivate the schedule
update medreport.benefit_distribution_schedule
set is_active = false, updated_at = now()
where company_id = new.account_id;
end if;
return new;
end;
$$;
-- Create a function to manually trigger benefit distribution (for testing)
create or replace function medreport.trigger_benefit_distribution(p_company_id uuid)
returns void
language plpgsql
as $$
declare
company_params record;
begin
-- Get company params
select * into company_params
from medreport.company_params
where account_id = p_company_id;
if found and company_params.benefit_amount > 0 then
-- Distribute benefits
perform medreport.distribute_health_benefits(
p_company_id,
company_params.benefit_amount,
coalesce(company_params.benefit_occurance, 'yearly')
);
end if;
end;
$$;
-- Grant execute permission
grant execute on function medreport.trigger_benefit_distribution(uuid) to service_role, authenticated;
grant execute on function medreport.trigger_distribute_benefits() to service_role, authenticated;

View File

@@ -0,0 +1,48 @@
-- Quick fix for RLS permissions issue
-- Run this immediately to fix the permission denied error
-- Grant execute permissions to authenticated users for all benefit distribution functions
grant execute on function medreport.get_account_balance(uuid) to authenticated;
grant execute on function medreport.distribute_health_benefits(uuid, numeric, text) to authenticated;
grant execute on function medreport.consume_account_balance(uuid, numeric, text, text) to authenticated;
grant execute on function medreport.upsert_benefit_distribution_schedule(uuid, numeric, text) to authenticated;
grant execute on function medreport.calculate_next_distribution_date(text, timestamp with time zone) to authenticated;
grant execute on function medreport.trigger_benefit_distribution(uuid) to authenticated;
grant execute on function medreport.trigger_distribute_benefits() to authenticated;
grant execute on function medreport.process_periodic_benefit_distributions() to authenticated;
-- Also ensure the trigger function has security definer
create or replace function medreport.trigger_distribute_benefits()
returns trigger
language plpgsql
security definer
as $$
begin
-- Only distribute if benefit_amount is set and greater than 0
if new.benefit_amount is not null and new.benefit_amount > 0 then
-- Distribute benefits to all company members immediately
perform medreport.distribute_health_benefits(
new.account_id,
new.benefit_amount,
coalesce(new.benefit_occurance, 'yearly')
);
-- Create or update the distribution schedule for future distributions
perform medreport.upsert_benefit_distribution_schedule(
new.account_id,
new.benefit_amount,
coalesce(new.benefit_occurance, 'yearly')
);
else
-- If benefit_amount is 0 or null, deactivate the schedule
update medreport.benefit_distribution_schedule
set is_active = false, updated_at = now()
where company_id = new.account_id;
end if;
return new;
end;
$$;
-- Grant execute permission to the updated trigger function
grant execute on function medreport.trigger_distribute_benefits() to authenticated, service_role;

View File

@@ -0,0 +1,114 @@
-- Fix upsert function and RLS permissions
-- Run this to fix the ON CONFLICT error and 403 permission error
-- 1. Fix the upsert function to not use ON CONFLICT
create or replace function medreport.upsert_benefit_distribution_schedule(
p_company_id uuid,
p_benefit_amount numeric,
p_benefit_occurrence text
)
returns void
language plpgsql
as $$
declare
next_distribution_date timestamp with time zone;
existing_record_id uuid;
begin
-- Calculate next distribution date
next_distribution_date := medreport.calculate_next_distribution_date(p_benefit_occurrence);
-- Check if there's an existing record for this company
select id into existing_record_id
from medreport.benefit_distribution_schedule
where company_id = p_company_id
limit 1;
if existing_record_id is not null then
-- Update existing record
update medreport.benefit_distribution_schedule
set
benefit_amount = p_benefit_amount,
benefit_occurrence = p_benefit_occurrence,
next_distribution_at = next_distribution_date,
is_active = true,
updated_at = now()
where id = existing_record_id;
else
-- Insert new record
insert into medreport.benefit_distribution_schedule (
company_id,
benefit_amount,
benefit_occurrence,
next_distribution_at
) values (
p_company_id,
p_benefit_amount,
p_benefit_occurrence,
next_distribution_date
);
end if;
end;
$$;
-- 2. Add RLS policy for authenticated users to read distribution schedules
create policy "Users can view distribution schedules for their companies"
on "medreport"."benefit_distribution_schedule"
for select
to authenticated
using (
company_id in (
select account_id
from medreport.accounts_memberships
where user_id = auth.uid()
)
);
-- 3. Grant permissions to authenticated users
grant select, insert, update, delete on table "medreport"."benefit_distribution_schedule" to authenticated;
-- 4. Grant execute permissions to all functions
grant execute on function medreport.get_account_balance(uuid) to authenticated;
grant execute on function medreport.distribute_health_benefits(uuid, numeric, text) to authenticated;
grant execute on function medreport.consume_account_balance(uuid, numeric, text, text) to authenticated;
grant execute on function medreport.upsert_benefit_distribution_schedule(uuid, numeric, text) to authenticated;
grant execute on function medreport.calculate_next_distribution_date(text, timestamp with time zone) to authenticated;
grant execute on function medreport.trigger_benefit_distribution(uuid) to authenticated;
grant execute on function medreport.trigger_distribute_benefits() to authenticated;
grant execute on function medreport.process_periodic_benefit_distributions() to authenticated;
-- 5. Ensure trigger function has security definer
create or replace function medreport.trigger_distribute_benefits()
returns trigger
language plpgsql
security definer
as $$
begin
-- Only distribute if benefit_amount is set and greater than 0
if new.benefit_amount is not null and new.benefit_amount > 0 then
-- Distribute benefits to all company members immediately
perform medreport.distribute_health_benefits(
new.account_id,
new.benefit_amount,
coalesce(new.benefit_occurance, 'yearly')
);
-- Create or update the distribution schedule for future distributions
perform medreport.upsert_benefit_distribution_schedule(
new.account_id,
new.benefit_amount,
coalesce(new.benefit_occurance, 'yearly')
);
else
-- If benefit_amount is 0 or null, deactivate the schedule
update medreport.benefit_distribution_schedule
set is_active = false, updated_at = now()
where company_id = new.account_id;
end if;
return new;
end;
$$;
-- 6. Grant execute permission to the updated trigger function
grant execute on function medreport.trigger_distribute_benefits() to authenticated, service_role;