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:
27
app/admin/_components/admin-menu-navigation.tsx
Normal file
27
app/admin/_components/admin-menu-navigation.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,72 +1,25 @@
|
||||
'use client';
|
||||
|
||||
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 { adminNavigationConfig } from '@kit/shared/config';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
useSidebar,
|
||||
SidebarNavigation,
|
||||
} from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
export function AdminSidebar({
|
||||
accounts,
|
||||
}: {
|
||||
accounts: UserWorkspace['accounts'];
|
||||
}) {
|
||||
const path = usePathname();
|
||||
const { open } = useSidebar();
|
||||
export function AdminSidebar() {
|
||||
const collapsible = adminNavigationConfig.sidebarCollapsedStyle;
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader className={'m-2'}>
|
||||
<AppLogo href={'/admin'} className="max-w-full" compact={!open} />
|
||||
<Sidebar collapsible={collapsible}>
|
||||
<SidebarHeader className="h-24 justify-center">
|
||||
<div className="mt-24 flex items-center">
|
||||
<h5>Superadmin</h5>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<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>
|
||||
<SidebarNavigation config={adminNavigationConfig} />
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<ProfileAccountDropdownContainer accounts={accounts} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AdminAccountPage } from '@kit/admin/components/admin-account-page';
|
||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||
|
||||
import { getAccount } from '~/lib/services/account.service';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
interface Params {
|
||||
params: Promise<{
|
||||
@@ -27,6 +28,6 @@ async function AccountPage(props: Params) {
|
||||
return <AdminAccountPage account={account} />;
|
||||
}
|
||||
|
||||
export default AdminGuard(AccountPage);
|
||||
export default withI18n(AdminGuard(AccountPage));
|
||||
|
||||
const loadAccount = cache(getAccount);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
interface SearchParams {
|
||||
page?: string;
|
||||
@@ -30,7 +31,7 @@ async function AccountsPage(props: AdminAccountsPageProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader description={<AppBreadcrumbs />}>
|
||||
<PageHeader description={<AppBreadcrumbs />} title="Accounts">
|
||||
<div className="flex justify-end gap-2">
|
||||
<AdminCreateUserDialog>
|
||||
<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));
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
|
||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { AdminSidebar } from '~/admin/_components/admin-sidebar';
|
||||
import { AdminMenuNavigation } from '~/admin/_components/admin-menu-navigation';
|
||||
import { AdminMobileNavigation } from '~/admin/_components/mobile-navigation';
|
||||
|
||||
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());
|
||||
|
||||
return (
|
||||
<SidebarProvider defaultOpen={state.open}>
|
||||
<Page style={'sidebar'}>
|
||||
<Page style={'header'}>
|
||||
<PageNavigation>
|
||||
<AdminSidebar accounts={workspace.accounts} />
|
||||
<AdminMenuNavigation workspace={workspace} />
|
||||
</PageNavigation>
|
||||
|
||||
<PageMobileNavigation>
|
||||
<AdminMobileNavigation />
|
||||
</PageMobileNavigation>
|
||||
|
||||
<SidebarProvider defaultOpen={state.open}>
|
||||
<Page style={'sidebar'}>
|
||||
<PageNavigation>
|
||||
<AdminSidebar />
|
||||
</PageNavigation>
|
||||
{props.children}
|
||||
</Page>
|
||||
</SidebarProvider>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { AdminDashboard } from '@kit/admin/components/admin-dashboard';
|
||||
import { AdminGuard } from '@kit/admin/components/admin-guard';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
function AdminPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHeader description={`Super Admin`} />
|
||||
<PageHeader title={`Super Admin`} />
|
||||
|
||||
<PageBody>
|
||||
<AdminDashboard />
|
||||
@@ -14,4 +15,4 @@ function AdminPage() {
|
||||
);
|
||||
}
|
||||
|
||||
export default AdminGuard(AdminPage);
|
||||
export default withI18n(AdminGuard(AdminPage));
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -1,20 +1,12 @@
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
|
||||
import { ApplicationRole } from '@kit/accounts/types/accounts';
|
||||
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
|
||||
import { getTeamAccountSidebarConfig } from '@kit/shared/config';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarNavigation,
|
||||
} 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 = {
|
||||
label: string | null;
|
||||
value: string | null;
|
||||
@@ -26,14 +18,12 @@ export function TeamAccountLayoutSidebar(props: {
|
||||
account: string;
|
||||
accountId: string;
|
||||
accounts: AccountModel[];
|
||||
user: User;
|
||||
}) {
|
||||
return (
|
||||
<SidebarContainer
|
||||
account={props.account}
|
||||
accountId={props.accountId}
|
||||
accounts={props.accounts}
|
||||
user={props.user}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -42,45 +32,27 @@ function SidebarContainer(props: {
|
||||
account: string;
|
||||
accountId: string;
|
||||
accounts: AccountModel[];
|
||||
user: User;
|
||||
}) {
|
||||
const { account, accounts, user } = props;
|
||||
const userId = user.id;
|
||||
const { account, accounts } = props;
|
||||
|
||||
const config = getTeamAccountSidebarConfig(account);
|
||||
const collapsible = config.sidebarCollapsedStyle;
|
||||
|
||||
const selectedAccount = accounts.find(({ value }) => value === account);
|
||||
const accountName = selectedAccount?.label || account;
|
||||
|
||||
return (
|
||||
<Sidebar collapsible={collapsible}>
|
||||
<SidebarHeader className="h-16 justify-center">
|
||||
<div className="flex items-center justify-between gap-x-3">
|
||||
<TeamAccountAccountsSelector
|
||||
userId={userId}
|
||||
selectedAccount={account}
|
||||
accounts={accounts}
|
||||
/>
|
||||
|
||||
<div className="group-data-[minimized=true]:hidden">
|
||||
<TeamAccountNotifications
|
||||
userId={userId}
|
||||
accountId={props.accountId}
|
||||
/>
|
||||
</div>
|
||||
<SidebarHeader className="h-24 justify-center">
|
||||
<div className="mt-24 flex items-center">
|
||||
<h5>{accountName}</h5>
|
||||
</div>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent className={`mt-5 h-[calc(100%-160px)] overflow-y-auto`}>
|
||||
<TeamAccountLayoutSidebarNavigation config={config} />
|
||||
<SidebarContent>
|
||||
<SidebarNavigation config={config} />
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<SidebarContent>
|
||||
<ProfileAccountDropdownContainer
|
||||
user={props.user}
|
||||
accounts={accounts}
|
||||
/>
|
||||
</SidebarContent>
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const HealthBenefitFields = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<FormField
|
||||
name="occurance"
|
||||
name="occurrence"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
@@ -30,20 +30,20 @@ const HealthBenefitFields = () => {
|
||||
<Select {...field} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={<Trans i18nKey="common:formField:occurance" />}
|
||||
placeholder={<Trans i18nKey="common:formField:occurrence" />}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="yearly">
|
||||
<Trans i18nKey="billing:occurance.yearly" />
|
||||
<Trans i18nKey="billing:occurrence.yearly" />
|
||||
</SelectItem>
|
||||
<SelectItem value="quarterly">
|
||||
<Trans i18nKey="billing:occurance.quarterly" />
|
||||
<Trans i18nKey="billing:occurrence.quarterly" />
|
||||
</SelectItem>
|
||||
<SelectItem value="monthly">
|
||||
<Trans i18nKey="billing:occurance.monthly" />
|
||||
<Trans i18nKey="billing:occurrence.monthly" />
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
|
||||
@@ -39,13 +39,13 @@ const HealthBenefitForm = ({
|
||||
resolver: zodResolver(UpdateHealthBenefitSchema),
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
occurance: currentCompanyParams.benefit_occurance || 'yearly',
|
||||
occurrence: currentCompanyParams.benefit_occurance || 'yearly',
|
||||
amount: currentCompanyParams.benefit_amount || 0,
|
||||
},
|
||||
});
|
||||
const isDirty = form.formState.isDirty;
|
||||
|
||||
const onSubmit = (data: { occurance: string; amount: number }) => {
|
||||
const onSubmit = (data: { occurrence: string; amount: number }) => {
|
||||
const promise = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -53,7 +53,7 @@ const HealthBenefitForm = ({
|
||||
setCurrentCompanyParams((prev) => ({
|
||||
...prev,
|
||||
benefit_amount: data.amount,
|
||||
benefit_occurance: data.occurance,
|
||||
benefit_occurance: data.occurrence,
|
||||
}));
|
||||
} finally {
|
||||
form.reset(data);
|
||||
@@ -78,7 +78,7 @@ const HealthBenefitForm = ({
|
||||
<h4>
|
||||
<Trans
|
||||
i18nKey="billing:pageTitle"
|
||||
values={{ companyName: account.slug }}
|
||||
values={{ companyName: account.name }}
|
||||
/>
|
||||
</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
|
||||
@@ -15,7 +15,6 @@ import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
// local imports
|
||||
import { TeamAccountLayoutMobileNavigation } from './_components/team-account-layout-mobile-navigation';
|
||||
import { TeamAccountLayoutSidebar } from './_components/team-account-layout-sidebar';
|
||||
import { TeamAccountNavigationMenu } from './_components/team-account-navigation-menu';
|
||||
@@ -57,13 +56,12 @@ function SidebarLayout({
|
||||
return (
|
||||
<TeamAccountWorkspaceContextProvider value={data}>
|
||||
<SidebarProvider defaultOpen={state.open}>
|
||||
<Page style={'sidebar'}>
|
||||
<Page style={'header'}>
|
||||
<PageNavigation>
|
||||
<TeamAccountLayoutSidebar
|
||||
account={account}
|
||||
accountId={data.account.id}
|
||||
accounts={accounts}
|
||||
user={data.user}
|
||||
/>
|
||||
</PageNavigation>
|
||||
|
||||
@@ -129,23 +127,8 @@ function HeaderLayout({
|
||||
account={account}
|
||||
accountId={data.account.id}
|
||||
accounts={accounts}
|
||||
user={data.user}
|
||||
/>
|
||||
</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}
|
||||
</Page>
|
||||
</SidebarProvider>
|
||||
|
||||
@@ -22,7 +22,6 @@ import { Trans } from '@kit/ui/trans';
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
// local imports
|
||||
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
|
||||
import { loadMembersPageData } from './_lib/server/members-page.loader';
|
||||
|
||||
@@ -56,8 +55,7 @@ async function TeamAccountMembersPage({ params }: TeamAccountMembersPageProps) {
|
||||
<>
|
||||
<TeamAccountLayoutPageHeader
|
||||
title={<Trans i18nKey={'common:routes.members'} />}
|
||||
description={<AppBreadcrumbs />}
|
||||
account={account.slug}
|
||||
description={<AppBreadcrumbs values={{ [account.slug]: account.name }}/>}
|
||||
/>
|
||||
|
||||
<PageBody>
|
||||
|
||||
@@ -8,7 +8,6 @@ import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
|
||||
// local imports
|
||||
import { TeamAccountLayoutPageHeader } from '../_components/team-account-layout-page-header';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
@@ -48,9 +47,8 @@ async function TeamAccountSettingsPage(props: TeamAccountSettingsPageProps) {
|
||||
return (
|
||||
<>
|
||||
<TeamAccountLayoutPageHeader
|
||||
account={account.slug}
|
||||
title={<Trans i18nKey={'teams:settings.pageTitle'} />}
|
||||
description={<AppBreadcrumbs />}
|
||||
description={<AppBreadcrumbs values={{ [account.slug]: account.name }} />}
|
||||
/>
|
||||
|
||||
<PageBody>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const UpdateHealthBenefitSchema = z.object({
|
||||
occurance: z
|
||||
occurrence: z
|
||||
.string({
|
||||
error: 'Occurance is required',
|
||||
error: 'Occurrence is required',
|
||||
})
|
||||
.nonempty(),
|
||||
amount: z.number({ error: 'Amount is required' }),
|
||||
|
||||
@@ -170,6 +170,7 @@ async function TeamAccountPage(props: {
|
||||
<>
|
||||
<PageHeader
|
||||
className="border-b"
|
||||
title={'Account'}
|
||||
description={
|
||||
<AppBreadcrumbs
|
||||
values={{
|
||||
|
||||
@@ -18,8 +18,8 @@ export async function AdminDashboard() {
|
||||
' xl:grid-cols-4'
|
||||
}
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="flex-1">
|
||||
<CardTitle>Users</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
@@ -34,8 +34,8 @@ export async function AdminDashboard() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Card className="flex flex-col">
|
||||
<CardHeader className="flex-1">
|
||||
<CardTitle>Company Accounts</CardTitle>
|
||||
|
||||
<CardDescription>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { DataTable } from '@kit/ui/enhanced-data-table';
|
||||
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
||||
import { formatDateAndTime } from '@kit/shared/utils';
|
||||
|
||||
type Memberships =
|
||||
Database['medreport']['Functions']['get_account_members']['Returns'][number];
|
||||
@@ -17,10 +18,6 @@ export function AdminMembersTable(props: { members: Memberships[] }) {
|
||||
|
||||
function getColumns(): ColumnDef<Memberships>[] {
|
||||
return [
|
||||
{
|
||||
header: 'User ID',
|
||||
accessorKey: 'user_id',
|
||||
},
|
||||
{
|
||||
header: 'Name',
|
||||
cell: ({ row }) => {
|
||||
@@ -58,10 +55,16 @@ function getColumns(): ColumnDef<Memberships>[] {
|
||||
{
|
||||
header: 'Created At',
|
||||
accessorKey: 'created_at',
|
||||
cell: ({ row }) => {
|
||||
return formatDateAndTime(row.original.created_at);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: 'Updated At',
|
||||
accessorKey: 'updated_at',
|
||||
cell: ({ row }) => {
|
||||
return formatDateAndTime(row.original.updated_at);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ export class TeamAccountsApi {
|
||||
.schema('medreport')
|
||||
.from('company_params')
|
||||
.update({
|
||||
benefit_occurance: data.occurance,
|
||||
benefit_occurance: data.occurrence,
|
||||
benefit_amount: data.amount,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface UpdateHealthBenefitData {
|
||||
accountId: string;
|
||||
occurance: string;
|
||||
occurrence: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
33
packages/shared/src/config/admin-navigation.config.tsx
Normal file
33
packages/shared/src/config/admin-navigation.config.tsx
Normal 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',
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import appConfig from './app.config';
|
||||
import authConfig from './auth.config';
|
||||
import billingConfig from './billing.config';
|
||||
import { adminNavigationConfig } from './admin-navigation.config';
|
||||
import {
|
||||
DynamicAuthConfig,
|
||||
getCachedAuthConfig,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
} from './team-account-navigation.config';
|
||||
|
||||
export {
|
||||
adminNavigationConfig,
|
||||
appConfig,
|
||||
authConfig,
|
||||
billingConfig,
|
||||
|
||||
@@ -7,7 +7,6 @@ const iconClasses = 'w-4';
|
||||
|
||||
const getRoutes = (account: string) => [
|
||||
{
|
||||
label: 'common:routes.application',
|
||||
children: [
|
||||
{
|
||||
label: 'common:routes.dashboard',
|
||||
@@ -15,12 +14,6 @@ const getRoutes = (account: string) => [
|
||||
Icon: <LayoutDashboard className={iconClasses} />,
|
||||
end: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'common:routes.settings',
|
||||
collapsible: false,
|
||||
children: [
|
||||
{
|
||||
label: 'common:routes.settings',
|
||||
path: createPath(pathsConfig.app.accountSettings, account),
|
||||
|
||||
@@ -41,15 +41,19 @@ const CardHeader: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||
);
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = ({
|
||||
const CardTitle: React.FC<React.HTMLAttributes<HTMLHeadingElement> & { size?: 'h3' | 'h4' | 'h5' }> = ({
|
||||
className,
|
||||
size = 'h3',
|
||||
...props
|
||||
}) => (
|
||||
<h3
|
||||
}) => {
|
||||
const Component = size;
|
||||
return (
|
||||
<Component
|
||||
className={cn('leading-none font-semibold tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardDescription: React.FC<React.HTMLAttributes<HTMLParagraphElement>> = ({
|
||||
|
||||
@@ -119,5 +119,25 @@
|
||||
},
|
||||
"cart": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,10 +80,11 @@
|
||||
"dashboard": "Dashboard",
|
||||
"settings": "Settings",
|
||||
"profile": "Profile",
|
||||
"application": "Application",
|
||||
"pickTime": "Pick time",
|
||||
"preferences": "Preferences",
|
||||
"security": "Security"
|
||||
"security": "Security",
|
||||
"admin": "Admin",
|
||||
"accounts": "Accounts"
|
||||
},
|
||||
"roles": {
|
||||
"owner": {
|
||||
@@ -124,7 +125,7 @@
|
||||
"city": "City",
|
||||
"weight": "Weight",
|
||||
"height": "Height",
|
||||
"occurance": "Support frequency",
|
||||
"occurrence": "Support frequency",
|
||||
"amount": "Amount",
|
||||
"selectDate": "Select date"
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"home": {
|
||||
"pageTitle": "Dashboard"
|
||||
"pageTitle": "Company Dashboard"
|
||||
},
|
||||
"settings": {
|
||||
"pageTitle": "Settings",
|
||||
"pageTitle": "Company Settings",
|
||||
"pageDescription": "Manage your Company details",
|
||||
"teamLogo": "Company Logo",
|
||||
"teamLogoDescription": "Update your company's logo to make it easier to identify",
|
||||
@@ -13,10 +13,10 @@
|
||||
"dangerZoneDescription": "This section contains actions that are irreversible"
|
||||
},
|
||||
"members": {
|
||||
"pageTitle": "Members"
|
||||
"pageTitle": "Company Members"
|
||||
},
|
||||
"billing": {
|
||||
"pageTitle": "Billing"
|
||||
"pageTitle": "Company Billing"
|
||||
},
|
||||
"yourTeams": "Your Companies ({{teamsCount}})",
|
||||
"createTeam": "Create a Company",
|
||||
|
||||
@@ -121,16 +121,16 @@
|
||||
"label": "Cart ({{ items }})"
|
||||
},
|
||||
"pageTitle": "{{companyName}} eelarve",
|
||||
"description": "Vali kalendrist sobiv kuupäev ja broneeri endale vastuvõtuaeg.",
|
||||
"description": "Muuda ettevõtte eelarve seadistusi.",
|
||||
"saveChanges": "Salvesta muudatused",
|
||||
"healthBenefitForm": {
|
||||
"title": "Tervisetoetuse vorm",
|
||||
"description": "Ettevõtte Tervisekassa toetus töötajale",
|
||||
"info": "* Hindadele lisanduvad riigipoolsed maksud"
|
||||
},
|
||||
"occurance": {
|
||||
"occurrence": {
|
||||
"yearly": "Kord aastas",
|
||||
"quarterly": "kord kvartalis",
|
||||
"quarterly": "Kord kvartalis",
|
||||
"monthly": "Kord kuus"
|
||||
},
|
||||
"expensesOverview": {
|
||||
|
||||
@@ -80,10 +80,11 @@
|
||||
"dashboard": "Ülevaade",
|
||||
"settings": "Seaded",
|
||||
"profile": "Profiil",
|
||||
"application": "Rakendus",
|
||||
"pickTime": "Vali aeg",
|
||||
"preferences": "Eelistused",
|
||||
"security": "Turvalisus"
|
||||
"security": "Turvalisus",
|
||||
"admin": "Admin",
|
||||
"accounts": "Kontod"
|
||||
},
|
||||
"roles": {
|
||||
"owner": {
|
||||
@@ -124,7 +125,7 @@
|
||||
"city": "Linn",
|
||||
"weight": "Kaal",
|
||||
"height": "Pikkus",
|
||||
"occurance": "Toetuse sagedus",
|
||||
"occurrence": "Toetuse sagedus",
|
||||
"amount": "Summa",
|
||||
"selectDate": "Vali kuupäev"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"home": {
|
||||
"pageTitle": "Ülevaade",
|
||||
"pageTitle": "Ettevõtte ülevaade",
|
||||
"headerTitle": "{{companyName}} Tervisekassa kokkuvõte",
|
||||
"healthDetails": "Ettevõtte terviseandmed",
|
||||
"membersSettingsButtonTitle": "Halda töötajaid",
|
||||
@@ -9,7 +9,7 @@
|
||||
"membersBillingButtonDescription": "Vali kuidas soovid eelarvet töötajate vahel jagada."
|
||||
},
|
||||
"settings": {
|
||||
"pageTitle": "Seaded",
|
||||
"pageTitle": "Ettevõtte seaded",
|
||||
"pageDescription": "Halda oma ettevõtte andmeid",
|
||||
"teamLogo": "Ettevõtte logo",
|
||||
"teamLogoDescription": "Uuenda oma ettevõtte logo, et seda oleks lihtsam tuvastada",
|
||||
@@ -19,10 +19,10 @@
|
||||
"dangerZoneDescription": "See osa sisaldab pöördumatuid toiminguid"
|
||||
},
|
||||
"members": {
|
||||
"pageTitle": "Töötajad"
|
||||
"pageTitle": "Ettevõtte töötajad"
|
||||
},
|
||||
"billing": {
|
||||
"pageTitle": "Arveldamine"
|
||||
"pageTitle": "Ettevõtte arveldamine"
|
||||
},
|
||||
"benefitStatistics": {
|
||||
"budget": {
|
||||
|
||||
@@ -121,14 +121,14 @@
|
||||
"label": "Корзина ({{ items }})"
|
||||
},
|
||||
"pageTitle": "Бюджет {{companyName}}",
|
||||
"description": "Выберите подходящую дату в календаре и запишитесь на прием.",
|
||||
"description": "Измените настройки бюджета компании.",
|
||||
"saveChanges": "Сохранить изменения",
|
||||
"healthBenefitForm": {
|
||||
"title": "Форма здоровья",
|
||||
"description": "Поддержка сотрудника из корпоративного фонда здоровья",
|
||||
"info": "* К ценам добавляются государственные налоги"
|
||||
},
|
||||
"occurance": {
|
||||
"occurrence": {
|
||||
"yearly": "Раз в год",
|
||||
"quarterly": "Раз в квартал",
|
||||
"monthly": "Раз в месяц"
|
||||
|
||||
@@ -80,10 +80,11 @@
|
||||
"dashboard": "Обзор",
|
||||
"settings": "Настройки",
|
||||
"profile": "Профиль",
|
||||
"application": "Приложение",
|
||||
"pickTime": "Выбрать время",
|
||||
"preferences": "Предпочтения",
|
||||
"security": "Безопасность"
|
||||
"security": "Безопасность",
|
||||
"admin": "Админ",
|
||||
"accounts": "Аккаунты"
|
||||
},
|
||||
"roles": {
|
||||
"owner": {
|
||||
@@ -124,7 +125,7 @@
|
||||
"city": "Город",
|
||||
"weight": "Вес",
|
||||
"height": "Рост",
|
||||
"occurance": "Частота поддержки",
|
||||
"occurrence": "Частота поддержки",
|
||||
"amount": "Сумма",
|
||||
"selectDate": "Выберите дату"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"home": {
|
||||
"pageTitle": "Обзор",
|
||||
"pageTitle": "Обзор компании",
|
||||
"headerTitle": "Обзор Tervisekassa {{companyName}}",
|
||||
"healthDetails": "Данные о здоровье компании",
|
||||
"membersSettingsButtonTitle": "Управление сотрудниками",
|
||||
@@ -9,7 +9,7 @@
|
||||
"membersBillingButtonDescription": "Выберите, как распределять бюджет между сотрудниками."
|
||||
},
|
||||
"settings": {
|
||||
"pageTitle": "Настройки",
|
||||
"pageTitle": "Настройки компании",
|
||||
"pageDescription": "Управление данными вашей компании",
|
||||
"teamLogo": "Логотип компании",
|
||||
"teamLogoDescription": "Обновите логотип вашей компании для упрощения идентификации",
|
||||
@@ -19,10 +19,10 @@
|
||||
"dangerZoneDescription": "Этот раздел содержит действия, которые невозможно отменить"
|
||||
},
|
||||
"members": {
|
||||
"pageTitle": "Сотрудники"
|
||||
"pageTitle": "Сотрудники компании"
|
||||
},
|
||||
"billing": {
|
||||
"pageTitle": "Биллинг"
|
||||
"pageTitle": "Биллинг компании"
|
||||
},
|
||||
"benefitStatistics": {
|
||||
"budget": {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
48
supabase/migrations/20250101000002_fix_permissions.sql
Normal file
48
supabase/migrations/20250101000002_fix_permissions.sql
Normal 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;
|
||||
114
supabase/migrations/20250101000003_fix_upsert_and_rls.sql
Normal file
114
supabase/migrations/20250101000003_fix_upsert_and_rls.sql
Normal 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;
|
||||
|
||||
Reference in New Issue
Block a user