Merge branch 'develop' into MED-177
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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.' };
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: '' };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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$/, {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const UpdateEmployeeBenefitSchema = z.object({
|
||||
accountId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user