From 47e8bd873c4a5d3bd86c97931524320f2bd2c6ab Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Fri, 3 Oct 2025 15:33:38 +0300 Subject: [PATCH] add benefit eligibility setting to HR members --- .../_lib/server/members-page.loader.ts | 25 ++--- app/home/[account]/members/page.tsx | 3 +- .../services/account-balance.service.ts | 16 +++ .../members/account-members-table.tsx | 25 ++++- .../update-employee-benefit-dialog.tsx | 99 +++++++++++++++++++ .../schema/update-employee-benefit.schema.ts | 6 ++ .../actions/team-members-server-actions.ts | 65 ++++++++++++ packages/supabase/src/database.types.ts | 4 + public/locales/et/teams.json | 7 ++ .../20251002191000_add_new_type.sql | 56 ++++++++++- 10 files changed, 287 insertions(+), 19 deletions(-) create mode 100644 packages/features/team-accounts/src/components/members/update-employee-benefit-dialog.tsx create mode 100644 packages/features/team-accounts/src/schema/update-employee-benefit.schema.ts 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 498b7fe..02a04de 100644 --- a/app/home/[account]/members/page.tsx +++ b/app/home/[account]/members/page.tsx @@ -52,7 +52,7 @@ async function TeamAccountMembersPage({ params }: TeamAccountMembersPageProps) { const canManageRoles = account.permissions.includes('roles.manage'); const canManageInvitations = account.permissions.includes('invites.manage'); - const canManageBenefit = account.permissions.include('benefit.manage'); + const canUpdateBenefit = account.permissions.includes('benefit.manage'); const isPrimaryOwner = account.primary_owner_user_id === user.id; const currentUserRoleHierarchy = account.role_hierarchy_level; @@ -104,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..8c851ec 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 ( + + + setIsUpdatingBenefit(true)}> + + + @@ -348,6 +359,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..ef4b2fd 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,66 @@ 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) + .eq('type', 'benefit') + .single(); + + logger.info( + ctx, + 'Changing employee benefit to ', + !data?.is_eligible_for_benefits, + ); + + if (error) { + logger.error(ctx, 'Error on receiving balance entry', error); + } + + 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(ctx, `Error on updating balance entry`, error); + } + + const { data: scheduleData, error: scheduleError } = await client + .schema('medreport') + .from('benefit_distribution_schedule') + .select('id') + .eq('company_id', accountId) + .single(); + + if (scheduleError) { + logger.error(ctx, `Error on getting company benefit schedule`, error); + } + + 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..5c1b966 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": "Uuenda toetust", + "updateBenefitHeading": "Uuenda toetus staatust", + "removeBenefitDescription": "Eemalda töötaja toetusest", + "allowBenefitDescription": "Luba töötajale toetus", + "removeBenefitSubmitLabel": "Eemalda toetus", + "allowBenefitSubmitLabel": "Luba toetus", + "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/20251002191000_add_new_type.sql b/supabase/migrations/20251002191000_add_new_type.sql index 7467dcd..c111974 100644 --- a/supabase/migrations/20251002191000_add_new_type.sql +++ b/supabase/migrations/20251002191000_add_new_type.sql @@ -1,2 +1,56 @@ insert into medreport.role_permissions (role, permission) values -('owner', 'benefit.manage'); \ No newline at end of file +('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;