Merge branch 'develop' into MED-177

This commit is contained in:
Danel Kungla
2025-10-21 17:27:54 +03:00
131 changed files with 2202 additions and 921 deletions

View File

@@ -43,7 +43,7 @@ export async function renderBookTimeFailedEmail({
</Text>
<Text>
Broneeringu {reservationId} Connected Online'i saatmine ei
Broneeringu {reservationId} Connected Online&apos;i saatmine ei
õnnestunud, kliendile tuleb teha tagasimakse.
</Text>
<Text>Saadud error: {error}</Text>

View File

@@ -69,7 +69,7 @@ export async function renderNewJobsAvailableEmail({
</Text>
<ul className="list-none text-[16px] leading-[24px]">
{analysisResponseIds.map((analysisResponseId, index) => (
<li>
<li key={index}>
<Link
key={analysisResponseId}
href={`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisResponseId}`}

View File

@@ -0,0 +1,9 @@
{
"subject": "Teid on kutsutud tiimi",
"heading": "Liitu tiimiga {{teamName}}",
"hello": "Tere {{invitedUserEmail}},",
"mainText": "<strong>{{inviter}}</strong> on kutsunud teid ühinema tiimiga <strong>{{teamName}}</strong> platvormil <strong>{{productName}}</strong>.",
"joinTeam": "Liitu {{teamName}}",
"copyPasteLink": "või kopeeri ja kleebi see URL teie brauseris:",
"invitationIntendedFor": "See kutse on mõeldud {{invitedUserEmail}} omanikule."
}

View File

@@ -43,17 +43,9 @@ export function PersonalAccountDropdown({
showProfileName = true,
paths,
features,
account,
accounts = [],
}: {
user: User;
account?: {
id: string | null;
name: string | null;
picture_url: string | null;
application_role: ApplicationRole | null;
};
accounts: {
label: string | null;
value: string | null;
@@ -102,8 +94,8 @@ export function PersonalAccountDropdown({
const hasDoctorRole =
personalAccountData?.application_role === ApplicationRoleEnum.Doctor;
return hasDoctorRole && hasTotpFactor;
}, [personalAccountData, hasTotpFactor]);
return hasDoctorRole;
}, [personalAccountData]);
return (
<DropdownMenu>

View File

@@ -214,7 +214,7 @@ class AccountsApi {
.schema('medreport')
.from('accounts_memberships')
.select('account_id', { count: 'exact', head: true })
.eq('account_id', accountId);
.eq('user_id', accountId);
if (error) {
throw error;

View File

@@ -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<AccountBalanceSummary> {
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,
@@ -120,6 +134,22 @@ export class AccountBalanceService {
};
}
async upsertHealthBenefitsBySchedule(
benefitDistributionScheduleId: string,
): Promise<void> {
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<void> {
console.info('Processing periodic benefit distributions...');
const { error } = await this.supabase

View File

@@ -4,7 +4,6 @@ import { Tables } from '@kit/supabase/database';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createTeamAccountsApi } from '@kit/team-accounts/api';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
@@ -12,14 +11,6 @@ import { Heading } from '@kit/ui/heading';
import { If } from '@kit/ui/if';
import { PageBody, PageHeader } from '@kit/ui/page';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@kit/ui/table';
import { AdminBanUserDialog } from './admin-ban-user-dialog';
import { AdminDeleteAccountDialog } from './admin-delete-account-dialog';
@@ -224,148 +215,6 @@ async function TeamAccountPage(props: {
);
}
async function SubscriptionsTable(props: { accountId: string }) {
const client = getSupabaseServerClient();
const { data: subscription, error } = await client
.schema('medreport')
.from('subscriptions')
.select('*, subscription_items !inner (*)')
.eq('account_id', props.accountId)
.maybeSingle();
if (error) {
return (
<Alert variant={'destructive'}>
<AlertTitle>There was an error loading subscription.</AlertTitle>
<AlertDescription>
Please check the logs for more information or try again later.
</AlertDescription>
</Alert>
);
}
return (
<div className={'flex flex-col gap-y-1'}>
<Heading level={6}>Subscription</Heading>
<If
condition={subscription}
fallback={
<span className={'text-muted-foreground text-sm'}>
This account does not currently have a subscription.
</span>
}
>
{(subscription) => {
return (
<div className={'flex flex-col space-y-4'}>
<Table>
<TableHeader>
<TableHead>Subscription ID</TableHead>
<TableHead>Provider</TableHead>
<TableHead>Customer ID</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created At</TableHead>
<TableHead>Period Starts At</TableHead>
<TableHead>Ends At</TableHead>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>
<span>{subscription.id}</span>
</TableCell>
<TableCell>
<span>{subscription.billing_provider}</span>
</TableCell>
<TableCell>
<span>{subscription.billing_customer_id}</span>
</TableCell>
<TableCell>
<span>{subscription.status}</span>
</TableCell>
<TableCell>
<span>{subscription.created_at}</span>
</TableCell>
<TableCell>
<span>{subscription.period_starts_at}</span>
</TableCell>
<TableCell>
<span>{subscription.period_ends_at}</span>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Table>
<TableHeader>
<TableHead>Product ID</TableHead>
<TableHead>Variant ID</TableHead>
<TableHead>Quantity</TableHead>
<TableHead>Price</TableHead>
<TableHead>Interval</TableHead>
<TableHead>Type</TableHead>
</TableHeader>
<TableBody>
{subscription.subscription_items.map((item) => {
return (
<TableRow key={item.variant_id}>
<TableCell>
<span>{item.product_id}</span>
</TableCell>
<TableCell>
<span>{item.variant_id}</span>
</TableCell>
<TableCell>
<span>{item.quantity}</span>
</TableCell>
<TableCell>
<span>{item.price_amount}</span>
</TableCell>
<TableCell>
<span>{item.interval}</span>
</TableCell>
<TableCell>
<span>{item.type}</span>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}}
</If>
</div>
);
}
async function getMemberships(userId: string) {
const client = getSupabaseServerClient();

View File

@@ -46,8 +46,13 @@ export function MultiFactorChallengeContainer({
const router = useRouter();
const verifyMFAChallenge = useVerifyMFAChallenge({
onSuccess: () => {
router.replace(paths.redirectPath);
onSuccess: async () => {
try {
await fetch('/api/after-mfa', { method: 'POST' });
router.replace(paths.redirectPath);
} catch {
// ignore
}
},
});

View File

@@ -42,14 +42,14 @@ export const selectJobAction = doctorAction(
revalidateDoctorAnalysis();
return { success: true };
} catch (e) {
logger.error('Failed to select job', e);
if (e instanceof Error) {
} catch (error) {
logger.error({ error }, 'Failed to select job');
if (error instanceof Error) {
revalidateDoctorAnalysis();
return {
success: false,
reason:
e['message'] === ErrorReason.JOB_ASSIGNED
error['message'] === ErrorReason.JOB_ASSIGNED
? ErrorReason.JOB_ASSIGNED
: ErrorReason.UNKNOWN,
};
@@ -133,16 +133,16 @@ export const giveFeedbackAction = doctorAction(
}
return { success: true };
} catch (e: any) {
} catch (error) {
if (isCompleted) {
await createNotificationLog({
action: NotificationAction.PATIENT_DOCTOR_FEEDBACK_RECEIVED,
status: 'FAIL',
comment: e?.message,
comment: error instanceof Error ? error.message : '',
relatedRecordId: analysisOrderId,
});
}
logger.error('Failed to give feedback', e);
logger.error({ error }, 'Failed to give feedback');
return { success: false, reason: ErrorReason.UNKNOWN };
}
},

View File

@@ -62,7 +62,7 @@ export const getOpenResponsesAction = doctorAction(
const data = await getOpenResponses({ page, pageSize });
return { success: true, data };
} catch (error) {
logger.error(`Error fetching open analysis response jobs`, error);
logger.error({ error }, `Error fetching open analysis response jobs`);
return { success: false, error: 'Failed to fetch data from the server.' };
}
},

View File

@@ -47,7 +47,7 @@ export type Patient = z.infer<typeof PatientSchema>;
export const AnalysisResponsesSchema = z.object({
user_id: z.string(),
analysis_order_id: AnalysisOrderIdSchema,
analysis_order: AnalysisOrderIdSchema,
});
export type AnalysisResponses = z.infer<typeof AnalysisResponsesSchema>;
@@ -56,8 +56,8 @@ export const AnalysisResponseSchema = z.object({
analysis_response_id: z.number(),
analysis_element_original_id: z.string(),
unit: z.string().nullable(),
response_value: z.number(),
response_time: z.string(),
response_value: z.number().nullable(),
response_time: z.string().nullable(),
norm_upper: z.number().nullable(),
norm_upper_included: z.boolean().nullable(),
norm_lower: z.number().nullable(),
@@ -74,8 +74,8 @@ export const AnalysisResponseSchema = z.object({
analysis_response_id: z.number(),
analysis_element_original_id: z.string(),
unit: z.string().nullable(),
response_value: z.number(),
response_time: z.string(),
response_value: z.number().nullable(),
response_time: z.string().nullable(),
norm_upper: z.number().nullable(),
norm_upper_included: z.boolean().nullable(),
norm_lower: z.number().nullable(),

View File

@@ -47,8 +47,8 @@ export const ElementSchema = z.object({
analysis_response_id: z.number(),
analysis_element_original_id: z.string(),
unit: z.string().nullable(),
response_value: z.number(),
response_time: z.string(),
response_value: z.number().nullable(),
response_time: z.string().nullable(),
norm_upper: z.number().nullable(),
norm_upper_included: z.boolean().nullable(),
norm_lower: z.number().nullable(),

View File

@@ -1,8 +1,10 @@
import 'server-only';
import { listOrdersByIds, retrieveOrder } from '@lib/data/orders';
import { isBefore } from 'date-fns';
import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates';
import { getLogger } from '@kit/shared/logger';
import { getFullName } from '@kit/shared/utils';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createUserAnalysesApi } from '@kit/user-analyses/api';
@@ -31,7 +33,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
const [
{ data: doctorFeedbackItems },
{ data: medusaOrderItems },
medusaOrders,
{ data: analysisResponseElements },
{ data: accounts },
] = await Promise.all([
@@ -43,11 +45,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
'analysis_order_id',
analysisResponses.map((r) => r.analysis_order_id.id),
),
supabase
.schema('public')
.from('order_item')
.select('order_id, item_id(product_title, product_type)')
.in('order_id', medusaOrderIds),
listOrdersByIds(medusaOrderIds),
supabase
.schema('medreport')
.from('analysis_response_elements')
@@ -56,10 +54,15 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
supabase
.schema('medreport')
.from('accounts')
.select('name, last_name, id, primary_owner_user_id, preferred_locale')
.select('name,last_name,id,primary_owner_user_id,preferred_locale,slug')
.in('primary_owner_user_id', userIds),
]);
if (!analysisResponseElements || analysisResponseElements?.length === 0) {
console.info(`${analysisResponseIds} has no response elements`);
return [];
}
const doctorUserIds =
doctorFeedbackItems
?.map((item) => item.doctor_user_id)
@@ -69,7 +72,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
? await supabase
.schema('medreport')
.from('accounts')
.select('name, last_name, id, primary_owner_user_id, preferred_locale')
.select('name,last_name,id,primary_owner_user_id,preferred_locale,slug')
.in('primary_owner_user_id', doctorUserIds)
: { data: [] };
@@ -82,21 +85,26 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
) || [];
const firstSampleGivenAt = responseElements.length
? responseElements.reduce((earliest, current) =>
new Date(current.response_time) < new Date(earliest.response_time)
? current
: earliest,
)?.response_time
? responseElements.reduce((earliest, current) => {
if (current.response_time && earliest.response_time) {
if (
new Date(current.response_time) < new Date(earliest.response_time)
) {
return current;
}
return earliest;
}
return current;
}).response_time
: null;
const medusaOrder = medusaOrderItems?.find(
({ order_id }) =>
order_id === analysisResponse.analysis_order_id.medusa_order_id,
const medusaOrder = medusaOrders?.find(
({ id }) => id === analysisResponse.analysis_order_id.medusa_order_id,
);
const patientAccount = allAccounts?.find(
({ primary_owner_user_id }) =>
analysisResponse.user_id === primary_owner_user_id,
({ primary_owner_user_id, slug }) =>
analysisResponse.user_id === primary_owner_user_id && !slug,
);
const feedback = doctorFeedbackItems?.find(
@@ -110,9 +118,10 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
);
const order = {
title: medusaOrder?.item_id.product_title,
title: medusaOrder?.items?.[0]?.product_title,
isPackage:
medusaOrder?.item_id.product_type?.toLowerCase() === 'analysis package',
medusaOrder?.items?.[0]?.product_type?.toLowerCase() ===
'analysis package',
analysisOrderId: analysisResponse.analysis_order_id.id,
status: analysisResponse.order_status,
};
@@ -177,6 +186,7 @@ export async function getUserInProgressResponses({
`,
{ count: 'exact' },
)
.neq('status', 'ON_HOLD')
.in('analysis_order_id', analysisOrderIds)
.range(offset, offset + pageSize - 1)
.order('created_at', { ascending: false });
@@ -365,47 +375,50 @@ export async function getOtherResponses({
export async function getAnalysisResultsForDoctor(
analysisResponseId: number,
): Promise<AnalysisResultDetails> {
const logger = await getLogger();
const ctx = {
action: 'get-analysis-results-for-doctor',
analysisResponseId,
};
const supabase = getSupabaseServerClient();
const { data: analysisResponseElements, error } = await supabase
.schema('medreport')
.from(`analysis_response_elements`)
.select(
`*,
analysis_responses(user_id, analysis_order_id(id,medusa_order_id, analysis_element_ids))`,
)
.eq('analysis_response_id', analysisResponseId);
const { data: analysisResponsesData, error: analysisResponsesError } =
await supabase
.schema('medreport')
.from(`analysis_response_elements`)
.select(
`*,
analysis_responses(user_id, analysis_order:analysis_order_id(id,medusa_order_id, analysis_element_ids))`,
)
.eq('analysis_response_id', analysisResponseId);
if (error) {
throw new Error('Something went wrong.');
if (analysisResponsesError) {
logger.error(
{ ...ctx, analysisResponsesError },
'No order response for this analysis response id',
);
throw new Error('No order for this analysis id');
}
const firstAnalysisResponse = analysisResponseElements?.[0];
const firstAnalysisResponse = analysisResponsesData?.[0];
const userId = firstAnalysisResponse?.analysis_responses.user_id;
const medusaOrderId =
firstAnalysisResponse?.analysis_responses?.analysis_order_id
?.medusa_order_id;
firstAnalysisResponse?.analysis_responses?.analysis_order?.medusa_order_id;
if (!analysisResponseElements?.length || !userId || !medusaOrderId) {
if (!analysisResponsesData?.length || !userId || !medusaOrderId) {
throw new Error('Failed to retrieve full analysis data.');
}
const responseElementAnalysisElementOriginalIds =
analysisResponseElements.map(
({ analysis_element_original_id }) => analysis_element_original_id,
);
const responseElementAnalysisElementOriginalIds = analysisResponsesData.map(
({ analysis_element_original_id }) => analysis_element_original_id,
);
const [
{ data: medusaOrderItems, error: medusaOrderError },
medusaOrder,
{ data: accountWithParams, error: accountError },
{ data: doctorFeedback, error: feedbackError },
{ data: previousAnalyses, error: previousAnalysesError },
] = await Promise.all([
supabase
.schema('public')
.from('order_item')
.select(`order_id, item_id(product_title, product_type)`)
.eq('order_id', medusaOrderId),
retrieveOrder(medusaOrderId, true, '*items'),
supabase
.schema('medreport')
.from('accounts')
@@ -422,7 +435,7 @@ export async function getAnalysisResultsForDoctor(
.select(`*`)
.eq(
'analysis_order_id',
firstAnalysisResponse.analysis_responses.analysis_order_id.id,
firstAnalysisResponse.analysis_responses.analysis_order.id,
)
.limit(1),
supabase
@@ -452,12 +465,7 @@ export async function getAnalysisResultsForDoctor(
.order('response_time'),
]);
if (
medusaOrderError ||
accountError ||
feedbackError ||
previousAnalysesError
) {
if (!medusaOrder || accountError || feedbackError || previousAnalysesError) {
throw new Error('Something went wrong.');
}
@@ -478,15 +486,20 @@ export async function getAnalysisResultsForDoctor(
} = accountWithParams[0];
const analysisResponseElementsWithPreviousData = [];
for (const analysisResponseElement of analysisResponseElements) {
for (const analysisResponseElement of analysisResponsesData) {
const latestPreviousAnalysis = previousAnalyses.find(
({ analysis_element_original_id, response_time }) =>
analysis_element_original_id ===
analysisResponseElement.analysis_element_original_id &&
isBefore(
new Date(response_time),
new Date(analysisResponseElement.response_time),
),
({ analysis_element_original_id, response_time }) => {
if (response_time && analysisResponseElement.response_time) {
return (
analysis_element_original_id ===
analysisResponseElement.analysis_element_original_id &&
isBefore(
new Date(response_time),
new Date(analysisResponseElement.response_time),
)
);
}
},
);
analysisResponseElementsWithPreviousData.push({
...analysisResponseElement,
@@ -497,12 +510,12 @@ export async function getAnalysisResultsForDoctor(
return {
analysisResponse: analysisResponseElementsWithPreviousData,
order: {
title: medusaOrderItems?.[0]?.item_id.product_title ?? '-',
title: medusaOrder.items?.[0]?.product_title ?? '-',
isPackage:
medusaOrderItems?.[0]?.item_id.product_type?.toLowerCase() ===
medusaOrder.items?.[0]?.product_type?.toLowerCase() ===
'analysis package',
analysisOrderId:
firstAnalysisResponse.analysis_responses.analysis_order_id.id,
firstAnalysisResponse.analysis_responses.analysis_order.id,
},
doctorFeedback: doctorFeedback?.[0],
patient: {
@@ -525,8 +538,15 @@ export async function selectJob(analysisOrderId: number, userId: string) {
const {
data: { user },
} = await supabase.auth.getUser();
const logger = await getLogger();
const ctx = {
action: 'select-job',
patientUserId: userId,
currentUserId: user?.id,
};
if (!user?.id) {
logger.error(ctx, 'No user logged in');
throw new Error('No user logged in.');
}
@@ -541,6 +561,7 @@ export async function selectJob(analysisOrderId: number, userId: string) {
const jobAssignedToUserId = existingFeedback?.[0]?.doctor_user_id;
if (!!jobAssignedToUserId && jobAssignedToUserId !== user.id) {
logger.error(ctx, 'Job assigned to a different user');
throw new Error(ErrorReason.JOB_ASSIGNED);
}
@@ -557,6 +578,10 @@ export async function selectJob(analysisOrderId: number, userId: string) {
);
if (error || existingFeedbackError) {
logger.error(
{ ...ctx, error, existingFeedbackError },
'Failed updating doctor feedback',
);
throw new Error('Something went wrong');
}

View File

@@ -1,5 +1,5 @@
{
"extends": "@kit/tsconfig/base.json",
"extends": "../../../tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},

View File

@@ -23,7 +23,9 @@ import { getRegion } from './regions';
* @param cartId - optional - The ID of the cart to retrieve.
* @returns The cart object if found, or null if not found.
*/
export async function retrieveCart(cartId?: string) {
export async function retrieveCart(
cartId?: string,
): Promise<(StoreCart & { promotions: StoreCartPromotion[] }) | null> {
const id = cartId || (await getCartId());
if (!id) {
@@ -135,13 +137,21 @@ export async function addToCart({
quantity: number;
countryCode: string;
}) {
const logger = await getLogger();
const ctx = {
variantId,
quantity,
countryCode,
};
if (!variantId) {
logger.error(ctx, 'Missing variant ID when adding to cart');
throw new Error('Missing variant ID when adding to cart');
}
const cart = await getOrSetCart(countryCode);
if (!cart) {
logger.error(ctx, 'Error retrieving or creating cart');
throw new Error('Error retrieving or creating cart');
}

View File

@@ -6,7 +6,11 @@ import { HttpTypes } from '@medusajs/types';
import { getAuthHeaders, getCacheOptions } from './cookies';
export const retrieveOrder = async (id: string, allowCache = true) => {
export const retrieveOrder = async (
id: string,
allowCache = true,
fields = '*payment_collections.payments,*items,*items.metadata,*items.variant,*items.product',
) => {
const headers = {
...(await getAuthHeaders()),
};
@@ -19,8 +23,7 @@ export const retrieveOrder = async (id: string, allowCache = true) => {
.fetch<HttpTypes.StoreOrderResponse>(`/store/orders/${id}`, {
method: 'GET',
query: {
fields:
'*payment_collections.payments,*items,*items.metadata,*items.variant,*items.product',
fields,
},
headers,
next,
@@ -59,7 +62,18 @@ export const listOrders = async (
credentials: 'include',
})
.then(({ orders }) => orders)
.catch((err) => medusaError(err));
.catch((err) => {
console.error('Error receiving orders', { err });
return medusaError(err);
});
};
export const listOrdersByIds = async (ids: string[]) => {
try {
return Promise.all(ids.map((id) => retrieveOrder(id)));
} catch (error) {
console.error('response Error', error);
}
};
export const createTransferRequest = async (

View File

@@ -31,7 +31,7 @@ export function useNotificationsStream(params: {
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
schema: 'medreport',
filter: `account_id=in.(${params.accountIds.join(', ')})`,
table: 'notifications',
},

View File

@@ -50,4 +50,13 @@ class NotificationsApi {
createNotification(params: Notification['Insert']) {
return this.service.createNotification(params);
}
/**
* @name createNotification
* @description Create a new notification in the database
* @param params
*/
dismissNotification(eqValue: string, eqColumn?: string) {
return this.service.dismissNotification(eqColumn, eqValue);
}
}

View File

@@ -29,4 +29,21 @@ class NotificationsService {
throw error;
}
}
async dismissNotification(eqColumn = 'id', eqValue: string) {
const logger = await getLogger();
const { error } = await this.client
.schema('medreport')
.from('notifications')
.update({ dismissed: true })
.eq(eqColumn, eqValue);
if (error) {
logger.error(
{ eqColumn, eqValue },
`Could not dismiss notification: ${error.message}`,
);
throw error;
}
}
}

View File

@@ -16,7 +16,7 @@ export function CompanyGuard<Params extends object>(
Component: LayoutOrPageComponent<Params>,
) {
return async function AdminGuardServerComponentWrapper(params: Params) {
//@ts-ignore
// @ts-expect-error incorrectly typed params
const { account } = await params.params;
const client = getSupabaseServerClient();
const [isUserSuperAdmin, isUserCompanyAdmin] = await Promise.all([

View File

@@ -25,15 +25,17 @@ 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 =
Database['medreport']['Functions']['get_account_members']['Returns'];
interface Permissions {
canUpdateRole: (roleHierarchy: number) => boolean;
canUpdateRole: 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,23 +60,21 @@ export function AccountMembersTable({
isPrimaryOwner,
userRoleHierarchy,
canManageRoles,
canUpdateBenefit,
membersBenefitsUsage,
}: AccountMembersTableProps) {
const [search, setSearch] = useState('');
const { t } = useTranslation('teams');
const permissions = {
canUpdateRole: (targetRole: number) => {
return (
isPrimaryOwner || (canManageRoles && userRoleHierarchy < targetRole)
);
},
canUpdateRole: canManageRoles,
canRemoveFromAccount: (targetRole: number) => {
return (
isPrimaryOwner || (canManageRoles && userRoleHierarchy < targetRole)
);
},
canTransferOwnership: isPrimaryOwner,
canUpdateBenefit,
};
const columns = useGetColumns(permissions, {
@@ -211,8 +212,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 (
<span
@@ -221,16 +221,6 @@ function useGetColumns(
}
>
<RoleBadge role={role} />
<If condition={isPrimaryOwner}>
<span
className={
'rounded-md bg-yellow-400 px-2.5 py-1 text-xs font-medium dark:text-black'
}
>
{t('primaryOwnerLabel')}
</span>
</If>
</span>
);
},
@@ -248,7 +238,6 @@ function useGetColumns(
<ActionsDropdown
permissions={permissions}
member={row.original}
currentUserId={params.currentUserId}
currentTeamAccountId={params.currentAccountId}
currentRoleHierarchy={params.currentRoleHierarchy}
/>
@@ -262,29 +251,22 @@ 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);
const canRemoveFromAccount =
permissions.canRemoveFromAccount(memberRoleHierarchy);
@@ -292,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;
}
@@ -309,23 +292,29 @@ function ActionsDropdown({
</DropdownMenuTrigger>
<DropdownMenuContent>
<If condition={canUpdateRole}>
<If condition={permissions.canUpdateRole}>
<DropdownMenuItem onClick={() => setIsUpdatingRole(true)}>
<Trans i18nKey={'teams:updateRole'} />
</DropdownMenuItem>
</If>
<If condition={permissions.canTransferOwnership}>
<If condition={permissions.canTransferOwnership && !isPrimaryOwner}>
<DropdownMenuItem onClick={() => setIsTransferring(true)}>
<Trans i18nKey={'teams:transferOwnership'} />
</DropdownMenuItem>
</If>
<If condition={canRemoveFromAccount}>
<If condition={canRemoveFromAccount && !isPrimaryOwner}>
<DropdownMenuItem onClick={() => setIsRemoving(true)}>
<Trans i18nKey={'teams:removeMember'} />
</DropdownMenuItem>
</If>
<If condition={permissions.canUpdateBenefit}>
<DropdownMenuItem onClick={() => setIsUpdatingBenefit(true)}>
<Trans i18nKey={'teams:updateBenefit'} />
</DropdownMenuItem>
</If>
</DropdownMenuContent>
</DropdownMenu>
@@ -358,6 +347,16 @@ function ActionsDropdown({
userId={member.user_id}
/>
</If>
<If condition={isUpdatingBenefit}>
<UpdateEmployeeBenefitDialog
isOpen
setIsOpen={setIsUpdatingBenefit}
accountId={member.account_id}
userId={member.user_id}
isEligibleForBenefits={member.is_eligible_for_benefits}
/>
</If>
</>
);
}

View File

@@ -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<typeof createEmptyInviteModel>;
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({
<Dialog open={isOpen} onOpenChange={setIsOpen} modal>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent
className="max-w-[800px]"
onInteractOutside={(e) => e.preventDefault()}
>
<DialogContent onInteractOutside={(e) => e.preventDefault()}>
<DialogHeader>
<DialogTitle>
<Trans i18nKey={'teams:inviteMembersHeading'} />
@@ -81,10 +75,9 @@ export function InviteMembersDialogContainer({
</DialogHeader>
<RolesDataProvider maxRoleHierarchy={userRoleHierarchy}>
{(roles) => (
{() => (
<InviteMembersForm
pending={pending}
roles={roles}
onSubmit={(data) => {
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 (
<div data-test={'invite-member-form-item'} key={field.id}>
<div className={'flex items-end gap-x-1 md:space-x-2'}>
<div className={'w-4/12'}>
<div data-test="invite-member-form-item" key={field.id}>
<div className="flex items-end gap-x-1 md:space-x-2">
<div className="w-5/12">
<FormField
name={personalCodeInputName}
render={({ field }) => {
@@ -178,7 +168,7 @@ function InviteMembersForm({
}}
/>
</div>
<div className={'w-4/12'}>
<div className={'w-5/12'}>
<FormField
name={emailInputName}
render={({ field }) => {
@@ -205,37 +195,7 @@ function InviteMembersForm({
/>
</div>
<div className={'w-4/12'}>
<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'}>
<div className={'flex w-1/12 items-end justify-end'}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@@ -303,5 +263,5 @@ function InviteMembersForm({
}
function createEmptyInviteModel() {
return { email: '', role: 'member' as Role, personal_code: '' };
return { email: '', personal_code: '' };
}

View File

@@ -6,16 +6,19 @@ import { Trans } from '@kit/ui/trans';
type Role = string;
const roles = {
owner: '',
owner: 'bg-yellow-400 text-black',
member:
'bg-blue-50 hover:bg-blue-50 text-blue-500 dark:bg-blue-500/10 dark:hover:bg-blue-500/10',
'bg-blue-50 text-blue-500 dark:bg-blue-500/10 dark:hover:bg-blue-500/10',
};
const roleClassNameBuilder = cva('font-medium capitalize shadow-none', {
variants: {
role: roles,
const roleClassNameBuilder = cva(
'px-2.5 py-1 font-medium capitalize shadow-none',
{
variants: {
role: roles,
},
},
});
);
export function RoleBadge({ role }: { role: Role }) {
// @ts-expect-error: hard to type this since users can add custom roles

View File

@@ -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<boolean>();
const updateEmployeeBenefit = () => {
startTransition(async () => {
try {
await updateEmployeeBenefitAction({ accountId, userId });
setIsOpen(false);
router.refresh();
} catch {
setError(true);
}
});
};
return (
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Trans i18nKey="team:updateBenefitHeading" />
</AlertDialogTitle>
<AlertDialogDescription>
{isEligibleForBenefits ? (
<Trans i18nKey="team:removeBenefitDescription" />
) : (
<Trans i18nKey="team:allowBenefitDescription" />
)}
</AlertDialogDescription>
</AlertDialogHeader>
<If condition={error}>
<Alert variant="destructive">
<AlertDescription>
<Trans i18nKey="teams:updateBenefiErrorMessage" />
</AlertDescription>
</Alert>
</If>
<AlertDialogFooter>
<AlertDialogCancel>
<Trans i18nKey="common:cancel" />
</AlertDialogCancel>
<Button
data-test="update-member-benefit"
variant="destructive"
disabled={isSubmitting}
onClick={updateEmployeeBenefit}
>
{isEligibleForBenefits ? (
<Trans i18nKey="teams:removeBenefitSubmitLabel" />
) : (
<Trans i18nKey="teams:allowBenefitSubmitLabel" />
)}
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
export default UpdateEmployeeBenefitDialog;

View File

@@ -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$/, {

View File

@@ -0,0 +1,6 @@
import { z } from 'zod';
export const UpdateEmployeeBenefitSchema = z.object({
accountId: z.string().uuid(),
userId: z.string().uuid(),
});

View File

@@ -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';
@@ -149,7 +148,6 @@ export const updateInvitationAction = enhanceAction(
export const acceptInvitationAction = enhanceAction(
async (data: FormData, user) => {
const client = getSupabaseServerClient();
const accountBalanceService = new AccountBalanceService();
const { inviteToken, nextPath } = AcceptInvitationSchema.parse(
Object.fromEntries(data),
@@ -173,9 +171,6 @@ export const acceptInvitationAction = enhanceAction(
throw new Error('Failed to accept invitation');
}
// Make sure new account gets company benefits added to balance
await accountBalanceService.processPeriodicBenefitDistributions();
// Increase the seats for the account
await perSeatBillingService.increaseSeats(accountId);

View File

@@ -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 },
);

View File

@@ -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,
});

View File

@@ -1,9 +1,14 @@
import { SupabaseClient } from '@supabase/supabase-js';
import { createNotificationsApi } from '@kit/notifications/api';
import { pathsConfig } from '@kit/shared/config';
import { getLogger } from '@kit/shared/logger';
import type { UuringuVastus } from '@kit/shared/types/medipost-analysis';
import { toArray } from '@kit/shared/utils';
import { Database } from '@kit/supabase/database';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import type {
AnalysisOrder,
AnalysisOrderStatus,
@@ -463,13 +468,19 @@ class UserAnalysesApi {
medusaOrderId?: string;
orderStatus: AnalysisOrderStatus;
}) {
const logger = await getLogger();
const orderIdParam = orderId;
const medusaOrderIdParam = medusaOrderId;
const ctx = {
action: 'update-analysis-order-status',
orderId,
medusaOrderId,
orderStatus,
};
console.info(
`Updating order id=${orderId} medusaOrderId=${medusaOrderId} status=${orderStatus}`,
);
logger.info(ctx, 'Updating order');
if (!orderIdParam && !medusaOrderIdParam) {
logger.error(ctx, 'Missing orderId or medusaOrderId');
throw new Error('Either orderId or medusaOrderId must be provided');
}
await this.client
@@ -481,6 +492,39 @@ class UserAnalysesApi {
})
.throwOnError();
}
async sendAnalysisResultsNotification({
hasFullAnalysisResponse,
hasPartialAnalysisResponse,
analysisOrderId,
}: {
hasFullAnalysisResponse: boolean;
hasPartialAnalysisResponse: boolean;
analysisOrderId?: number;
}) {
if (!analysisOrderId) {
return;
}
const { data, error: userError } = await this.client.auth.getUser();
if (userError) {
throw userError;
}
const { user } = data;
const notificationsApi = createNotificationsApi(this.client);
const { t } = await createI18nServerInstance();
console.info(
`Order ${analysisOrderId} got new responses -> Sending new notification`,
);
if (hasFullAnalysisResponse || hasPartialAnalysisResponse) {
await notificationsApi.createNotification({
account_id: user.id,
body: t('analysis-results:notification.body'),
link: `${pathsConfig.app.analysisResults}/${analysisOrderId}`,
});
}
}
}
export function createUserAnalysesApi(client: SupabaseClient<Database>) {

View File

@@ -1,10 +1,7 @@
{
"extends": "@kit/tsconfig/base.json",
"extends": "../../../tsconfig.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"paths": {
"~/lib/utils": ["../../../lib/utils.ts"]
}
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "*.tsx", "src"],
"exclude": ["node_modules"]

View File

@@ -18,14 +18,16 @@ export const PackageHeader = ({
return (
<div className="space-y-1 text-center">
<p className="text-sm sm:text-lg sm:font-medium">{title}</p>
<h2 className="text-xl sm:text-4xl">
<h2 className="xs:text-xl text-lg sm:text-4xl">
{formatCurrency({
currencyCode: 'eur',
locale: language,
value: price,
})}
</h2>
<Badge className={cn('text-xs', tagColor)}>{analysesNr}</Badge>
<Badge className={cn('xs:text-xs text-[10px]', tagColor)}>
{analysesNr}
</Badge>
</div>
);
};

View File

@@ -46,11 +46,10 @@ export function ProfileAccountDropdownContainer(props: {
return (
<PersonalAccountDropdown
className={'w-full'}
className="w-full"
paths={paths}
features={features}
user={userData}
account={props.account}
accounts={props.accounts}
signOutRequested={() => signOut.mutateAsync()}
showProfileName={props.showProfileName}

View File

@@ -117,6 +117,7 @@ export default function SelectAnalysisPackage({
<Button
className="w-full text-[10px] sm:text-sm"
onClick={handleSelect}
disabled={isAddingToCart}
>
{isAddingToCart ? (
<Spinner />

View File

@@ -1,4 +1,4 @@
import React, { JSX, ReactNode } from 'react';
import React, { JSX } from 'react';
import { cn } from '@kit/ui/utils';

View File

@@ -1,7 +1,5 @@
import 'server-only';
import getBaseWebpackConfig from 'next/dist/build/webpack-config';
import {
AuthError,
type EmailOtpType,
@@ -9,6 +7,8 @@ import {
User,
} from '@supabase/supabase-js';
import { checkRequiresMultiFactorAuthentication } from './check-requires-mfa';
/**
* @name createAuthCallbackService
* @description Creates an instance of the AuthCallbackService
@@ -137,10 +137,12 @@ class AuthCallbackService {
| {
isSuccess: boolean;
user: User;
requiresMultiFactorAuthentication: boolean;
}
| ErrorURLParameters
> {
let user: User;
let requiresMultiFactorAuthentication: boolean;
try {
const { data, error } =
await this.client.auth.exchangeCodeForSession(authCode);
@@ -153,8 +155,14 @@ class AuthCallbackService {
});
}
// Handle Keycloak users - set up Medusa integration
if (data?.user && this.isKeycloakUser(data.user)) {
requiresMultiFactorAuthentication =
await checkRequiresMultiFactorAuthentication(this.client);
if (
!requiresMultiFactorAuthentication &&
data?.user &&
this.isKeycloakUser(data.user)
) {
await this.setupMedusaUserForKeycloak(data.user);
}
@@ -179,20 +187,21 @@ class AuthCallbackService {
return {
isSuccess: true,
user,
requiresMultiFactorAuthentication,
};
}
/**
* Check if user is from Keycloak provider
*/
private isKeycloakUser(user: any): boolean {
isKeycloakUser(user: any): boolean {
return (
user?.app_metadata?.provider === 'keycloak' ||
user?.app_metadata?.providers?.includes('keycloak')
);
}
private async setupMedusaUserForKeycloak(user: any): Promise<void> {
async setupMedusaUserForKeycloak(user: any): Promise<void> {
if (!user.email) {
console.warn('Keycloak user has no email, skipping Medusa setup');
return;
@@ -285,6 +294,7 @@ interface ErrorURLParameters {
error: string;
code?: string;
searchParams: string;
requiresMultiFactorAuthentication: boolean;
}
export function getErrorURLParameters({
@@ -313,6 +323,7 @@ export function getErrorURLParameters({
error: errorMessage,
code: code ?? '',
searchParams: searchParams.toString(),
requiresMultiFactorAuthentication: false,
};
}

View File

@@ -341,6 +341,7 @@ export type Database = {
Row: {
account_id: string
amount: number
benefit_distribution_schedule_id: string | null
created_at: string
created_by: string | null
description: string | null
@@ -348,14 +349,15 @@ export type Database = {
expires_at: string | null
id: string
is_active: boolean
is_analysis_order: boolean
is_analysis_package_order: boolean
is_analysis_order: boolean | null
is_analysis_package_order: boolean | null
reference_id: string | null
source_company_id: string | null
}
Insert: {
account_id: string
amount: number
benefit_distribution_schedule_id?: string | null
created_at?: string
created_by?: string | null
description?: string | null
@@ -363,14 +365,15 @@ export type Database = {
expires_at?: string | null
id?: string
is_active?: boolean
is_analysis_order?: boolean
is_analysis_package_order?: boolean
is_analysis_order?: boolean | null
is_analysis_package_order?: boolean | null
reference_id?: string | null
source_company_id?: string | null
}
Update: {
account_id?: string
amount?: number
benefit_distribution_schedule_id?: string | null
created_at?: string
created_by?: string | null
description?: string | null
@@ -378,8 +381,8 @@ export type Database = {
expires_at?: string | null
id?: string
is_active?: boolean
is_analysis_order?: boolean
is_analysis_package_order?: boolean
is_analysis_order?: boolean | null
is_analysis_package_order?: boolean | null
reference_id?: string | null
source_company_id?: string | null
}
@@ -554,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
@@ -565,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
@@ -576,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
@@ -2214,6 +2220,8 @@ export type Database = {
p_account_id: string
p_amount: number
p_description: string
p_is_analysis_order?: boolean
p_is_analysis_package_order?: boolean
p_reference_id?: string
}
Returns: boolean
@@ -2271,14 +2279,6 @@ export type Database = {
updated_by: string | null
}
}
distribute_health_benefits: {
Args: {
p_benefit_amount: number
p_benefit_occurrence?: string
p_company_id: string
}
Returns: undefined
}
get_account_balance: {
Args: { p_account_id: string }
Returns: number
@@ -2306,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
@@ -2317,14 +2318,12 @@ export type Database = {
}[]
}
get_benefits_usages_for_company_members: {
Args: {
p_account_id: string
}
Args: { p_account_id: string }
Returns: {
personal_account_id: string
benefit_amount: number
benefit_unused_amount: number
}
personal_account_id: string
}[]
}
get_config: {
Args: Record<PropertyKey, never>
@@ -2530,6 +2529,10 @@ export type Database = {
p_benefit_occurrence: string
p_company_id: string
}
Returns: string
}
upsert_health_benefits: {
Args: { p_benefit_distribution_schedule_id: string }
Returns: undefined
}
upsert_order: {
@@ -2619,6 +2622,7 @@ export type Database = {
| "settings.manage"
| "members.manage"
| "invites.manage"
| "benefit.manage"
application_role: "user" | "doctor" | "super_admin"
billing_provider: "stripe" | "lemon-squeezy" | "paddle" | "montonio"
connected_online_order_status:
@@ -8540,6 +8544,7 @@ export const Constants = {
"settings.manage",
"members.manage",
"invites.manage",
"benefit.manage",
],
application_role: ["user", "doctor", "super_admin"],
billing_provider: ["stripe", "lemon-squeezy", "paddle", "montonio"],

View File

@@ -37,7 +37,7 @@ function PageWithSidebar(props: PageProps) {
<div
className={
props.contentContainerClassName ??
'mx-auto flex h-screen w-full flex-col bg-inherit'
'mx-auto flex w-full flex-col bg-inherit'
}
>
{MobileNavigation}
@@ -71,7 +71,7 @@ function PageWithHeader(props: PageProps) {
const { Navigation, Children, MobileNavigation } = getSlotsFromPage(props);
return (
<div className={cn('z-900 flex h-screen flex-1 flex-col', props.className)}>
<div className={cn('flex h-screen flex-1 flex-col', props.className)}>
<div
className={
props.contentContainerClassName ?? 'flex flex-1 flex-col space-y-4'
@@ -81,7 +81,7 @@ function PageWithHeader(props: PageProps) {
className={cn(
'bg-bg-background light:border-border dark:border-border dark:shadow-primary/10 flex h-15 items-center justify-between border-1 border-b px-4 py-1 lg:justify-start lg:shadow-xs',
{
'sticky top-0 z-1000 backdrop-blur-md': props.sticky ?? true,
'sticky top-0 z-1000': props.sticky ?? true,
},
)}
>