diff --git a/app/home/[account]/billing/page.tsx b/app/home/[account]/billing/page.tsx
index e3ca767..9a1aeff 100644
--- a/app/home/[account]/billing/page.tsx
+++ b/app/home/[account]/billing/page.tsx
@@ -29,10 +29,13 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
const account = await api.getTeamAccount(accountSlug);
const { members } = await api.getMembers(accountSlug);
+ const eligibleMembersCount = members.filter(
+ ({ is_eligible_for_benefits }) => !!is_eligible_for_benefits,
+ ).length;
const [expensesOverview, companyParams] = await Promise.all([
loadTeamAccountBenefitExpensesOverview({
companyId: account.id,
- employeeCount: members.length,
+ employeeCount: eligibleMembersCount,
}),
api.getTeamAccountParams(account.id),
]);
@@ -42,7 +45,7 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
diff --git a/app/home/[account]/members/_lib/server/members-page.loader.ts b/app/home/[account]/members/_lib/server/members-page.loader.ts
index 0f0c50e..1befda2 100644
--- a/app/home/[account]/members/_lib/server/members-page.loader.ts
+++ b/app/home/[account]/members/_lib/server/members-page.loader.ts
@@ -65,18 +65,17 @@ async function loadAccountMembers(
const members = data ?? [];
- return members
- .sort((prev, next) => {
- if (prev.primary_owner_user_id === prev.user_id) {
- return -1;
- }
+ return members.sort((prev, next) => {
+ if (prev.primary_owner_user_id === prev.user_id) {
+ return -1;
+ }
- if (prev.role_hierarchy_level < next.role_hierarchy_level) {
- return -1;
- }
+ if (prev.role_hierarchy_level < next.role_hierarchy_level) {
+ return -1;
+ }
- return 1;
- });
+ return 1;
+ });
}
export async function loadAccountMembersBenefitsUsage(
@@ -100,11 +99,7 @@ export async function loadAccountMembersBenefitsUsage(
return [];
}
- return (data ?? []) as unknown as {
- personal_account_id: string;
- benefit_amount: number;
- benefit_unused_amount: number;
- }[];
+ return data ?? [];
}
/**
diff --git a/app/home/[account]/members/page.tsx b/app/home/[account]/members/page.tsx
index eeb012e..02a04de 100644
--- a/app/home/[account]/members/page.tsx
+++ b/app/home/[account]/members/page.tsx
@@ -52,6 +52,7 @@ async function TeamAccountMembersPage({ params }: TeamAccountMembersPageProps) {
const canManageRoles = account.permissions.includes('roles.manage');
const canManageInvitations = account.permissions.includes('invites.manage');
+ const canUpdateBenefit = account.permissions.includes('benefit.manage');
const isPrimaryOwner = account.primary_owner_user_id === user.id;
const currentUserRoleHierarchy = account.role_hierarchy_level;
@@ -103,6 +104,7 @@ async function TeamAccountMembersPage({ params }: TeamAccountMembersPageProps) {
members={members}
isPrimaryOwner={isPrimaryOwner}
canManageRoles={canManageRoles}
+ canUpdateBenefit={canUpdateBenefit}
membersBenefitsUsage={membersBenefitsUsage}
/>
diff --git a/packages/features/accounts/src/server/services/account-balance.service.ts b/packages/features/accounts/src/server/services/account-balance.service.ts
index 8a7f1c5..fc75a2e 100644
--- a/packages/features/accounts/src/server/services/account-balance.service.ts
+++ b/packages/features/accounts/src/server/services/account-balance.service.ts
@@ -120,6 +120,22 @@ export class AccountBalanceService {
};
}
+ async upsertHealthBenefitsBySchedule(
+ benefitDistributionScheduleId: string,
+ ): Promise {
+ console.info('Updating health benefits...');
+ const { error } = await this.supabase
+ .schema('medreport')
+ .rpc('upsert_health_benefits', {
+ p_benefit_distribution_schedule_id: benefitDistributionScheduleId,
+ });
+ if (error) {
+ console.error('Error Updating health benefits.', error);
+ throw new Error('Failed Updating health benefits.');
+ }
+ console.info('Updating health benefits successfully');
+ }
+
async processPeriodicBenefitDistributions(): Promise {
console.info('Processing periodic benefit distributions...');
const { error } = await this.supabase
diff --git a/packages/features/team-accounts/src/components/members/account-members-table.tsx b/packages/features/team-accounts/src/components/members/account-members-table.tsx
index 1d248d9..3eddd0a 100644
--- a/packages/features/team-accounts/src/components/members/account-members-table.tsx
+++ b/packages/features/team-accounts/src/components/members/account-members-table.tsx
@@ -25,6 +25,7 @@ import { Trans } from '@kit/ui/trans';
import { RemoveMemberDialog } from './remove-member-dialog';
import { RoleBadge } from './role-badge';
import { TransferOwnershipDialog } from './transfer-ownership-dialog';
+import UpdateEmployeeBenefitDialog from './update-employee-benefit-dialog';
import { UpdateMemberRoleDialog } from './update-member-role-dialog';
type Members =
@@ -34,6 +35,7 @@ interface Permissions {
canUpdateRole: (roleHierarchy: number) => boolean;
canRemoveFromAccount: (roleHierarchy: number) => boolean;
canTransferOwnership: boolean;
+ canUpdateBenefit: boolean;
}
type AccountMembersTableProps = {
@@ -43,6 +45,7 @@ type AccountMembersTableProps = {
userRoleHierarchy: number;
isPrimaryOwner: boolean;
canManageRoles: boolean;
+ canUpdateBenefit: boolean;
membersBenefitsUsage: {
personal_account_id: string;
benefit_amount: number;
@@ -57,6 +60,7 @@ export function AccountMembersTable({
isPrimaryOwner,
userRoleHierarchy,
canManageRoles,
+ canUpdateBenefit,
membersBenefitsUsage,
}: AccountMembersTableProps) {
const [search, setSearch] = useState('');
@@ -74,6 +78,7 @@ export function AccountMembersTable({
);
},
canTransferOwnership: isPrimaryOwner,
+ canUpdateBenefit,
};
const columns = useGetColumns(permissions, {
@@ -211,8 +216,7 @@ function useGetColumns(
{
header: t('roleLabel'),
cell: ({ row }) => {
- const { role, primary_owner_user_id, user_id } = row.original;
- const isPrimaryOwner = primary_owner_user_id === user_id;
+ const { role } = row.original;
return (
@@ -252,27 +255,21 @@ function useGetColumns(
function ActionsDropdown({
permissions,
member,
- currentUserId,
currentTeamAccountId,
currentRoleHierarchy,
}: {
permissions: Permissions;
member: Members[0];
- currentUserId: string;
currentTeamAccountId: string;
currentRoleHierarchy: number;
}) {
const [isRemoving, setIsRemoving] = useState(false);
const [isTransferring, setIsTransferring] = useState(false);
const [isUpdatingRole, setIsUpdatingRole] = useState(false);
+ const [isUpdatingBenefit, setIsUpdatingBenefit] = useState(false);
- const isCurrentUser = member.user_id === currentUserId;
const isPrimaryOwner = member.primary_owner_user_id === member.user_id;
- if (isCurrentUser || isPrimaryOwner) {
- return null;
- }
-
const memberRoleHierarchy = member.role_hierarchy_level;
const canUpdateRole = permissions.canUpdateRole(memberRoleHierarchy);
@@ -299,23 +296,29 @@ function ActionsDropdown({
-
+
setIsUpdatingRole(true)}>
-
+
setIsTransferring(true)}>
-
+
setIsRemoving(true)}>
+
+
+ setIsUpdatingBenefit(true)}>
+
+
+
@@ -348,6 +351,16 @@ function ActionsDropdown({
userId={member.user_id}
/>
+
+
+
+
>
);
}
diff --git a/packages/features/team-accounts/src/components/members/update-employee-benefit-dialog.tsx b/packages/features/team-accounts/src/components/members/update-employee-benefit-dialog.tsx
new file mode 100644
index 0000000..50c819e
--- /dev/null
+++ b/packages/features/team-accounts/src/components/members/update-employee-benefit-dialog.tsx
@@ -0,0 +1,99 @@
+import React, { useState, useTransition } from 'react';
+
+import { useRouter } from 'next/navigation';
+
+import { Alert, AlertDescription } from '@kit/ui/alert';
+import {
+ AlertDialog,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+} from '@kit/ui/alert-dialog';
+import { Button } from '@kit/ui/button';
+import { If } from '@kit/ui/if';
+import { Trans } from '@kit/ui/trans';
+
+import { updateEmployeeBenefitAction } from '../../server/actions/team-members-server-actions';
+
+const UpdateEmployeeBenefitDialog = ({
+ isOpen,
+ setIsOpen,
+ accountId,
+ userId,
+ isEligibleForBenefits,
+}: {
+ isOpen: boolean;
+ setIsOpen: (isOpen: boolean) => void;
+ accountId: string;
+ userId: string;
+ isEligibleForBenefits: boolean;
+}) => {
+ const router = useRouter();
+ const [isSubmitting, startTransition] = useTransition();
+ const [error, setError] = useState();
+ const updateEmployeeBenefit = () => {
+ startTransition(async () => {
+ try {
+ await updateEmployeeBenefitAction({ accountId, userId });
+
+ setIsOpen(false);
+
+ router.refresh();
+ } catch {
+ setError(true);
+ }
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {isEligibleForBenefits ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default UpdateEmployeeBenefitDialog;
diff --git a/packages/features/team-accounts/src/schema/update-employee-benefit.schema.ts b/packages/features/team-accounts/src/schema/update-employee-benefit.schema.ts
new file mode 100644
index 0000000..c4ea5a0
--- /dev/null
+++ b/packages/features/team-accounts/src/schema/update-employee-benefit.schema.ts
@@ -0,0 +1,6 @@
+import { z } from 'zod';
+
+export const UpdateEmployeeBenefitSchema = z.object({
+ accountId: z.string().uuid(),
+ userId: z.string().uuid(),
+});
diff --git a/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts b/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts
index 92c0b5f..2fa4fd7 100644
--- a/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts
+++ b/packages/features/team-accounts/src/server/actions/team-members-server-actions.ts
@@ -2,6 +2,7 @@
import { revalidatePath } from 'next/cache';
+import { AccountBalanceService } from '@kit/accounts/services/account-balance.service';
import { enhanceAction } from '@kit/next/actions';
import { createOtpApi } from '@kit/otp';
import { getLogger } from '@kit/shared/logger';
@@ -10,6 +11,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { RemoveMemberSchema } from '../../schema/remove-member.schema';
import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-ownership-confirmation.schema';
+import { UpdateEmployeeBenefitSchema } from '../../schema/update-employee-benefit.schema';
import { UpdateMemberRoleSchema } from '../../schema/update-member-role.schema';
import { createAccountMembersService } from '../services/account-members.service';
@@ -144,3 +146,64 @@ export const transferOwnershipAction = enhanceAction(
schema: TransferOwnershipConfirmationSchema,
},
);
+
+export const updateEmployeeBenefitAction = enhanceAction(
+ async ({ accountId, userId }) => {
+ const client = getSupabaseServerClient();
+ const logger = await getLogger();
+ const accountBalanceService = new AccountBalanceService();
+
+ const ctx = {
+ name: 'teams.updateEmployeeBenefit',
+ userId,
+ accountId,
+ };
+
+ const { data, error } = await client
+ .schema('medreport')
+ .from('accounts_memberships')
+ .select('id,is_eligible_for_benefits')
+ .eq('user_id', userId)
+ .eq('account_id', accountId)
+ .single();
+
+ logger.info(
+ { ...ctx, isEligible: !data?.is_eligible_for_benefits, id: data?.id },
+ 'Changing employee benefit',
+ );
+
+ if (error) {
+ logger.error({ error }, 'Error on receiving balance entry');
+ }
+
+ if (data) {
+ const { error } = await client
+ .schema('medreport')
+ .from('accounts_memberships')
+ .update({ is_eligible_for_benefits: !data.is_eligible_for_benefits })
+ .eq('id', data.id);
+
+ if (error) {
+ logger.error({ error }, `Error on updating balance entry`);
+ }
+
+ const { data: scheduleData, error: scheduleError } = await client
+ .schema('medreport')
+ .from('benefit_distribution_schedule')
+ .select('id')
+ .eq('company_id', accountId)
+ .single();
+
+ if (scheduleError) {
+ logger.error({ error }, 'Error on getting company benefit schedule');
+ }
+
+ if (scheduleData?.id) {
+ await accountBalanceService.upsertHealthBenefitsBySchedule(
+ scheduleData.id,
+ );
+ }
+ }
+ },
+ { schema: UpdateEmployeeBenefitSchema },
+);
diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts
index 1fc4d6d..4ab0950 100644
--- a/packages/supabase/src/database.types.ts
+++ b/packages/supabase/src/database.types.ts
@@ -557,6 +557,7 @@ export type Database = {
created_by: string | null
has_seen_confirmation: boolean
id: string
+ is_eligible_for_benefits: boolean
updated_at: string
updated_by: string | null
user_id: string
@@ -568,6 +569,7 @@ export type Database = {
created_by?: string | null
has_seen_confirmation?: boolean
id?: string
+ is_eligible_for_benefits?: boolean
updated_at?: string
updated_by?: string | null
user_id: string
@@ -579,6 +581,7 @@ export type Database = {
created_by?: string | null
has_seen_confirmation?: boolean
id?: string
+ is_eligible_for_benefits?: boolean
updated_at?: string
updated_by?: string | null
user_id?: string
@@ -2303,6 +2306,7 @@ export type Database = {
created_at: string
email: string
id: string
+ is_eligible_for_benefits: boolean
name: string
personal_code: string
picture_url: string
diff --git a/public/locales/et/teams.json b/public/locales/et/teams.json
index 48b0281..2ab7ada 100644
--- a/public/locales/et/teams.json
+++ b/public/locales/et/teams.json
@@ -96,6 +96,13 @@
"updateRoleLoadingMessage": "Rolli uuendatakse...",
"updateRoleSuccessMessage": "Roll edukalt uuendatud",
"updatingRoleErrorMessage": "Vabandust, tekkis viga. Palun proovi uuesti.",
+ "updateBenefit": "Tervisekonto staatus",
+ "updateBenefitHeading": "Muuda tervisekonto staatust",
+ "removeBenefitDescription": "Deaktiveeri töötaja tervisekonto",
+ "allowBenefitDescription": "Aktiveeri töötaja tervisekonto",
+ "removeBenefitSubmitLabel": "Deaktiveeri",
+ "allowBenefitSubmitLabel": "Aktiveeri",
+ "updateBenefiErrorMessage": "Vabandus, tekkis viga. Palun proovi uuesti.",
"updateMemberRoleModalHeading": "Uuenda töötaja rolli",
"updateMemberRoleModalDescription": "Muuda valitud töötaja rolli. Roll määrab töötaja õigused.",
"roleMustBeDifferent": "Roll peab erinema praegusest",
diff --git a/supabase/migrations/20251002190600_account_management.sql b/supabase/migrations/20251002190600_account_management.sql
new file mode 100644
index 0000000..f2f5f29
--- /dev/null
+++ b/supabase/migrations/20251002190600_account_management.sql
@@ -0,0 +1,2 @@
+ALTER TYPE medreport.app_permissions
+ ADD VALUE IF NOT EXISTS 'benefit.manage';
\ No newline at end of file
diff --git a/supabase/migrations/20251002191000_add_new_type.sql b/supabase/migrations/20251002191000_add_new_type.sql
new file mode 100644
index 0000000..95fb34b
--- /dev/null
+++ b/supabase/migrations/20251002191000_add_new_type.sql
@@ -0,0 +1,150 @@
+insert into medreport.role_permissions (role, permission) values
+('owner', 'benefit.manage');
+
+ALTER TABLE medreport.accounts_memberships
+ ADD COLUMN is_eligible_for_benefits boolean NOT NULL DEFAULT true;
+
+DROP FUNCTION IF EXISTS medreport.get_account_members(text);
+
+CREATE OR REPLACE FUNCTION medreport.get_account_members(account_slug text)
+ RETURNS TABLE(
+ id uuid,
+ user_id uuid,
+ account_id uuid,
+ role character varying,
+ role_hierarchy_level integer,
+ primary_owner_user_id uuid,
+ name text,
+ email character varying,
+ personal_code text,
+ picture_url character varying,
+ created_at timestamp with time zone,
+ updated_at timestamp with time zone,
+ is_eligible_for_benefits boolean
+)
+ LANGUAGE plpgsql
+ SET search_path TO ''
+AS $function$begin
+ return QUERY
+ select
+ acc.id,
+ am.user_id,
+ am.account_id,
+ am.account_role,
+ r.hierarchy_level,
+ a.primary_owner_user_id,
+ TRIM(CONCAT(acc.name, ' ', acc.last_name)) as name,
+ acc.email,
+ acc.personal_code,
+ acc.picture_url,
+ am.created_at,
+ am.updated_at,
+ am.is_eligible_for_benefits
+ from
+ medreport.accounts_memberships am
+ join medreport.accounts a on a.id = am.account_id
+ join medreport.accounts acc on acc.id = am.user_id
+ join medreport.roles r on r.name = am.account_role
+ where
+ a.slug = account_slug;
+
+end;$function$
+;
+
+grant
+ execute on function medreport.get_account_members (text) to authenticated,
+ service_role;
+
+create policy "update_accounts_memberships"
+on "medreport"."accounts_memberships"
+as permissive
+for update
+to authenticated
+using (medreport.is_account_owner(account_id))
+with check (medreport.is_account_owner(account_id));
+
+drop policy "restrict_mfa_accounts_memberships" on "medreport"."accounts_memberships";
+grant update on table "medreport"."accounts_memberships" to "authenticated";
+
+drop TRIGGER if exists prevent_memberships_update_check on "medreport"."accounts_memberships";
+drop function if exists kit.prevent_memberships_update();
+
+create or replace function medreport.upsert_health_benefits(
+ p_benefit_distribution_schedule_id uuid
+)
+returns void
+language plpgsql
+security definer
+as $$
+declare
+ member_record record;
+ expires_date timestamp with time zone;
+ v_company_id uuid;
+ v_benefit_amount numeric;
+ existing_entry_id uuid;
+ v_target_amount numeric;
+begin
+ -- Expires on first day of next year.
+ expires_date := date_trunc('year', now() + interval '1 year');
+
+ -- Get company_id and benefit_amount from benefit_distribution_schedule
+ select company_id, benefit_amount into v_company_id, v_benefit_amount
+ from medreport.benefit_distribution_schedule
+ where id = p_benefit_distribution_schedule_id;
+
+ -- Get all personal accounts that are members of this company
+ for member_record in
+ select distinct
+ a.id as personal_account_id,
+ coalesce(am.is_eligible_for_benefits) as is_eligible
+ from medreport.accounts a
+ join medreport.accounts_memberships am on a.id = am.user_id
+ where am.account_id = v_company_id
+ and a.is_personal_account = true
+ loop
+ v_target_amount := case when member_record.is_eligible
+ then v_benefit_amount
+ else 0 end;
+
+ -- Check if there is already a balance entry for this personal account from the same company in same month
+ select id into existing_entry_id
+ from medreport.account_balance_entries
+ where entry_type = 'benefit'
+ and account_id = member_record.personal_account_id
+ and source_company_id = v_company_id
+ and date_trunc('month', created_at) = date_trunc('month', now())
+ LIMIT 1;
+
+ if existing_entry_id is not null then
+ update medreport.account_balance_entries set
+ amount = v_target_amount,
+ expires_at = expires_date,
+ benefit_distribution_schedule_id = p_benefit_distribution_schedule_id
+ where id = existing_entry_id;
+ else
+ -- Insert new balance entry for personal account
+ insert into medreport.account_balance_entries (
+ account_id,
+ amount,
+ entry_type,
+ description,
+ source_company_id,
+ created_by,
+ expires_at,
+ benefit_distribution_schedule_id
+ ) values (
+ member_record.personal_account_id,
+ v_target_amount,
+ 'benefit',
+ 'Health benefit from company',
+ v_company_id,
+ auth.uid(),
+ expires_date,
+ p_benefit_distribution_schedule_id
+ );
+ end if;
+ end loop;
+end;
+$$;
+
+grant execute on function medreport.upsert_health_benefits(uuid) to authenticated, service_role;
\ No newline at end of file