@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import { Table, TableBody } from '@kit/ui/shadcn/table';
|
import { Table, TableBody } from '@kit/ui/shadcn/table';
|
||||||
|
|
||||||
import MobileCartRow from './mobile-cart-row';
|
import MobileTableRow from './mobile-table-row';
|
||||||
|
|
||||||
const MobileCartItems = ({
|
const MobileCartItems = ({
|
||||||
item,
|
item,
|
||||||
@@ -24,12 +24,12 @@ const MobileCartItems = ({
|
|||||||
return (
|
return (
|
||||||
<Table className="border-separate rounded-lg border p-2">
|
<Table className="border-separate rounded-lg border p-2">
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<MobileCartRow
|
<MobileTableRow
|
||||||
titleKey={productColumnLabelKey}
|
titleKey={productColumnLabelKey}
|
||||||
value={item.product_title}
|
value={item.product_title}
|
||||||
/>
|
/>
|
||||||
<MobileCartRow titleKey="cart:table.time" value={item.quantity} />
|
<MobileTableRow titleKey="cart:table.time" value={item.quantity} />
|
||||||
<MobileCartRow
|
<MobileTableRow
|
||||||
titleKey="cart:table.price"
|
titleKey="cart:table.price"
|
||||||
value={formatCurrency({
|
value={formatCurrency({
|
||||||
value: item.unit_price,
|
value: item.unit_price,
|
||||||
@@ -37,7 +37,7 @@ const MobileCartItems = ({
|
|||||||
locale: language,
|
locale: language,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<MobileCartRow
|
<MobileTableRow
|
||||||
titleKey="cart:table.total"
|
titleKey="cart:table.total"
|
||||||
value={
|
value={
|
||||||
item.total &&
|
item.total &&
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Button } from '@kit/ui/shadcn/button';
|
|||||||
import { Table, TableBody, TableCell, TableRow } from '@kit/ui/shadcn/table';
|
import { Table, TableBody, TableCell, TableRow } from '@kit/ui/shadcn/table';
|
||||||
|
|
||||||
import CartItemDelete from './cart-item-delete';
|
import CartItemDelete from './cart-item-delete';
|
||||||
import MobileCartRow from './mobile-cart-row';
|
import MobileTableRow from './mobile-table-row';
|
||||||
import { EnrichedCartItem } from './types';
|
import { EnrichedCartItem } from './types';
|
||||||
|
|
||||||
const MobileCartServiceItems = ({
|
const MobileCartServiceItems = ({
|
||||||
@@ -31,20 +31,20 @@ const MobileCartServiceItems = ({
|
|||||||
return (
|
return (
|
||||||
<Table className="border-separate rounded-lg border p-2">
|
<Table className="border-separate rounded-lg border p-2">
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<MobileCartRow
|
<MobileTableRow
|
||||||
titleKey={productColumnLabelKey}
|
titleKey={productColumnLabelKey}
|
||||||
value={item.product_title}
|
value={item.product_title}
|
||||||
/>
|
/>
|
||||||
<MobileCartRow
|
<MobileTableRow
|
||||||
titleKey="cart:table.time"
|
titleKey="cart:table.time"
|
||||||
value={formatDateAndTime(item.reservation.startTime.toString())}
|
value={formatDateAndTime(item.reservation.startTime.toString())}
|
||||||
/>
|
/>
|
||||||
<MobileCartRow
|
<MobileTableRow
|
||||||
titleKey="cart:table.location"
|
titleKey="cart:table.location"
|
||||||
value={item.reservation.location?.address ?? '-'}
|
value={item.reservation.location?.address ?? '-'}
|
||||||
/>
|
/>
|
||||||
<MobileCartRow titleKey="cart:table.quantity" value={item.quantity} />
|
<MobileTableRow titleKey="cart:table.quantity" value={item.quantity} />
|
||||||
<MobileCartRow
|
<MobileTableRow
|
||||||
titleKey="cart:table.price"
|
titleKey="cart:table.price"
|
||||||
value={formatCurrency({
|
value={formatCurrency({
|
||||||
value: item.unit_price,
|
value: item.unit_price,
|
||||||
@@ -52,7 +52,7 @@ const MobileCartServiceItems = ({
|
|||||||
locale: language,
|
locale: language,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<MobileCartRow
|
<MobileTableRow
|
||||||
titleKey="cart:table.total"
|
titleKey="cart:table.total"
|
||||||
value={
|
value={
|
||||||
item.total &&
|
item.total &&
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import { Trans } from '@kit/ui/makerkit/trans';
|
import { Trans } from '@kit/ui/makerkit/trans';
|
||||||
import { TableCell, TableHead, TableRow } from '@kit/ui/shadcn/table';
|
import { TableCell, TableHead, TableRow } from '@kit/ui/shadcn/table';
|
||||||
|
|
||||||
const MobileCartRow = ({
|
const MobleTableRow = ({
|
||||||
titleKey,
|
titleKey,
|
||||||
value,
|
value,
|
||||||
}: {
|
}: {
|
||||||
@@ -16,14 +16,9 @@ const MobileCartRow = ({
|
|||||||
</TableHead>
|
</TableHead>
|
||||||
|
|
||||||
<TableCell className="p-0 text-right">
|
<TableCell className="p-0 text-right">
|
||||||
<p
|
<p className="txt-medium-plus text-ui-fg-base">{value}</p>
|
||||||
className="txt-medium-plus text-ui-fg-base"
|
|
||||||
data-testid="product-title"
|
|
||||||
>
|
|
||||||
{value}
|
|
||||||
</p>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default MobileCartRow;
|
export default MobleTableRow;
|
||||||
@@ -51,7 +51,7 @@ export default function OrderBlock({
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col sm:gap-4">
|
||||||
{analysisOrder && (
|
{analysisOrder && (
|
||||||
<OrderItemsTable
|
<OrderItemsTable
|
||||||
items={itemsAnalysisPackage}
|
items={itemsAnalysisPackage}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
import type { Order } from '~/lib/types/order';
|
import type { Order } from '~/lib/types/order';
|
||||||
|
|
||||||
import { cancelTtoBooking } from '../../_lib/server/actions';
|
import { cancelTtoBooking } from '../../_lib/server/actions';
|
||||||
|
import MobileTableRow from '../cart/mobile-table-row';
|
||||||
import { logAnalysisResultsNavigateAction } from './actions';
|
import { logAnalysisResultsNavigateAction } from './actions';
|
||||||
|
|
||||||
export type OrderItemType = 'analysisOrder' | 'ttoService';
|
export type OrderItemType = 'analysisOrder' | 'ttoService';
|
||||||
@@ -60,76 +61,130 @@ export default function OrderItemsTable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className="border-separate rounded-lg border">
|
<>
|
||||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
<Table className="border-separate rounded-lg border p-2 sm:hidden">
|
||||||
<TableRow>
|
<TableBody>
|
||||||
<TableHead className="px-6">
|
{items
|
||||||
<Trans i18nKey={title} />
|
.sort((a, b) =>
|
||||||
</TableHead>
|
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||||
<TableHead className="px-6">
|
)
|
||||||
<Trans i18nKey="orders:table.createdAt" />
|
.map((orderItem) => (
|
||||||
</TableHead>
|
<div key={`${orderItem.id}-mobile`}>
|
||||||
{order.location && (
|
<MobileTableRow
|
||||||
|
titleKey={title}
|
||||||
|
value={orderItem.product_title || ''}
|
||||||
|
/>
|
||||||
|
<MobileTableRow
|
||||||
|
titleKey="orders:table.createdAt"
|
||||||
|
value={formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')}
|
||||||
|
/>
|
||||||
|
{order.location && (
|
||||||
|
<MobileTableRow
|
||||||
|
titleKey="orders:table.location"
|
||||||
|
value={order.location}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<MobileTableRow
|
||||||
|
titleKey="orders:table.status"
|
||||||
|
value={
|
||||||
|
isPackage
|
||||||
|
? `orders:status.analysisPackageOrder.${order?.status ?? 'CONFIRMED'}`
|
||||||
|
: `orders:status.${type}.${order?.status ?? 'CONFIRMED'}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell />
|
||||||
|
<TableCell className="flex w-full items-center justify-end p-0 pt-2">
|
||||||
|
<Button size="sm" onClick={openDetailedView}>
|
||||||
|
<Trans i18nKey="analysis-results:view" />
|
||||||
|
</Button>
|
||||||
|
{isTtoservice && order.bookingCode && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="bg-warning/90 hover:bg-warning"
|
||||||
|
onClick={() => setIsConfirmOpen(true)}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="analysis-results:cancel" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<Table className="hidden border-separate rounded-lg border sm:block">
|
||||||
|
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||||
|
<TableRow>
|
||||||
<TableHead className="px-6">
|
<TableHead className="px-6">
|
||||||
<Trans i18nKey="orders:table.location" />
|
<Trans i18nKey={title} />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
)}
|
<TableHead className="px-6">
|
||||||
<TableHead className="px-6">
|
<Trans i18nKey="orders:table.createdAt" />
|
||||||
<Trans i18nKey="orders:table.status" />
|
</TableHead>
|
||||||
</TableHead>
|
{order.location && (
|
||||||
{isAnalysisOrder && <TableHead className="px-6"></TableHead>}
|
<TableHead className="px-6">
|
||||||
</TableRow>
|
<Trans i18nKey="orders:table.location" />
|
||||||
</TableHeader>
|
</TableHead>
|
||||||
<TableBody>
|
)}
|
||||||
{items
|
<TableHead className="px-6">
|
||||||
.sort((a, b) =>
|
<Trans i18nKey="orders:table.status" />
|
||||||
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
</TableHead>
|
||||||
)
|
{isAnalysisOrder && <TableHead className="px-6"></TableHead>}
|
||||||
.map((orderItem) => (
|
</TableRow>
|
||||||
<TableRow className="w-full" key={orderItem.id}>
|
</TableHeader>
|
||||||
<TableCell className="w-[100%] px-6 text-left">
|
<TableBody>
|
||||||
<p className="txt-medium-plus text-ui-fg-base">
|
{items
|
||||||
{orderItem.product_title}
|
.sort((a, b) =>
|
||||||
</p>
|
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||||
</TableCell>
|
)
|
||||||
|
.map((orderItem) => (
|
||||||
<TableCell className="px-6 whitespace-nowrap">
|
<TableRow className="w-full" key={orderItem.id}>
|
||||||
{formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')}
|
<TableCell className="w-[100%] px-6 text-left">
|
||||||
</TableCell>
|
<p className="txt-medium-plus text-ui-fg-base">
|
||||||
{order.location && (
|
{orderItem.product_title}
|
||||||
<TableCell className="min-w-[180px] px-6">
|
</p>
|
||||||
{order.location}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
|
||||||
<TableCell className="min-w-[180px] px-6">
|
|
||||||
{isPackage ? (
|
|
||||||
<Trans
|
|
||||||
i18nKey={`orders:status.analysisPackageOrder.${order?.status ?? 'CONFIRMED'}`}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Trans
|
|
||||||
i18nKey={`orders:status.${type}.${order?.status ?? 'CONFIRMED'}`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell className="px-6 text-right">
|
<TableCell className="px-6 whitespace-nowrap">
|
||||||
<Button size="sm" onClick={openDetailedView}>
|
{formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')}
|
||||||
<Trans i18nKey="analysis-results:view" />
|
</TableCell>
|
||||||
</Button>
|
{order.location && (
|
||||||
{isTtoservice && order.bookingCode && (
|
<TableCell className="min-w-[180px] px-6">
|
||||||
<Button
|
{order.location}
|
||||||
size="sm"
|
</TableCell>
|
||||||
className="bg-warning/90 hover:bg-warning mt-2 w-full"
|
|
||||||
onClick={() => setIsConfirmOpen(true)}
|
|
||||||
>
|
|
||||||
<Trans i18nKey="analysis-results:cancel" />
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
<TableCell className="min-w-[180px] px-6">
|
||||||
</TableRow>
|
{isPackage ? (
|
||||||
))}
|
<Trans
|
||||||
</TableBody>
|
i18nKey={`orders:status.analysisPackageOrder.${order?.status ?? 'CONFIRMED'}`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Trans
|
||||||
|
i18nKey={`orders:status.${type}.${order?.status ?? 'CONFIRMED'}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="px-6 text-right">
|
||||||
|
<Button size="sm" onClick={openDetailedView}>
|
||||||
|
<Trans i18nKey="analysis-results:view" />
|
||||||
|
</Button>
|
||||||
|
{isTtoservice && order.bookingCode && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="bg-warning/90 hover:bg-warning mt-2 w-full"
|
||||||
|
onClick={() => setIsConfirmOpen(true)}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="analysis-results:cancel" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
{order?.bookingCode && order?.clinicId && (
|
{order?.bookingCode && order?.clinicId && (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={isConfirmOpen}
|
isOpen={isConfirmOpen}
|
||||||
@@ -141,6 +196,6 @@ export default function OrderItemsTable({
|
|||||||
descriptionKey="orders:confirmBookingCancel.description"
|
descriptionKey="orders:confirmBookingCancel.description"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Table>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ export function PersonalAccountDropdown({
|
|||||||
const hasDoctorRole =
|
const hasDoctorRole =
|
||||||
personalAccountData?.application_role === ApplicationRoleEnum.Doctor;
|
personalAccountData?.application_role === ApplicationRoleEnum.Doctor;
|
||||||
|
|
||||||
return hasDoctorRole && hasTotpFactor;
|
return hasDoctorRole;
|
||||||
}, [personalAccountData, hasTotpFactor]);
|
}, [personalAccountData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
import type { AccountBalanceEntry } from '../../types/account-balance-entry';
|
import type { AccountBalanceEntry } from '../../types/account-balance-entry';
|
||||||
|
import { createAccountsApi } from '../api';
|
||||||
|
|
||||||
export type AccountBalanceSummary = {
|
export type AccountBalanceSummary = {
|
||||||
totalBalance: number;
|
totalBalance: number;
|
||||||
@@ -88,6 +89,11 @@ export class AccountBalanceService {
|
|||||||
* Get balance summary for dashboard display
|
* Get balance summary for dashboard display
|
||||||
*/
|
*/
|
||||||
async getBalanceSummary(accountId: string): Promise<AccountBalanceSummary> {
|
async getBalanceSummary(accountId: string): Promise<AccountBalanceSummary> {
|
||||||
|
const api = createAccountsApi(this.supabase);
|
||||||
|
|
||||||
|
const hasAccountTeamMembership =
|
||||||
|
await api.hasAccountTeamMembership(accountId);
|
||||||
|
|
||||||
const [balance, entries] = await Promise.all([
|
const [balance, entries] = await Promise.all([
|
||||||
this.getAccountBalance(accountId),
|
this.getAccountBalance(accountId),
|
||||||
this.getAccountBalanceEntries(accountId, { limit: 5 }),
|
this.getAccountBalanceEntries(accountId, { limit: 5 }),
|
||||||
@@ -113,6 +119,14 @@ export class AccountBalanceService {
|
|||||||
const expiringSoon =
|
const expiringSoon =
|
||||||
expiringData?.reduce((sum, entry) => sum + (entry.amount || 0), 0) || 0;
|
expiringData?.reduce((sum, entry) => sum + (entry.amount || 0), 0) || 0;
|
||||||
|
|
||||||
|
if (!hasAccountTeamMembership) {
|
||||||
|
return {
|
||||||
|
totalBalance: 0,
|
||||||
|
expiringSoon,
|
||||||
|
recentEntries: entries.entries,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalBalance: balance,
|
totalBalance: balance,
|
||||||
expiringSoon,
|
expiringSoon,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ type Members =
|
|||||||
Database['medreport']['Functions']['get_account_members']['Returns'];
|
Database['medreport']['Functions']['get_account_members']['Returns'];
|
||||||
|
|
||||||
interface Permissions {
|
interface Permissions {
|
||||||
canUpdateRole: (roleHierarchy: number) => boolean;
|
canUpdateRole: boolean;
|
||||||
canRemoveFromAccount: (roleHierarchy: number) => boolean;
|
canRemoveFromAccount: (roleHierarchy: number) => boolean;
|
||||||
canTransferOwnership: boolean;
|
canTransferOwnership: boolean;
|
||||||
canUpdateBenefit: boolean;
|
canUpdateBenefit: boolean;
|
||||||
@@ -67,11 +67,7 @@ export function AccountMembersTable({
|
|||||||
const { t } = useTranslation('teams');
|
const { t } = useTranslation('teams');
|
||||||
|
|
||||||
const permissions = {
|
const permissions = {
|
||||||
canUpdateRole: (targetRole: number) => {
|
canUpdateRole: canManageRoles,
|
||||||
return (
|
|
||||||
isPrimaryOwner || (canManageRoles && userRoleHierarchy < targetRole)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
canRemoveFromAccount: (targetRole: number) => {
|
canRemoveFromAccount: (targetRole: number) => {
|
||||||
return (
|
return (
|
||||||
isPrimaryOwner || (canManageRoles && userRoleHierarchy < targetRole)
|
isPrimaryOwner || (canManageRoles && userRoleHierarchy < targetRole)
|
||||||
@@ -271,7 +267,6 @@ function ActionsDropdown({
|
|||||||
const isPrimaryOwner = member.primary_owner_user_id === member.user_id;
|
const isPrimaryOwner = member.primary_owner_user_id === member.user_id;
|
||||||
|
|
||||||
const memberRoleHierarchy = member.role_hierarchy_level;
|
const memberRoleHierarchy = member.role_hierarchy_level;
|
||||||
const canUpdateRole = permissions.canUpdateRole(memberRoleHierarchy);
|
|
||||||
|
|
||||||
const canRemoveFromAccount =
|
const canRemoveFromAccount =
|
||||||
permissions.canRemoveFromAccount(memberRoleHierarchy);
|
permissions.canRemoveFromAccount(memberRoleHierarchy);
|
||||||
@@ -279,9 +274,10 @@ function ActionsDropdown({
|
|||||||
// if has no permission to update role, transfer ownership or remove from account
|
// if has no permission to update role, transfer ownership or remove from account
|
||||||
// do not render the dropdown menu
|
// do not render the dropdown menu
|
||||||
if (
|
if (
|
||||||
!canUpdateRole &&
|
!permissions.canUpdateRole &&
|
||||||
!permissions.canTransferOwnership &&
|
!permissions.canTransferOwnership &&
|
||||||
!canRemoveFromAccount
|
!canRemoveFromAccount &&
|
||||||
|
!permissions.canUpdateBenefit
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -296,7 +292,7 @@ function ActionsDropdown({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<If condition={canUpdateRole && !isPrimaryOwner}>
|
<If condition={permissions.canUpdateRole}>
|
||||||
<DropdownMenuItem onClick={() => setIsUpdatingRole(true)}>
|
<DropdownMenuItem onClick={() => setIsUpdatingRole(true)}>
|
||||||
<Trans i18nKey={'teams:updateRole'} />
|
<Trans i18nKey={'teams:updateRole'} />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -37,13 +37,10 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
|
|
||||||
import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
import { InviteMembersSchema } from '../../schema/invite-members.schema';
|
||||||
import { createInvitationsAction } from '../../server/actions/team-invitations-server-actions';
|
import { createInvitationsAction } from '../../server/actions/team-invitations-server-actions';
|
||||||
import { MembershipRoleSelector } from './membership-role-selector';
|
|
||||||
import { RolesDataProvider } from './roles-data-provider';
|
import { RolesDataProvider } from './roles-data-provider';
|
||||||
|
|
||||||
type InviteModel = ReturnType<typeof createEmptyInviteModel>;
|
type InviteModel = ReturnType<typeof createEmptyInviteModel>;
|
||||||
|
|
||||||
type Role = string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The maximum number of invites that can be sent at once.
|
* The maximum number of invites that can be sent at once.
|
||||||
* Useful to avoid spamming the server with too large payloads
|
* Useful to avoid spamming the server with too large payloads
|
||||||
@@ -66,10 +63,7 @@ export function InviteMembersDialogContainer({
|
|||||||
<Dialog open={isOpen} onOpenChange={setIsOpen} modal>
|
<Dialog open={isOpen} onOpenChange={setIsOpen} modal>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent
|
<DialogContent onInteractOutside={(e) => e.preventDefault()}>
|
||||||
className="max-w-[800px]"
|
|
||||||
onInteractOutside={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans i18nKey={'teams:inviteMembersHeading'} />
|
<Trans i18nKey={'teams:inviteMembersHeading'} />
|
||||||
@@ -81,10 +75,9 @@ export function InviteMembersDialogContainer({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<RolesDataProvider maxRoleHierarchy={userRoleHierarchy}>
|
<RolesDataProvider maxRoleHierarchy={userRoleHierarchy}>
|
||||||
{(roles) => (
|
{() => (
|
||||||
<InviteMembersForm
|
<InviteMembersForm
|
||||||
pending={pending}
|
pending={pending}
|
||||||
roles={roles}
|
|
||||||
onSubmit={(data) => {
|
onSubmit={(data) => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
const promise = createInvitationsAction({
|
const promise = createInvitationsAction({
|
||||||
@@ -111,12 +104,10 @@ export function InviteMembersDialogContainer({
|
|||||||
|
|
||||||
function InviteMembersForm({
|
function InviteMembersForm({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
roles,
|
|
||||||
pending,
|
pending,
|
||||||
}: {
|
}: {
|
||||||
onSubmit: (data: { invitations: InviteModel[] }) => void;
|
onSubmit: (data: { invitations: InviteModel[] }) => void;
|
||||||
pending: boolean;
|
pending: boolean;
|
||||||
roles: string[];
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('teams');
|
const { t } = useTranslation('teams');
|
||||||
|
|
||||||
@@ -148,12 +139,11 @@ function InviteMembersForm({
|
|||||||
const personalCodeInputName =
|
const personalCodeInputName =
|
||||||
`invitations.${index}.personal_code` as const;
|
`invitations.${index}.personal_code` as const;
|
||||||
const emailInputName = `invitations.${index}.email` as const;
|
const emailInputName = `invitations.${index}.email` as const;
|
||||||
const roleInputName = `invitations.${index}.role` as const;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-test={'invite-member-form-item'} key={field.id}>
|
<div data-test="invite-member-form-item" key={field.id}>
|
||||||
<div className={'flex items-end gap-x-1 md:space-x-2'}>
|
<div className="flex items-end gap-x-1 md:space-x-2">
|
||||||
<div className={'w-4/12'}>
|
<div className="w-5/12">
|
||||||
<FormField
|
<FormField
|
||||||
name={personalCodeInputName}
|
name={personalCodeInputName}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
@@ -178,7 +168,7 @@ function InviteMembersForm({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'w-4/12'}>
|
<div className={'w-5/12'}>
|
||||||
<FormField
|
<FormField
|
||||||
name={emailInputName}
|
name={emailInputName}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
@@ -205,37 +195,7 @@ function InviteMembersForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'w-4/12'}>
|
<div className={'flex w-1/12 items-end justify-end'}>
|
||||||
<FormField
|
|
||||||
name={roleInputName}
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<If condition={isFirst}>
|
|
||||||
<FormLabel>
|
|
||||||
<Trans i18nKey={'teams:roleLabel'} />
|
|
||||||
</FormLabel>
|
|
||||||
</If>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<MembershipRoleSelector
|
|
||||||
triggerClassName={'m-0'}
|
|
||||||
roles={roles}
|
|
||||||
value={field.value}
|
|
||||||
onChange={(role) => {
|
|
||||||
form.setValue(field.name, role);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'flex w-[40px] items-end justify-end'}>
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -303,5 +263,5 @@ function InviteMembersForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createEmptyInviteModel() {
|
function createEmptyInviteModel() {
|
||||||
return { email: '', role: 'member' as Role, personal_code: '' };
|
return { email: '', personal_code: '' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
const InviteSchema = z.object({
|
const InviteSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
role: z.string().min(1).max(100),
|
|
||||||
personal_code: z
|
personal_code: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^[1-6]\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}\d$/, {
|
.regex(/^[1-6]\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}\d$/, {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { redirect } from 'next/navigation';
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { AccountBalanceService } from '@kit/accounts/services/account-balance.service';
|
|
||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
import { createNotificationsApi } from '@kit/notifications/api';
|
import { createNotificationsApi } from '@kit/notifications/api';
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
|
|||||||
@@ -191,7 +191,10 @@ class AccountInvitationsService {
|
|||||||
const response = await this.client
|
const response = await this.client
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.rpc('add_invitations_to_account', {
|
.rpc('add_invitations_to_account', {
|
||||||
invitations,
|
invitations: invitations.map((invitation) => ({
|
||||||
|
...invitation,
|
||||||
|
role: 'member',
|
||||||
|
})),
|
||||||
account_slug: accountSlug,
|
account_slug: accountSlug,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
47
supabase/migrations/20251009180300_fix_member_management.sql
Normal file
47
supabase/migrations/20251009180300_fix_member_management.sql
Normal file
@@ -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();
|
||||||
Reference in New Issue
Block a user