diff --git a/app/home/(user)/_components/cart/mobile-cart-items.tsx b/app/home/(user)/_components/cart/mobile-cart-items.tsx index 9b21f86..0c26780 100644 --- a/app/home/(user)/_components/cart/mobile-cart-items.tsx +++ b/app/home/(user)/_components/cart/mobile-cart-items.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { Table, TableBody } from '@kit/ui/shadcn/table'; -import MobileCartRow from './mobile-cart-row'; +import MobileTableRow from './mobile-table-row'; const MobileCartItems = ({ item, @@ -24,12 +24,12 @@ const MobileCartItems = ({ return ( - - - + - - - - - - + - -

- {value} -

+

{value}

); -export default MobileCartRow; +export default MobleTableRow; diff --git a/app/home/(user)/_components/orders/order-block.tsx b/app/home/(user)/_components/orders/order-block.tsx index 5eef5cb..2185e4e 100644 --- a/app/home/(user)/_components/orders/order-block.tsx +++ b/app/home/(user)/_components/orders/order-block.tsx @@ -51,7 +51,7 @@ export default function OrderBlock({ )} -
+
{analysisOrder && ( - - - - - - - - - {order.location && ( + <> +
+ + {items + .sort((a, b) => + (a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1, + ) + .map((orderItem) => ( +
+ + + {order.location && ( + + )} + + + + + + {isTtoservice && order.bookingCode && ( + + )} + + +
+ ))} +
+
+ + + + - + - )} - - - - {isAnalysisOrder && } - - - - {items - .sort((a, b) => - (a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1, - ) - .map((orderItem) => ( - - -

- {orderItem.product_title} -

-
- - - {formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')} - - {order.location && ( - - {order.location} + + + + {order.location && ( + + + + )} + + + + {isAnalysisOrder && } +
+ + + {items + .sort((a, b) => + (a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1, + ) + .map((orderItem) => ( + + +

+ {orderItem.product_title} +

- )} - - {isPackage ? ( - - ) : ( - - )} - - - - {isTtoservice && order.bookingCode && ( - + + {formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')} + + {order.location && ( + + {order.location} + )} - -
- ))} -
+ + {isPackage ? ( + + ) : ( + + )} + + + + + {isTtoservice && order.bookingCode && ( + + )} + + + ))} +
+
{order?.bookingCode && order?.clinicId && ( )} - + ); } diff --git a/packages/features/accounts/src/components/personal-account-dropdown.tsx b/packages/features/accounts/src/components/personal-account-dropdown.tsx index 447a8a3..8fc2c8c 100644 --- a/packages/features/accounts/src/components/personal-account-dropdown.tsx +++ b/packages/features/accounts/src/components/personal-account-dropdown.tsx @@ -94,8 +94,8 @@ export function PersonalAccountDropdown({ const hasDoctorRole = personalAccountData?.application_role === ApplicationRoleEnum.Doctor; - return hasDoctorRole && hasTotpFactor; - }, [personalAccountData, hasTotpFactor]); + return hasDoctorRole; + }, [personalAccountData]); return ( 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 fc75a2e..8767b00 100644 --- a/packages/features/accounts/src/server/services/account-balance.service.ts +++ b/packages/features/accounts/src/server/services/account-balance.service.ts @@ -1,6 +1,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client'; import type { AccountBalanceEntry } from '../../types/account-balance-entry'; +import { createAccountsApi } from '../api'; export type AccountBalanceSummary = { totalBalance: number; @@ -88,6 +89,11 @@ export class AccountBalanceService { * Get balance summary for dashboard display */ async getBalanceSummary(accountId: string): Promise { + const api = createAccountsApi(this.supabase); + + const hasAccountTeamMembership = + await api.hasAccountTeamMembership(accountId); + const [balance, entries] = await Promise.all([ this.getAccountBalance(accountId), this.getAccountBalanceEntries(accountId, { limit: 5 }), @@ -113,6 +119,14 @@ export class AccountBalanceService { const expiringSoon = expiringData?.reduce((sum, entry) => sum + (entry.amount || 0), 0) || 0; + if (!hasAccountTeamMembership) { + return { + totalBalance: 0, + expiringSoon, + recentEntries: entries.entries, + }; + } + return { totalBalance: balance, expiringSoon, 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 3eddd0a..8b3275b 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 @@ -32,7 +32,7 @@ type Members = Database['medreport']['Functions']['get_account_members']['Returns']; interface Permissions { - canUpdateRole: (roleHierarchy: number) => boolean; + canUpdateRole: boolean; canRemoveFromAccount: (roleHierarchy: number) => boolean; canTransferOwnership: boolean; canUpdateBenefit: boolean; @@ -67,11 +67,7 @@ export function AccountMembersTable({ const { t } = useTranslation('teams'); const permissions = { - canUpdateRole: (targetRole: number) => { - return ( - isPrimaryOwner || (canManageRoles && userRoleHierarchy < targetRole) - ); - }, + canUpdateRole: canManageRoles, canRemoveFromAccount: (targetRole: number) => { return ( isPrimaryOwner || (canManageRoles && userRoleHierarchy < targetRole) @@ -271,7 +267,6 @@ function ActionsDropdown({ const isPrimaryOwner = member.primary_owner_user_id === member.user_id; const memberRoleHierarchy = member.role_hierarchy_level; - const canUpdateRole = permissions.canUpdateRole(memberRoleHierarchy); const canRemoveFromAccount = permissions.canRemoveFromAccount(memberRoleHierarchy); @@ -279,9 +274,10 @@ function ActionsDropdown({ // if has no permission to update role, transfer ownership or remove from account // do not render the dropdown menu if ( - !canUpdateRole && + !permissions.canUpdateRole && !permissions.canTransferOwnership && - !canRemoveFromAccount + !canRemoveFromAccount && + !permissions.canUpdateBenefit ) { return null; } @@ -296,7 +292,7 @@ function ActionsDropdown({ - + setIsUpdatingRole(true)}> diff --git a/packages/features/team-accounts/src/components/members/invite-members-dialog-container.tsx b/packages/features/team-accounts/src/components/members/invite-members-dialog-container.tsx index 1812577..e74eb27 100644 --- a/packages/features/team-accounts/src/components/members/invite-members-dialog-container.tsx +++ b/packages/features/team-accounts/src/components/members/invite-members-dialog-container.tsx @@ -37,13 +37,10 @@ import { Trans } from '@kit/ui/trans'; import { InviteMembersSchema } from '../../schema/invite-members.schema'; import { createInvitationsAction } from '../../server/actions/team-invitations-server-actions'; -import { MembershipRoleSelector } from './membership-role-selector'; import { RolesDataProvider } from './roles-data-provider'; type InviteModel = ReturnType; -type Role = string; - /** * The maximum number of invites that can be sent at once. * Useful to avoid spamming the server with too large payloads @@ -66,10 +63,7 @@ export function InviteMembersDialogContainer({ {children} - e.preventDefault()} - > + e.preventDefault()}> @@ -81,10 +75,9 @@ export function InviteMembersDialogContainer({ - {(roles) => ( + {() => ( { startTransition(() => { const promise = createInvitationsAction({ @@ -111,12 +104,10 @@ export function InviteMembersDialogContainer({ function InviteMembersForm({ onSubmit, - roles, pending, }: { onSubmit: (data: { invitations: InviteModel[] }) => void; pending: boolean; - roles: string[]; }) { const { t } = useTranslation('teams'); @@ -148,12 +139,11 @@ function InviteMembersForm({ const personalCodeInputName = `invitations.${index}.personal_code` as const; const emailInputName = `invitations.${index}.email` as const; - const roleInputName = `invitations.${index}.role` as const; return ( -
-
-
+
+
+
{ @@ -178,7 +168,7 @@ function InviteMembersForm({ }} />
-
+
{ @@ -205,37 +195,7 @@ function InviteMembersForm({ />
-
- { - return ( - - - - - - - - - { - form.setValue(field.name, role); - }} - /> - - - - - ); - }} - /> -
- -
+
@@ -303,5 +263,5 @@ function InviteMembersForm({ } function createEmptyInviteModel() { - return { email: '', role: 'member' as Role, personal_code: '' }; + return { email: '', personal_code: '' }; } diff --git a/packages/features/team-accounts/src/schema/invite-members.schema.ts b/packages/features/team-accounts/src/schema/invite-members.schema.ts index 5d41885..fb3161f 100644 --- a/packages/features/team-accounts/src/schema/invite-members.schema.ts +++ b/packages/features/team-accounts/src/schema/invite-members.schema.ts @@ -2,7 +2,6 @@ import { z } from 'zod'; const InviteSchema = z.object({ email: z.string().email(), - role: z.string().min(1).max(100), personal_code: z .string() .regex(/^[1-6]\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}\d$/, { diff --git a/packages/features/team-accounts/src/server/actions/team-invitations-server-actions.ts b/packages/features/team-accounts/src/server/actions/team-invitations-server-actions.ts index d08736b..4f11f93 100644 --- a/packages/features/team-accounts/src/server/actions/team-invitations-server-actions.ts +++ b/packages/features/team-accounts/src/server/actions/team-invitations-server-actions.ts @@ -5,7 +5,6 @@ import { redirect } from 'next/navigation'; import { z } from 'zod'; -import { AccountBalanceService } from '@kit/accounts/services/account-balance.service'; import { enhanceAction } from '@kit/next/actions'; import { createNotificationsApi } from '@kit/notifications/api'; import { getLogger } from '@kit/shared/logger'; diff --git a/packages/features/team-accounts/src/server/services/account-invitations.service.ts b/packages/features/team-accounts/src/server/services/account-invitations.service.ts index c707f84..b9443d3 100644 --- a/packages/features/team-accounts/src/server/services/account-invitations.service.ts +++ b/packages/features/team-accounts/src/server/services/account-invitations.service.ts @@ -191,7 +191,10 @@ class AccountInvitationsService { const response = await this.client .schema('medreport') .rpc('add_invitations_to_account', { - invitations, + invitations: invitations.map((invitation) => ({ + ...invitation, + role: 'member', + })), account_slug: accountSlug, }); diff --git a/supabase/migrations/20251009174500_remove_otp_from_doctor.sql b/supabase/migrations/20251009174500_remove_otp_from_doctor.sql new file mode 100644 index 0000000..77bd8f6 --- /dev/null +++ b/supabase/migrations/20251009174500_remove_otp_from_doctor.sql @@ -0,0 +1,15 @@ +CREATE OR REPLACE FUNCTION medreport.is_doctor() +RETURNS BOOLEAN +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +BEGIN + RETURN EXISTS ( + SELECT 1 + FROM medreport.accounts + WHERE primary_owner_user_id = auth.uid() + AND application_role = 'doctor' + ); +END; +$$; +grant execute on function medreport.is_doctor() to authenticated; \ No newline at end of file diff --git a/supabase/migrations/20251009180300_fix_member_management.sql b/supabase/migrations/20251009180300_fix_member_management.sql new file mode 100644 index 0000000..a87edc1 --- /dev/null +++ b/supabase/migrations/20251009180300_fix_member_management.sql @@ -0,0 +1,47 @@ +drop policy "Allow select and update if user is account's primary owner" on medreport.company_params; + +create policy "Allow select and update if user is account's HR" +on medreport.company_params +for all +using ( + EXISTS ( + SELECT 1 + FROM medreport.accounts_memberships am + WHERE am.account_id = company_params.account_id + AND am.user_id = auth.uid() + AND am.account_role = 'owner' + ) +) +with check ( + EXISTS ( + SELECT 1 + FROM medreport.accounts_memberships am + WHERE am.account_id = company_params.account_id + AND am.user_id = auth.uid() + AND am.account_role = 'owner' + ) +); + +create or replace function medreport.clear_benefit_amount_on_employee_deletion() +returns trigger +language plpgsql +security definer +set search_path = medreport, public +as $$ +begin + update medreport.account_balance_entries abe + set amount = 0 + where abe.account_id = old.user_id + AND abe.entry_type = 'benefit'; + + return null; +end; +$$; + +drop trigger if exists trigger_accounts_memberships_after_delete + on medreport.accounts_memberships; + +create trigger trigger_accounts_memberships_after_delete +after delete on medreport.accounts_memberships +for each row +execute function medreport.clear_benefit_amount_on_employee_deletion();