Merge branch 'develop' into MED-177
This commit is contained in:
@@ -20,6 +20,7 @@ EMAIL_PORT= # or 465 for SSL
|
|||||||
EMAIL_TLS= # or false for SSL (see provider documentation)
|
EMAIL_TLS= # or false for SSL (see provider documentation)
|
||||||
|
|
||||||
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=
|
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=
|
||||||
|
MEDUSA_SECRET_API_KEY=
|
||||||
|
|
||||||
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||||
MONTONIO_SECRET_KEY=rNZkzwxOiH93mzkdV53AvhSsbGidrgO2Kl5lE/IT7cvo
|
MONTONIO_SECRET_KEY=rNZkzwxOiH93mzkdV53AvhSsbGidrgO2Kl5lE/IT7cvo
|
||||||
|
|||||||
@@ -98,13 +98,13 @@ To access admin pages follow these steps:
|
|||||||
- Register new user
|
- Register new user
|
||||||
- Go to Profile and add Multi-Factor Authentication
|
- Go to Profile and add Multi-Factor Authentication
|
||||||
- Authenticate with mfa (at current time profile page prompts it again)
|
- Authenticate with mfa (at current time profile page prompts it again)
|
||||||
- update your role. look at `supabase/sql/super-admin.sql`
|
- update your `account.application_role` to `super_admin`.
|
||||||
- Sign out and Sign in
|
- Sign out and Sign in
|
||||||
|
|
||||||
## Company User
|
## Company User
|
||||||
|
|
||||||
- With admin account go to `http://localhost:3000/admin/accounts`
|
- With admin account go to `http://localhost:3000/admin/accounts`
|
||||||
- For Create Company Account to work you need to have rows in `medreport.roles` table. For that you can sql in `supabase/sql/super-admin.sql`
|
- For Create Company Account to work you need to have rows in `medreport.roles` table.
|
||||||
|
|
||||||
## Start email server
|
## Start email server
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import { Label } from '@kit/ui/label';
|
|||||||
import { Spinner } from '@kit/ui/spinner';
|
import { Spinner } from '@kit/ui/spinner';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { sendCompanyOfferEmail } from '../_lib/server/company-offer-actions';
|
||||||
|
|
||||||
const CompanyOfferForm = () => {
|
const CompanyOfferForm = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -34,6 +36,16 @@ const CompanyOfferForm = () => {
|
|||||||
|
|
||||||
const onSubmit = async (data: CompanySubmitData) => {
|
const onSubmit = async (data: CompanySubmitData) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await sendCompanyOfferEmail(data, language);
|
||||||
|
router.push('/company-offer/success');
|
||||||
|
} catch (err) {
|
||||||
|
setIsLoading(false);
|
||||||
|
if (err instanceof Error) {
|
||||||
|
console.warn('Could not send company offer email: ' + err.message);
|
||||||
|
}
|
||||||
|
console.warn('Could not send company offer email: ', err);
|
||||||
|
}
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
Object.entries(data).forEach(([key, value]) => {
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
if (value !== undefined) formData.append(key, value);
|
if (value !== undefined) formData.append(key, value);
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { renderCompanyOfferEmail } from '@/packages/email-templates/src';
|
||||||
|
|
||||||
|
import { sendEmailFromTemplate } from '~/lib/services/mailer.service';
|
||||||
|
import { CompanySubmitData } from '~/lib/types/company';
|
||||||
|
|
||||||
|
export const sendCompanyOfferEmail = async (
|
||||||
|
data: CompanySubmitData,
|
||||||
|
language: string,
|
||||||
|
) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined) formData.append(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
sendEmailFromTemplate(
|
||||||
|
renderCompanyOfferEmail,
|
||||||
|
{
|
||||||
|
companyData: data,
|
||||||
|
language,
|
||||||
|
},
|
||||||
|
process.env.CONTACT_EMAIL!,
|
||||||
|
);
|
||||||
|
};
|
||||||
27
app/api/after-mfa/route.ts
Normal file
27
app/api/after-mfa/route.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { enhanceRouteHandler } from '@/packages/next/src/routes';
|
||||||
|
import { createAuthCallbackService } from '@/packages/supabase/src/auth-callback.service';
|
||||||
|
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||||
|
|
||||||
|
export const POST = enhanceRouteHandler(
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const supabaseClient = getSupabaseServerClient();
|
||||||
|
const {
|
||||||
|
data: { user },
|
||||||
|
} = await supabaseClient.auth.getUser();
|
||||||
|
const service = createAuthCallbackService(supabaseClient);
|
||||||
|
|
||||||
|
if (user && service.isKeycloakUser(user)) {
|
||||||
|
await service.setupMedusaUserForKeycloak(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(null, { status: 200 });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error on verifying:', { err });
|
||||||
|
return new Response(null, { status: 500 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
auth: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -7,10 +7,18 @@ import { sendEmailFromTemplate } from '~/lib/services/mailer.service';
|
|||||||
export default async function sendOpenJobsEmails() {
|
export default async function sendOpenJobsEmails() {
|
||||||
const analysisResponseIds = await getOpenJobAnalysisResponseIds();
|
const analysisResponseIds = await getOpenJobAnalysisResponseIds();
|
||||||
|
|
||||||
|
if (analysisResponseIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const doctorAccounts = await getDoctorAccounts();
|
const doctorAccounts = await getDoctorAccounts();
|
||||||
const doctorEmails: string[] = doctorAccounts
|
const doctorEmails = doctorAccounts
|
||||||
.map(({ email }) => email)
|
.map(({ email }) => email)
|
||||||
.filter((email): email is string => !!email);
|
.filter((email) => !!email);
|
||||||
|
|
||||||
|
if (doctorEmails !== null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
await sendEmailFromTemplate(
|
await sendEmailFromTemplate(
|
||||||
renderNewJobsAvailableEmail,
|
renderNewJobsAvailableEmail,
|
||||||
@@ -20,4 +28,6 @@ export default async function sendOpenJobsEmails() {
|
|||||||
},
|
},
|
||||||
doctorEmails,
|
doctorEmails,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return doctorAccounts.filter((email) => !!email).map(({ id }) => id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default async function syncAnalysisGroups() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
console.info('Getting latest public message id');
|
console.info('Getting latest public message id');
|
||||||
const lastCheckedDate = await getLastCheckedDate();
|
// const lastCheckedDate = await getLastCheckedDate(); never used?
|
||||||
|
|
||||||
const latestMessage = await getLatestPublicMessageListItem();
|
const latestMessage = await getLatestPublicMessageListItem();
|
||||||
if (!latestMessage) {
|
if (!latestMessage) {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api';
|
||||||
|
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||||
|
|
||||||
import { readPrivateMessageResponse } from '~/lib/services/medipost/medipostPrivateMessage.service';
|
import { readPrivateMessageResponse } from '~/lib/services/medipost/medipostPrivateMessage.service';
|
||||||
|
|
||||||
type ProcessedMessage = {
|
type ProcessedMessage = {
|
||||||
@@ -16,6 +19,8 @@ type GroupedResults = {
|
|||||||
|
|
||||||
export default async function syncAnalysisResults() {
|
export default async function syncAnalysisResults() {
|
||||||
console.info('Syncing analysis results');
|
console.info('Syncing analysis results');
|
||||||
|
const supabase = getSupabaseServerClient();
|
||||||
|
const api = createUserAnalysesApi(supabase);
|
||||||
|
|
||||||
const processedMessages: ProcessedMessage[] = [];
|
const processedMessages: ProcessedMessage[] = [];
|
||||||
const excludedMessageIds: string[] = [];
|
const excludedMessageIds: string[] = [];
|
||||||
@@ -25,6 +30,12 @@ export default async function syncAnalysisResults() {
|
|||||||
processedMessages.push(result as ProcessedMessage);
|
processedMessages.push(result as ProcessedMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await api.sendAnalysisResultsNotification({
|
||||||
|
hasFullAnalysisResponse: result.hasFullAnalysisResponse,
|
||||||
|
hasPartialAnalysisResponse: result.hasAnalysisResponse,
|
||||||
|
analysisOrderId: result.analysisOrderId,
|
||||||
|
});
|
||||||
|
|
||||||
if (!result.messageId) {
|
if (!result.messageId) {
|
||||||
console.info('No more messages to process');
|
console.info('No more messages to process');
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -81,21 +81,19 @@ export default async function syncConnectedOnline() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let clinics;
|
|
||||||
let services;
|
|
||||||
let serviceProviders;
|
|
||||||
let jobTitleTranslations;
|
|
||||||
// Filter out "Dentas Demo OÜ" in prod or only sync "Dentas Demo OÜ" in any other environment
|
// Filter out "Dentas Demo OÜ" in prod or only sync "Dentas Demo OÜ" in any other environment
|
||||||
const isDemoClinic = (clinicId: number) =>
|
const isDemoClinic = (clinicId: number) =>
|
||||||
isProd ? clinicId !== 2 : clinicId === 2;
|
isProd ? clinicId !== 2 : clinicId === 2;
|
||||||
clinics = responseData.Data.T_Lic.filter(({ ID }) => isDemoClinic(ID));
|
const clinics = responseData.Data.T_Lic.filter(({ ID }) =>
|
||||||
services = responseData.Data.T_Service.filter(({ ClinicID }) =>
|
isDemoClinic(ID),
|
||||||
|
);
|
||||||
|
const services = responseData.Data.T_Service.filter(({ ClinicID }) =>
|
||||||
isDemoClinic(ClinicID),
|
isDemoClinic(ClinicID),
|
||||||
);
|
);
|
||||||
serviceProviders = responseData.Data.T_Doctor.filter(({ ClinicID }) =>
|
const serviceProviders = responseData.Data.T_Doctor.filter(({ ClinicID }) =>
|
||||||
isDemoClinic(ClinicID),
|
isDemoClinic(ClinicID),
|
||||||
);
|
);
|
||||||
jobTitleTranslations = createTranslationMap(
|
const jobTitleTranslations = createTranslationMap(
|
||||||
responseData.Data.P_JobTitleTranslations.filter(({ ClinicID }) =>
|
responseData.Data.P_JobTitleTranslations.filter(({ ClinicID }) =>
|
||||||
isDemoClinic(ClinicID),
|
isDemoClinic(ClinicID),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { getLogger } from '@/packages/shared/src/logger';
|
||||||
import { retrieveOrder } from '@lib/data/orders';
|
import { retrieveOrder } from '@lib/data/orders';
|
||||||
|
|
||||||
import { getMedipostDispatchTries } from '~/lib/services/audit.service';
|
import { getMedipostDispatchTries } from '~/lib/services/audit.service';
|
||||||
@@ -10,13 +11,17 @@ import loadEnv from '../handler/load-env';
|
|||||||
import validateApiKey from '../handler/validate-api-key';
|
import validateApiKey from '../handler/validate-api-key';
|
||||||
|
|
||||||
export const POST = async (request: NextRequest) => {
|
export const POST = async (request: NextRequest) => {
|
||||||
|
const logger = await getLogger();
|
||||||
|
const ctx = {
|
||||||
|
api: '/job/medipost-retry-dispatch',
|
||||||
|
};
|
||||||
loadEnv();
|
loadEnv();
|
||||||
|
|
||||||
const { medusaOrderId } = await request.json();
|
const { medusaOrderId } = await request.json();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
validateApiKey(request);
|
validateApiKey(request);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,15 +41,15 @@ export const POST = async (request: NextRequest) => {
|
|||||||
medusaOrder,
|
medusaOrder,
|
||||||
});
|
});
|
||||||
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
||||||
console.info('Successfully sent order to medipost');
|
logger.info(ctx, 'Successfully sent order to medipost');
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
message: 'Successfully sent order to medipost',
|
message: 'Successfully sent order to medipost',
|
||||||
},
|
},
|
||||||
{ status: 200 },
|
{ status: 200 },
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.error('Error sending order to medipost', e);
|
logger.error({ ...ctx, error }, 'Error sending order to medipost');
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
message: 'Failed to send order to medipost',
|
message: 'Failed to send order to medipost',
|
||||||
|
|||||||
@@ -14,18 +14,19 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
validateApiKey(request);
|
validateApiKey(request);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendOpenJobsEmails();
|
const doctors = await sendOpenJobsEmails();
|
||||||
console.info(
|
console.info(
|
||||||
'Successfully sent out open job notification emails to doctors.',
|
'Successfully sent out open job notification emails to doctors',
|
||||||
);
|
);
|
||||||
await createNotificationLog({
|
await createNotificationLog({
|
||||||
action: NotificationAction.DOCTOR_NEW_JOBS,
|
action: NotificationAction.DOCTOR_NEW_JOBS,
|
||||||
status: 'SUCCESS',
|
status: 'SUCCESS',
|
||||||
|
comment: `doctors that received email: ${doctors}`,
|
||||||
});
|
});
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
validateApiKey(request);
|
validateApiKey(request);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
validateApiKey(request);
|
validateApiKey(request);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
validateApiKey(request);
|
validateApiKey(request);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const POST = async (request: NextRequest) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
validateApiKey(request);
|
validateApiKey(request);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
validateApiKey(request);
|
validateApiKey(request);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
const service = createAuthCallbackService(getSupabaseServerClient());
|
const service = createAuthCallbackService(getSupabaseServerClient());
|
||||||
const oauthResult = await service.exchangeCodeForSession(authCode);
|
const oauthResult = await service.exchangeCodeForSession(authCode);
|
||||||
|
|
||||||
|
if (oauthResult.requiresMultiFactorAuthentication) {
|
||||||
|
redirect(pathsConfig.auth.verifyMfa);
|
||||||
|
}
|
||||||
|
|
||||||
if (!('isSuccess' in oauthResult)) {
|
if (!('isSuccess' in oauthResult)) {
|
||||||
return redirectOnError(oauthResult.searchParams);
|
return redirectOnError(oauthResult.searchParams);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const MembershipConfirmationNotification: React.FC<{
|
|||||||
descriptionKey="account:membershipConfirmation:successDescription"
|
descriptionKey="account:membershipConfirmation:successDescription"
|
||||||
buttonProps={{
|
buttonProps={{
|
||||||
buttonTitleKey: 'account:membershipConfirmation:successButton',
|
buttonTitleKey: 'account:membershipConfirmation:successButton',
|
||||||
href: pathsConfig.app.home,
|
href: pathsConfig.app.selectPackage,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
|
||||||
|
|
||||||
async function SiteLayout(props: React.PropsWithChildren) {
|
async function SiteLayout(props: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
|
<div className={'flex min-h-[100vh] flex-col items-center justify-center'}>
|
||||||
|
|||||||
@@ -181,80 +181,74 @@ export function UpdateAccountForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!isEmailUser && (
|
<>
|
||||||
<>
|
<FormField
|
||||||
|
name="city"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans i18nKey={'common:formField:city'} />
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-between gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
name="city"
|
name="weight"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="flex-1 basis-0">
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'common:formField:city'} />
|
<Trans i18nKey={'common:formField:weight'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="kg"
|
||||||
|
{...field}
|
||||||
|
value={field.value ?? ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
e.target.value === '' ? null : Number(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row justify-between gap-4">
|
<FormField
|
||||||
<FormField
|
name="height"
|
||||||
name="weight"
|
render={({ field }) => (
|
||||||
render={({ field }) => (
|
<FormItem className="flex-1 basis-0">
|
||||||
<FormItem className="flex-1 basis-0">
|
<FormLabel>
|
||||||
<FormLabel>
|
<Trans i18nKey={'common:formField:height'} />
|
||||||
<Trans i18nKey={'common:formField:weight'} />
|
</FormLabel>
|
||||||
</FormLabel>
|
<FormControl>
|
||||||
<FormControl>
|
<Input
|
||||||
<Input
|
placeholder="cm"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="kg"
|
{...field}
|
||||||
{...field}
|
value={field.value ?? ''}
|
||||||
value={field.value ?? ''}
|
onChange={(e) =>
|
||||||
onChange={(e) =>
|
field.onChange(
|
||||||
field.onChange(
|
e.target.value === '' ? null : Number(e.target.value),
|
||||||
e.target.value === ''
|
)
|
||||||
? null
|
}
|
||||||
: Number(e.target.value),
|
/>
|
||||||
)
|
</FormControl>
|
||||||
}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
</FormControl>
|
)}
|
||||||
<FormMessage />
|
/>
|
||||||
</FormItem>
|
</div>
|
||||||
)}
|
</>
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
name="height"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex-1 basis-0">
|
|
||||||
<FormLabel>
|
|
||||||
<Trans i18nKey={'common:formField:height'} />
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="cm"
|
|
||||||
type="number"
|
|
||||||
{...field}
|
|
||||||
value={field.value ?? ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
field.onChange(
|
|
||||||
e.target.value === ''
|
|
||||||
? null
|
|
||||||
: Number(e.target.value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name="userConsent"
|
name="userConsent"
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { updateCustomer } from '@lib/data/customer';
|
|||||||
|
|
||||||
import { AccountSubmitData, createAuthApi } from '@kit/auth/api';
|
import { AccountSubmitData, createAuthApi } from '@kit/auth/api';
|
||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
import { UpdateAccountSchemaServer } from '../schemas/update-account.schema';
|
import { UpdateAccountSchemaServer } from '../schemas/update-account.schema';
|
||||||
|
|||||||
@@ -44,12 +44,7 @@ async function VerifyPage(props: Props) {
|
|||||||
!!nextPath && nextPath.length > 0 ? nextPath : pathsConfig.app.home;
|
!!nextPath && nextPath.length > 0 ? nextPath : pathsConfig.app.home;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultiFactorChallengeContainer
|
<MultiFactorChallengeContainer userId={user.id} paths={{ redirectPath }} />
|
||||||
userId={user.id}
|
|
||||||
paths={{
|
|
||||||
redirectPath,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
156
app/doctor/_components/analysis-feedback.tsx
Normal file
156
app/doctor/_components/analysis-feedback.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { giveFeedbackAction } from '@/packages/features/doctor/src/lib/server/actions/doctor-server-actions';
|
||||||
|
import {
|
||||||
|
DoctorFeedback,
|
||||||
|
Order,
|
||||||
|
Patient,
|
||||||
|
} from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis-detail-view.schema';
|
||||||
|
import {
|
||||||
|
DoctorAnalysisFeedbackForm,
|
||||||
|
doctorAnalysisFeedbackFormSchema,
|
||||||
|
} from '@/packages/features/doctor/src/lib/server/schema/doctor-analysis.schema';
|
||||||
|
import ConfirmationModal from '@/packages/shared/src/components/confirmation-modal';
|
||||||
|
import { useUser } from '@/packages/supabase/src/hooks/use-user';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Trans } from '@kit/ui/makerkit/trans';
|
||||||
|
import { Button } from '@kit/ui/shadcn/button';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormMessage,
|
||||||
|
} from '@kit/ui/shadcn/form';
|
||||||
|
import { toast } from '@kit/ui/shadcn/sonner';
|
||||||
|
import { Textarea } from '@kit/ui/shadcn/textarea';
|
||||||
|
|
||||||
|
const AnalysisFeedback = ({
|
||||||
|
feedback,
|
||||||
|
patient,
|
||||||
|
order,
|
||||||
|
}: {
|
||||||
|
feedback?: DoctorFeedback;
|
||||||
|
patient: Patient;
|
||||||
|
order: Order;
|
||||||
|
}) => {
|
||||||
|
const [isDraftSubmitting, setIsDraftSubmitting] = useState(false);
|
||||||
|
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||||
|
const { data: user } = useUser();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(doctorAnalysisFeedbackFormSchema),
|
||||||
|
reValidateMode: 'onChange',
|
||||||
|
defaultValues: {
|
||||||
|
feedbackValue: feedback?.value ?? '',
|
||||||
|
userId: patient.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isReadOnly =
|
||||||
|
!!feedback?.doctor_user_id && feedback?.doctor_user_id !== user?.id;
|
||||||
|
|
||||||
|
const handleDraftSubmit = async (e: React.FormEvent) => {
|
||||||
|
setIsDraftSubmitting(true);
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
form.formState.errors.feedbackValue = undefined;
|
||||||
|
const formData = form.getValues();
|
||||||
|
await onSubmit(formData, 'DRAFT');
|
||||||
|
setIsDraftSubmitting(false);
|
||||||
|
};
|
||||||
|
const handleCompleteSubmit = form.handleSubmit(async () => {
|
||||||
|
setIsConfirmOpen(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (
|
||||||
|
data: DoctorAnalysisFeedbackForm,
|
||||||
|
status: 'DRAFT' | 'COMPLETED',
|
||||||
|
) => {
|
||||||
|
const result = await giveFeedbackAction({
|
||||||
|
...data,
|
||||||
|
analysisOrderId: order.analysisOrderId,
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return toast.error(<Trans i18nKey="common:genericServerError" />);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
predicate: (query) => query.queryKey.includes('doctor-jobs'),
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success(<Trans i18nKey={'doctor:updateFeedbackSuccess'} />);
|
||||||
|
|
||||||
|
return setIsConfirmOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmComplete = form.handleSubmit(async (data) => {
|
||||||
|
await onSubmit(data, 'COMPLETED');
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<Trans i18nKey="doctor:feedback" />
|
||||||
|
</h3>
|
||||||
|
<p>{feedback?.value ?? '-'}</p>
|
||||||
|
{!isReadOnly && (
|
||||||
|
<Form {...form}>
|
||||||
|
<form className="space-y-4 lg:w-1/2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="feedbackValue"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea {...field} disabled={isReadOnly} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="xs:flex block justify-end gap-2 space-y-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleDraftSubmit}
|
||||||
|
disabled={
|
||||||
|
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
||||||
|
}
|
||||||
|
className="xs:w-auto w-full text-xs"
|
||||||
|
>
|
||||||
|
<Trans i18nKey="common:saveAsDraft" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCompleteSubmit}
|
||||||
|
disabled={
|
||||||
|
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
||||||
|
}
|
||||||
|
className="xs:w-1/4 w-full"
|
||||||
|
>
|
||||||
|
<Trans i18nKey="common:save" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={isConfirmOpen}
|
||||||
|
onClose={() => setIsConfirmOpen(false)}
|
||||||
|
onConfirm={confirmComplete}
|
||||||
|
titleKey="doctor:confirmFeedbackModal.title"
|
||||||
|
descriptionKey="doctor:confirmFeedbackModal.description"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AnalysisFeedback;
|
||||||
@@ -1,13 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { capitalize } from 'lodash';
|
import { capitalize } from 'lodash';
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
|
|
||||||
import { giveFeedbackAction } from '@kit/doctor/actions/doctor-server-actions';
|
|
||||||
import {
|
import {
|
||||||
getDOBWithAgeStringFromPersonalCode,
|
getDOBWithAgeStringFromPersonalCode,
|
||||||
getResultSetName,
|
getResultSetName,
|
||||||
@@ -18,28 +13,14 @@ import {
|
|||||||
Order,
|
Order,
|
||||||
Patient,
|
Patient,
|
||||||
} from '@kit/doctor/schema/doctor-analysis-detail-view.schema';
|
} from '@kit/doctor/schema/doctor-analysis-detail-view.schema';
|
||||||
import {
|
|
||||||
DoctorAnalysisFeedbackForm,
|
|
||||||
doctorAnalysisFeedbackFormSchema,
|
|
||||||
} from '@kit/doctor/schema/doctor-analysis.schema';
|
|
||||||
import ConfirmationModal from '@kit/shared/components/confirmation-modal';
|
|
||||||
import { useCurrentLocaleLanguageNames } from '@kit/shared/hooks';
|
import { useCurrentLocaleLanguageNames } from '@kit/shared/hooks';
|
||||||
import { getFullName } from '@kit/shared/utils';
|
import { getFullName } from '@kit/shared/utils';
|
||||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||||
import { Button } from '@kit/ui/button';
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormMessage,
|
|
||||||
} from '@kit/ui/form';
|
|
||||||
import { toast } from '@kit/ui/sonner';
|
|
||||||
import { Textarea } from '@kit/ui/textarea';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { bmiFromMetric } from '~/lib/utils';
|
import { bmiFromMetric } from '~/lib/utils';
|
||||||
|
|
||||||
|
import AnalysisFeedback from './analysis-feedback';
|
||||||
import DoctorAnalysisWrapper from './doctor-analysis-wrapper';
|
import DoctorAnalysisWrapper from './doctor-analysis-wrapper';
|
||||||
import DoctorJobSelect from './doctor-job-select';
|
import DoctorJobSelect from './doctor-job-select';
|
||||||
|
|
||||||
@@ -54,10 +35,8 @@ export default function AnalysisView({
|
|||||||
analyses: AnalysisResponse[];
|
analyses: AnalysisResponse[];
|
||||||
feedback?: DoctorFeedback;
|
feedback?: DoctorFeedback;
|
||||||
}) {
|
}) {
|
||||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
|
||||||
const [isDraftSubmitting, setIsDraftSubmitting] = useState(false);
|
|
||||||
|
|
||||||
const { data: user } = useUser();
|
const { data: user } = useUser();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const languageNames = useCurrentLocaleLanguageNames();
|
const languageNames = useCurrentLocaleLanguageNames();
|
||||||
|
|
||||||
@@ -68,66 +47,11 @@ export default function AnalysisView({
|
|||||||
);
|
);
|
||||||
const isCurrentDoctorJob =
|
const isCurrentDoctorJob =
|
||||||
!!feedback?.doctor_user_id && feedback?.doctor_user_id === user?.id;
|
!!feedback?.doctor_user_id && feedback?.doctor_user_id === user?.id;
|
||||||
const isReadOnly =
|
|
||||||
!isInProgress ||
|
|
||||||
(!!feedback?.doctor_user_id && feedback?.doctor_user_id !== user?.id);
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
resolver: zodResolver(doctorAnalysisFeedbackFormSchema),
|
|
||||||
reValidateMode: 'onChange',
|
|
||||||
defaultValues: {
|
|
||||||
feedbackValue: feedback?.value ?? '',
|
|
||||||
userId: patient.userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
if (!patient || !order || !analyses) {
|
if (!patient || !order || !analyses) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async (
|
|
||||||
data: DoctorAnalysisFeedbackForm,
|
|
||||||
status: 'DRAFT' | 'COMPLETED',
|
|
||||||
) => {
|
|
||||||
const result = await giveFeedbackAction({
|
|
||||||
...data,
|
|
||||||
analysisOrderId: order.analysisOrderId,
|
|
||||||
status,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
return toast.error(<Trans i18nKey="common:genericServerError" />);
|
|
||||||
}
|
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
predicate: (query) => query.queryKey.includes('doctor-jobs'),
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success(<Trans i18nKey={'doctor:updateFeedbackSuccess'} />);
|
|
||||||
|
|
||||||
return setIsConfirmOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDraftSubmit = async (e: React.FormEvent) => {
|
|
||||||
setIsDraftSubmitting(true);
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
form.formState.errors.feedbackValue = undefined;
|
|
||||||
const formData = form.getValues();
|
|
||||||
await onSubmit(formData, 'DRAFT');
|
|
||||||
setIsDraftSubmitting(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCompleteSubmit = form.handleSubmit(async () => {
|
|
||||||
setIsConfirmOpen(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
const confirmComplete = form.handleSubmit(async (data) => {
|
|
||||||
await onSubmit(data, 'COMPLETED');
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="xs:flex xs:justify-between">
|
<div className="xs:flex xs:justify-between">
|
||||||
@@ -229,59 +153,9 @@ export default function AnalysisView({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<h3>
|
{order.isPackage && (
|
||||||
<Trans i18nKey="doctor:feedback" />
|
<AnalysisFeedback order={order} patient={patient} feedback={feedback} />
|
||||||
</h3>
|
|
||||||
<p>{feedback?.value ?? '-'}</p>
|
|
||||||
{!isReadOnly && (
|
|
||||||
<Form {...form}>
|
|
||||||
<form className="space-y-4 lg:w-1/2">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="feedbackValue"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea {...field} disabled={isReadOnly} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="xs:flex block justify-end gap-2 space-y-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleDraftSubmit}
|
|
||||||
disabled={
|
|
||||||
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
|
||||||
}
|
|
||||||
className="xs:w-1/4 w-full"
|
|
||||||
>
|
|
||||||
<Trans i18nKey="common:saveAsDraft" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={handleCompleteSubmit}
|
|
||||||
disabled={
|
|
||||||
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
|
||||||
}
|
|
||||||
className="xs:w-1/4 w-full"
|
|
||||||
>
|
|
||||||
<Trans i18nKey="common:save" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
)}
|
)}
|
||||||
<ConfirmationModal
|
|
||||||
isOpen={isConfirmOpen}
|
|
||||||
onClose={() => setIsConfirmOpen(false)}
|
|
||||||
onConfirm={confirmComplete}
|
|
||||||
titleKey="doctor:confirmFeedbackModal.title"
|
|
||||||
descriptionKey="doctor:confirmFeedbackModal.description"
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
|
|
||||||
import ResultsTableWrapper from './results-table-wrapper';
|
import ResultsTableWrapper from './results-table-wrapper';
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function DoctorDashboard() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ResultsTableWrapper
|
<ResultsTableWrapper
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function DoctorSidebar({
|
|||||||
<Sidebar collapsible="icon">
|
<Sidebar collapsible="icon">
|
||||||
<SidebarHeader className={'m-2'}>
|
<SidebarHeader className={'m-2'}>
|
||||||
<AppLogo
|
<AppLogo
|
||||||
href={pathsConfig.app.doctor}
|
href={pathsConfig.app.home}
|
||||||
className="max-w-full"
|
className="max-w-full"
|
||||||
compact={!open}
|
compact={!open}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ async function AnalysisPage({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
<PageBody>
|
<PageBody className="px-12">
|
||||||
<AnalysisView
|
<AnalysisView
|
||||||
patient={analysisResultDetails.patient}
|
patient={analysisResultDetails.patient}
|
||||||
order={analysisResultDetails.order}
|
order={analysisResultDetails.order}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ async function CompletedJobsPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
<PageBody>
|
<PageBody className="px-12">
|
||||||
<ResultsTableWrapper
|
<ResultsTableWrapper
|
||||||
titleKey="doctor:completedReviews"
|
titleKey="doctor:completedReviews"
|
||||||
action={getUserDoneResponsesAction}
|
action={getUserDoneResponsesAction}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ async function MyReviewsPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
<PageBody>
|
<PageBody className="px-12">
|
||||||
<ResultsTableWrapper
|
<ResultsTableWrapper
|
||||||
titleKey="doctor:myReviews"
|
titleKey="doctor:myReviews"
|
||||||
action={getUserInProgressResponsesAction}
|
action={getUserInProgressResponsesAction}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ async function OpenJobsPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
<PageBody>
|
<PageBody className="px-12">
|
||||||
<ResultsTableWrapper
|
<ResultsTableWrapper
|
||||||
titleKey="doctor:openReviews"
|
titleKey="doctor:openReviews"
|
||||||
action={getOpenResponsesAction}
|
action={getOpenResponsesAction}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
createDoctorPageViewLog,
|
createDoctorPageViewLog,
|
||||||
} from '~/lib/services/audit/doctorPageView.service';
|
} from '~/lib/services/audit/doctorPageView.service';
|
||||||
|
|
||||||
import Dashboard from './_components/doctor-dashboard';
|
import DoctorDashboard from './_components/doctor-dashboard';
|
||||||
import { DoctorGuard } from './_components/doctor-guard';
|
import { DoctorGuard } from './_components/doctor-guard';
|
||||||
|
|
||||||
async function DoctorPage() {
|
async function DoctorPage() {
|
||||||
@@ -16,8 +16,8 @@ async function DoctorPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
<PageBody>
|
<PageBody className="px-12">
|
||||||
<Dashboard />
|
<DoctorDashboard />
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import React from 'react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
import { createNotificationsApi } from '@/packages/features/notifications/src/server/api';
|
||||||
|
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||||
|
|
||||||
import { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip';
|
import { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip';
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
@@ -25,7 +28,9 @@ export default async function AnalysisResultsPage({
|
|||||||
id: string;
|
id: string;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
|
const supabaseClient = getSupabaseServerClient();
|
||||||
const { id: analysisOrderId } = await params;
|
const { id: analysisOrderId } = await params;
|
||||||
|
const notificationsApi = createNotificationsApi(supabaseClient);
|
||||||
|
|
||||||
const [{ account }, analysisResponse] = await Promise.all([
|
const [{ account }, analysisResponse] = await Promise.all([
|
||||||
loadCurrentUserAccount(),
|
loadCurrentUserAccount(),
|
||||||
@@ -41,6 +46,11 @@ export default async function AnalysisResultsPage({
|
|||||||
action: PageViewAction.VIEW_ANALYSIS_RESULTS,
|
action: PageViewAction.VIEW_ANALYSIS_RESULTS,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await notificationsApi.dismissNotification(
|
||||||
|
`/home/analysis-results/${analysisOrderId}`,
|
||||||
|
'link',
|
||||||
|
);
|
||||||
|
|
||||||
if (!analysisResponse) {
|
if (!analysisResponse) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -108,7 +118,7 @@ export default async function AnalysisResultsPage({
|
|||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{orderedAnalysisElements ? (
|
{orderedAnalysisElements ? (
|
||||||
orderedAnalysisElements.map((element, index) => (
|
orderedAnalysisElements.map((element) => (
|
||||||
<React.Fragment key={element.analysisIdOriginal}>
|
<React.Fragment key={element.analysisIdOriginal}>
|
||||||
<Analysis element={element} />
|
<Analysis element={element} />
|
||||||
{element.results?.nestedElements?.map(
|
{element.results?.nestedElements?.map(
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { use } from 'react';
|
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
|
||||||
|
|||||||
@@ -28,17 +28,18 @@ async function OrderAnalysisPackagePage() {
|
|||||||
<PageBody>
|
<PageBody>
|
||||||
<div className="space-y-3 text-center">
|
<div className="space-y-3 text-center">
|
||||||
<h3>
|
<h3>
|
||||||
<Trans i18nKey={'marketing:selectPackage'} />
|
<Trans i18nKey="order-analysis-package:selectPackage" />
|
||||||
</h3>
|
</h3>
|
||||||
<ComparePackagesModal
|
<ComparePackagesModal
|
||||||
analysisPackages={analysisPackages}
|
analysisPackages={analysisPackages}
|
||||||
analysisPackageElements={analysisPackageElements}
|
analysisPackageElements={analysisPackageElements}
|
||||||
triggerElement={
|
triggerElement={
|
||||||
<Button variant="secondary" className="gap-2">
|
<Button variant="secondary" className="gap-2">
|
||||||
<Trans i18nKey={'marketing:comparePackages'} />
|
<Trans i18nKey="order-analysis-package:comparePackages" />
|
||||||
<Scale className="size-4 stroke-[1.5px]" />
|
<Scale className="size-4 stroke-[1.5px]" />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
countryCode={countryCode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SelectAnalysisPackages
|
<SelectAnalysisPackages
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { GlobalLoader } from '@kit/ui/makerkit/global-loader';
|
|||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { AnalysisOrder } from '~/lib/types/analysis-order';
|
import { AnalysisOrder } from '~/lib/types/order';
|
||||||
|
|
||||||
function OrderConfirmedLoadingWrapper({
|
function OrderConfirmedLoadingWrapper({
|
||||||
medusaOrder: initialMedusaOrder,
|
medusaOrder: initialMedusaOrder,
|
||||||
@@ -71,7 +71,14 @@ function OrderConfirmedLoadingWrapper({
|
|||||||
<PageHeader title={<Trans i18nKey="cart:orderConfirmed.title" />} />
|
<PageHeader title={<Trans i18nKey="cart:orderConfirmed.title" />} />
|
||||||
<Divider />
|
<Divider />
|
||||||
<div className="small:grid-cols-[1fr_360px] grid grid-cols-1 gap-x-40 gap-y-6 lg:px-4">
|
<div className="small:grid-cols-[1fr_360px] grid grid-cols-1 gap-x-40 gap-y-6 lg:px-4">
|
||||||
<OrderDetails order={order} />
|
<OrderDetails
|
||||||
|
order={{
|
||||||
|
id: order.medusa_order_id,
|
||||||
|
created_at: order.created_at,
|
||||||
|
location: null,
|
||||||
|
serviceProvider: null,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
<OrderItems medusaOrder={medusaOrder} />
|
<OrderItems medusaOrder={medusaOrder} />
|
||||||
<CartTotals medusaOrder={medusaOrder} />
|
<CartTotals medusaOrder={medusaOrder} />
|
||||||
|
|||||||
@@ -90,6 +90,14 @@ async function OrdersPage() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
medusaOrderItemsAnalysisPackages.length === 0 &&
|
||||||
|
medusaOrderItemsOther.length === 0 &&
|
||||||
|
medusaOrderItemsTtoServices.length === 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={medusaOrder.id}>
|
<React.Fragment key={medusaOrder.id}>
|
||||||
<Divider className="my-6" />
|
<Divider className="my-6" />
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const BookingProvider: React.FC<{
|
|||||||
);
|
);
|
||||||
setTimeSlots(response.timeSlots);
|
setTimeSlots(response.timeSlots);
|
||||||
setLocations(response.locations);
|
setLocations(response.locations);
|
||||||
} catch (error) {
|
} catch {
|
||||||
setTimeSlots(null);
|
setTimeSlots(null);
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingTimeSlots(false);
|
setIsLoadingTimeSlots(false);
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ const TimeSlots = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="xs:flex xs:justify-between grid w-full justify-center-safe gap-3 p-4"
|
className="xs:flex xs:justify-between w-full justify-center-safe gap-3 p-4"
|
||||||
key={index}
|
key={index}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { formatCurrency } from '@/packages/shared/src/utils';
|
import { formatCurrency } from '@/packages/shared/src/utils';
|
||||||
import { StoreCart, StoreCartLineItem } from '@medusajs/types';
|
import {
|
||||||
|
StoreCart,
|
||||||
|
StoreCartLineItem,
|
||||||
|
StoreCartPromotion,
|
||||||
|
} from '@medusajs/types';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -30,7 +34,9 @@ export default function CartFormContent({
|
|||||||
isInitiatingSession,
|
isInitiatingSession,
|
||||||
getBalanceSummarySelection,
|
getBalanceSummarySelection,
|
||||||
}: {
|
}: {
|
||||||
cart: StoreCart;
|
cart: StoreCart & {
|
||||||
|
promotions: StoreCartPromotion[];
|
||||||
|
};
|
||||||
synlabAnalyses: StoreCartLineItem[];
|
synlabAnalyses: StoreCartLineItem[];
|
||||||
ttoServiceItems: EnrichedCartItem[];
|
ttoServiceItems: EnrichedCartItem[];
|
||||||
unavailableLineItemIds?: string[];
|
unavailableLineItemIds?: string[];
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export default function CartItem({
|
|||||||
} = useTranslation();
|
} = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow className="w-full" data-testid="product-row">
|
<TableRow className="sm:w-full" data-testid="product-row">
|
||||||
<TableCell className="w-[100%] px-4 text-left sm:px-6">
|
<TableCell className="w-[100%] px-4 text-left sm:px-6">
|
||||||
<p
|
<p
|
||||||
className="txt-medium-plus text-ui-fg-base"
|
className="txt-medium-plus text-ui-fg-base"
|
||||||
@@ -41,11 +41,12 @@ export default function CartItem({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="min-w-[80px] px-4 text-right sm:px-6">
|
<TableCell className="min-w-[80px] px-4 text-right sm:px-6">
|
||||||
{formatCurrency({
|
{item.total &&
|
||||||
value: item.total,
|
formatCurrency({
|
||||||
currencyCode,
|
value: item.total,
|
||||||
locale: language,
|
currencyCode,
|
||||||
})}
|
locale: language,
|
||||||
|
})}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="px-4 text-right sm:px-6">
|
<TableCell className="px-4 text-right sm:px-6">
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import CartItem from './cart-item';
|
import CartItem from './cart-item';
|
||||||
|
import MobileCartItems from './mobile-cart-items';
|
||||||
|
|
||||||
export default function CartItems({
|
export default function CartItems({
|
||||||
cart,
|
cart,
|
||||||
@@ -25,37 +26,54 @@ export default function CartItems({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className="border-separate rounded-lg border">
|
<>
|
||||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
<Table className="hidden border-separate rounded-lg border sm:block">
|
||||||
<TableRow>
|
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||||
<TableHead className="px-4 sm:px-6">
|
<TableRow>
|
||||||
<Trans i18nKey={productColumnLabelKey} />
|
<TableHead className="px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey={productColumnLabelKey} />
|
||||||
<TableHead className="px-4 sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.quantity" />
|
<TableHead className="px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.quantity" />
|
||||||
<TableHead className="min-w-[100px] px-4 sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.price" />
|
<TableHead className="min-w-[100px] px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.price" />
|
||||||
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.total" />
|
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.total" />
|
||||||
<TableHead className="px-4 sm:px-6"></TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
<TableHead className="px-4 sm:px-6"></TableHead>
|
||||||
</TableHeader>
|
</TableRow>
|
||||||
<TableBody>
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{items
|
||||||
|
.sort((a, b) =>
|
||||||
|
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||||
|
)
|
||||||
|
.map((item) => (
|
||||||
|
<CartItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
currencyCode={cart.currency_code}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<div className="sm:hidden">
|
||||||
{items
|
{items
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||||
)
|
)
|
||||||
.map((item) => (
|
.map((item) => (
|
||||||
<CartItem
|
<MobileCartItems
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
currencyCode={cart.currency_code}
|
currencyCode={cart.currency_code}
|
||||||
|
productColumnLabelKey={productColumnLabelKey}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,76 +1,26 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { formatCurrency, formatDateAndTime } from '@/packages/shared/src/utils';
|
import { formatCurrency, formatDateAndTime } from '@/packages/shared/src/utils';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@kit/ui/dialog';
|
|
||||||
import { TableCell, TableRow } from '@kit/ui/table';
|
import { TableCell, TableRow } from '@kit/ui/table';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import BookingContainer from '../booking/booking-container';
|
|
||||||
import CartItemDelete from './cart-item-delete';
|
import CartItemDelete from './cart-item-delete';
|
||||||
import { EnrichedCartItem } from './types';
|
import { EnrichedCartItem } from './types';
|
||||||
|
|
||||||
const EditCartServiceItemModal = ({
|
|
||||||
item,
|
|
||||||
onComplete,
|
|
||||||
}: {
|
|
||||||
item: EnrichedCartItem | null;
|
|
||||||
onComplete: () => void;
|
|
||||||
}) => {
|
|
||||||
if (!item) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog defaultOpen>
|
|
||||||
<DialogContent className="xs:max-w-[90vw] flex max-h-screen max-w-full flex-col items-center gap-4 space-y-4 overflow-y-scroll">
|
|
||||||
<DialogHeader className="items-center text-center">
|
|
||||||
<DialogTitle>
|
|
||||||
<Trans i18nKey="cart:editServiceItem.title" />
|
|
||||||
</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
<Trans i18nKey="cart:editServiceItem.description" />
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div>
|
|
||||||
{item.product && item.reservation.countryCode ? (
|
|
||||||
<BookingContainer
|
|
||||||
category={{
|
|
||||||
products: [item.product],
|
|
||||||
countryCode: item.reservation.countryCode,
|
|
||||||
}}
|
|
||||||
cartItem={item}
|
|
||||||
onComplete={onComplete}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<p>
|
|
||||||
<Trans i18nKey="booking:noProducts" />
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CartServiceItem({
|
export default function CartServiceItem({
|
||||||
item,
|
item,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
isUnavailable,
|
isUnavailable,
|
||||||
|
setEditingItem,
|
||||||
}: {
|
}: {
|
||||||
item: EnrichedCartItem;
|
item: EnrichedCartItem;
|
||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
isUnavailable?: boolean;
|
isUnavailable?: boolean;
|
||||||
|
setEditingItem: (item: EnrichedCartItem | null) => void;
|
||||||
}) {
|
}) {
|
||||||
const [editingItem, setEditingItem] = useState<EnrichedCartItem | null>(null);
|
|
||||||
const {
|
const {
|
||||||
i18n: { language },
|
i18n: { language },
|
||||||
} = useTranslation();
|
} = useTranslation();
|
||||||
@@ -106,11 +56,12 @@ export default function CartServiceItem({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="min-w-[80px] px-4 text-right sm:px-6">
|
<TableCell className="min-w-[80px] px-4 text-right sm:px-6">
|
||||||
{formatCurrency({
|
{item.total &&
|
||||||
value: item.total,
|
formatCurrency({
|
||||||
currencyCode,
|
value: item.total,
|
||||||
locale: language,
|
currencyCode,
|
||||||
})}
|
locale: language,
|
||||||
|
})}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="px-4 text-right sm:px-6">
|
<TableCell className="px-4 text-right sm:px-6">
|
||||||
@@ -137,10 +88,6 @@ export default function CartServiceItem({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
<EditCartServiceItemModal
|
|
||||||
item={editingItem}
|
|
||||||
onComplete={() => setEditingItem(null)}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { StoreCart } from '@medusajs/types';
|
import { StoreCart } from '@medusajs/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@kit/ui/shadcn/dialog';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -9,9 +18,52 @@ import {
|
|||||||
} from '@kit/ui/table';
|
} from '@kit/ui/table';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import BookingContainer from '../booking/booking-container';
|
||||||
import CartServiceItem from './cart-service-item';
|
import CartServiceItem from './cart-service-item';
|
||||||
|
import MobileCartServiceItems from './mobile-cart-service-items';
|
||||||
import { EnrichedCartItem } from './types';
|
import { EnrichedCartItem } from './types';
|
||||||
|
|
||||||
|
const EditCartServiceItemModal = ({
|
||||||
|
item,
|
||||||
|
onComplete,
|
||||||
|
}: {
|
||||||
|
item: EnrichedCartItem | null;
|
||||||
|
onComplete: () => void;
|
||||||
|
}) => {
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog defaultOpen>
|
||||||
|
<DialogContent className="xs:max-w-[90vw] flex max-h-screen max-w-full flex-col items-center gap-4 space-y-4 overflow-y-scroll">
|
||||||
|
<DialogHeader className="items-center text-center">
|
||||||
|
<DialogTitle>
|
||||||
|
<Trans i18nKey="cart:editServiceItem.title" />
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
<Trans i18nKey="cart:editServiceItem.description" />
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div>
|
||||||
|
{item.product && item.reservation.countryCode ? (
|
||||||
|
<BookingContainer
|
||||||
|
category={{
|
||||||
|
products: [item.product],
|
||||||
|
countryCode: item.reservation.countryCode,
|
||||||
|
}}
|
||||||
|
cartItem={item}
|
||||||
|
onComplete={onComplete}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p>
|
||||||
|
<Trans i18nKey="booking:noProducts" />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function CartServiceItems({
|
export default function CartServiceItems({
|
||||||
cart,
|
cart,
|
||||||
items,
|
items,
|
||||||
@@ -23,50 +75,75 @@ export default function CartServiceItems({
|
|||||||
productColumnLabelKey: string;
|
productColumnLabelKey: string;
|
||||||
unavailableLineItemIds?: string[];
|
unavailableLineItemIds?: string[];
|
||||||
}) {
|
}) {
|
||||||
|
const [editingItem, setEditingItem] = useState<EnrichedCartItem | null>(null);
|
||||||
if (!items || items.length === 0) {
|
if (!items || items.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className="border-separate rounded-lg border">
|
<>
|
||||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
<Table className="hidden border-separate rounded-lg border sm:block">
|
||||||
<TableRow>
|
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||||
<TableHead className="px-4 sm:px-6">
|
<TableRow>
|
||||||
<Trans i18nKey={productColumnLabelKey} />
|
<TableHead className="px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey={productColumnLabelKey} />
|
||||||
<TableHead className="px-4 sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.time" />
|
<TableHead className="px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.time" />
|
||||||
<TableHead className="px-4 sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.location" />
|
<TableHead className="px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.location" />
|
||||||
<TableHead className="px-4 sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.quantity" />
|
<TableHead className="px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.quantity" />
|
||||||
<TableHead className="min-w-[100px] px-4 sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.price" />
|
<TableHead className="min-w-[100px] px-4 sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.price" />
|
||||||
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
|
</TableHead>
|
||||||
<Trans i18nKey="cart:table.total" />
|
<TableHead className="min-w-[100px] px-4 text-right sm:px-6">
|
||||||
</TableHead>
|
<Trans i18nKey="cart:table.total" />
|
||||||
<TableHead className="px-4 sm:px-6"></TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-4 sm:px-6"></TableHead>
|
<TableHead className="px-4 sm:px-6"></TableHead>
|
||||||
</TableRow>
|
<TableHead className="px-4 sm:px-6"></TableHead>
|
||||||
</TableHeader>
|
</TableRow>
|
||||||
<TableBody>
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{items
|
||||||
|
.sort((a, b) =>
|
||||||
|
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||||
|
)
|
||||||
|
.map((item) => (
|
||||||
|
<CartServiceItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
currencyCode={cart.currency_code}
|
||||||
|
isUnavailable={unavailableLineItemIds?.includes(item.id)}
|
||||||
|
setEditingItem={setEditingItem}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<div className="sm:hidden">
|
||||||
{items
|
{items
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||||
)
|
)
|
||||||
.map((item) => (
|
.map((item) => (
|
||||||
<CartServiceItem
|
<MobileCartServiceItems
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
currencyCode={cart.currency_code}
|
currencyCode={cart.currency_code}
|
||||||
isUnavailable={unavailableLineItemIds?.includes(item.id)}
|
isUnavailable={unavailableLineItemIds?.includes(item.id)}
|
||||||
|
productColumnLabelKey={productColumnLabelKey}
|
||||||
|
setEditingItem={setEditingItem}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</div>
|
||||||
</Table>
|
|
||||||
|
<EditCartServiceItemModal
|
||||||
|
item={editingItem}
|
||||||
|
onComplete={() => setEditingItem(null)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { convertToLocale } from '@lib/util/money';
|
import { convertToLocale } from '@lib/util/money';
|
||||||
import { StoreCart, StorePromotion } from '@medusajs/types';
|
import { StoreCart, StoreCartPromotion } from '@medusajs/types';
|
||||||
import { Badge, Text } from '@medusajs/ui';
|
import { Badge, Text } from '@medusajs/ui';
|
||||||
import Trash from '@modules/common/icons/trash';
|
import Trash from '@modules/common/icons/trash';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { useFormContext } from 'react-hook-form';
|
||||||
@@ -24,7 +24,7 @@ export default function DiscountCode({
|
|||||||
cart,
|
cart,
|
||||||
}: {
|
}: {
|
||||||
cart: StoreCart & {
|
cart: StoreCart & {
|
||||||
promotions: StorePromotion[];
|
promotions: StoreCartPromotion[];
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('cart');
|
const { t } = useTranslation('cart');
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { useCallback, useState } from 'react';
|
|||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { StoreCart, StoreCartLineItem } from '@medusajs/types';
|
import {
|
||||||
|
StoreCart,
|
||||||
|
StoreCartLineItem,
|
||||||
|
StoreCartPromotion,
|
||||||
|
} from '@medusajs/types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { AccountBalanceSummary } from '@kit/accounts/services/account-balance.service';
|
import { AccountBalanceSummary } from '@kit/accounts/services/account-balance.service';
|
||||||
@@ -23,7 +27,11 @@ export default function Cart({
|
|||||||
balanceSummary,
|
balanceSummary,
|
||||||
}: {
|
}: {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
cart: StoreCart | null;
|
cart:
|
||||||
|
| (StoreCart & {
|
||||||
|
promotions: StoreCartPromotion[];
|
||||||
|
})
|
||||||
|
| null;
|
||||||
synlabAnalyses: StoreCartLineItem[];
|
synlabAnalyses: StoreCartLineItem[];
|
||||||
ttoServiceItems: EnrichedCartItem[];
|
ttoServiceItems: EnrichedCartItem[];
|
||||||
balanceSummary: AccountBalanceSummary | null;
|
balanceSummary: AccountBalanceSummary | null;
|
||||||
|
|||||||
56
app/home/(user)/_components/cart/mobile-cart-items.tsx
Normal file
56
app/home/(user)/_components/cart/mobile-cart-items.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { formatCurrency } from '@/packages/shared/src/utils';
|
||||||
|
import { StoreCartLineItem } from '@medusajs/types';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Table, TableBody } from '@kit/ui/shadcn/table';
|
||||||
|
|
||||||
|
import MobileTableRow from './mobile-table-row';
|
||||||
|
|
||||||
|
const MobileCartItems = ({
|
||||||
|
item,
|
||||||
|
currencyCode,
|
||||||
|
productColumnLabelKey,
|
||||||
|
}: {
|
||||||
|
item: StoreCartLineItem;
|
||||||
|
currencyCode: string;
|
||||||
|
productColumnLabelKey: string;
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
i18n: { language },
|
||||||
|
} = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table className="border-separate rounded-lg border p-2">
|
||||||
|
<TableBody>
|
||||||
|
<MobileTableRow
|
||||||
|
titleKey={productColumnLabelKey}
|
||||||
|
value={item.product_title}
|
||||||
|
/>
|
||||||
|
<MobileTableRow titleKey="cart:table.time" value={item.quantity} />
|
||||||
|
<MobileTableRow
|
||||||
|
titleKey="cart:table.price"
|
||||||
|
value={formatCurrency({
|
||||||
|
value: item.unit_price,
|
||||||
|
currencyCode,
|
||||||
|
locale: language,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<MobileTableRow
|
||||||
|
titleKey="cart:table.total"
|
||||||
|
value={
|
||||||
|
item.total &&
|
||||||
|
formatCurrency({
|
||||||
|
value: item.total,
|
||||||
|
currencyCode,
|
||||||
|
locale: language,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileCartItems;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { formatCurrency, formatDateAndTime } from '@/packages/shared/src/utils';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Trans } from '@kit/ui/makerkit/trans';
|
||||||
|
import { Button } from '@kit/ui/shadcn/button';
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from '@kit/ui/shadcn/table';
|
||||||
|
|
||||||
|
import CartItemDelete from './cart-item-delete';
|
||||||
|
import MobileTableRow from './mobile-table-row';
|
||||||
|
import { EnrichedCartItem } from './types';
|
||||||
|
|
||||||
|
const MobileCartServiceItems = ({
|
||||||
|
item,
|
||||||
|
currencyCode,
|
||||||
|
isUnavailable,
|
||||||
|
productColumnLabelKey,
|
||||||
|
setEditingItem,
|
||||||
|
}: {
|
||||||
|
item: EnrichedCartItem;
|
||||||
|
currencyCode: string;
|
||||||
|
isUnavailable?: boolean;
|
||||||
|
productColumnLabelKey: string;
|
||||||
|
setEditingItem: (item: EnrichedCartItem | null) => void;
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
i18n: { language },
|
||||||
|
} = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table className="border-separate rounded-lg border p-2">
|
||||||
|
<TableBody>
|
||||||
|
<MobileTableRow
|
||||||
|
titleKey={productColumnLabelKey}
|
||||||
|
value={item.product_title}
|
||||||
|
/>
|
||||||
|
<MobileTableRow
|
||||||
|
titleKey="cart:table.time"
|
||||||
|
value={formatDateAndTime(item.reservation.startTime.toString())}
|
||||||
|
/>
|
||||||
|
<MobileTableRow
|
||||||
|
titleKey="cart:table.location"
|
||||||
|
value={item.reservation.location?.address ?? '-'}
|
||||||
|
/>
|
||||||
|
<MobileTableRow titleKey="cart:table.quantity" value={item.quantity} />
|
||||||
|
<MobileTableRow
|
||||||
|
titleKey="cart:table.price"
|
||||||
|
value={formatCurrency({
|
||||||
|
value: item.unit_price,
|
||||||
|
currencyCode,
|
||||||
|
locale: language,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<MobileTableRow
|
||||||
|
titleKey="cart:table.total"
|
||||||
|
value={
|
||||||
|
item.total &&
|
||||||
|
formatCurrency({
|
||||||
|
value: item.total,
|
||||||
|
currencyCode,
|
||||||
|
locale: language,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell />
|
||||||
|
<TableCell className="flex w-full items-center justify-end gap-4 p-0 pt-2">
|
||||||
|
<CartItemDelete id={item.id} />
|
||||||
|
<Button onClick={() => setEditingItem(item)}>
|
||||||
|
<Trans i18nKey="common:change" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
|
||||||
|
{isUnavailable && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={8}
|
||||||
|
className="text-destructive px-4 text-left sm:px-6"
|
||||||
|
>
|
||||||
|
<Trans i18nKey="booking:timeSlotUnavailable" />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileCartServiceItems;
|
||||||
24
app/home/(user)/_components/cart/mobile-table-row.tsx
Normal file
24
app/home/(user)/_components/cart/mobile-table-row.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Trans } from '@kit/ui/makerkit/trans';
|
||||||
|
import { TableCell, TableHead, TableRow } from '@kit/ui/shadcn/table';
|
||||||
|
|
||||||
|
const MobleTableRow = ({
|
||||||
|
titleKey,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
titleKey?: string;
|
||||||
|
value?: string | number;
|
||||||
|
}) => (
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="h-2 font-bold">
|
||||||
|
<Trans i18nKey={titleKey} />
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableCell className="p-0 text-right">
|
||||||
|
<p className="txt-medium-plus text-ui-fg-base">{value}</p>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default MobleTableRow;
|
||||||
11
app/home/(user)/_components/check-with-background.tsx
Normal file
11
app/home/(user)/_components/check-with-background.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Check } from 'lucide-react';
|
||||||
|
|
||||||
|
export const CheckWithBackground = () => {
|
||||||
|
return (
|
||||||
|
<div className="bg-primary w-min rounded-full p-1 text-white">
|
||||||
|
<Check className="size-3 stroke-2" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import { AnalysisPackageWithVariant } from '@/packages/shared/src/components/select-analysis-package';
|
||||||
|
import { pathsConfig } from '@/packages/shared/src/config';
|
||||||
|
|
||||||
|
import { Spinner } from '@kit/ui/makerkit/spinner';
|
||||||
|
import { Trans } from '@kit/ui/makerkit/trans';
|
||||||
|
import { Button } from '@kit/ui/shadcn/button';
|
||||||
|
import { toast } from '@kit/ui/shadcn/sonner';
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from '@kit/ui/shadcn/table';
|
||||||
|
|
||||||
|
import { handleAddToCart } from '~/lib/services/medusaCart.service';
|
||||||
|
import { cn } from '~/lib/utils';
|
||||||
|
|
||||||
|
const AddToCartButton = ({
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
isLoading,
|
||||||
|
}: {
|
||||||
|
onClick: () => void;
|
||||||
|
disabled: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<TableCell align="center" className="xs:px-2 px-1 py-6">
|
||||||
|
<Button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
className="xs:p-6 xs:text-sm relative p-2 text-[10px]"
|
||||||
|
>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn({
|
||||||
|
invisible: isLoading,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="compare-packages-modal:selectThisPackage" />
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ComparePackagesAddToCartButtons = ({
|
||||||
|
countryCode,
|
||||||
|
standardPackage,
|
||||||
|
standardPlusPackage,
|
||||||
|
premiumPackage,
|
||||||
|
}: {
|
||||||
|
countryCode: string;
|
||||||
|
standardPackage: AnalysisPackageWithVariant;
|
||||||
|
standardPlusPackage: AnalysisPackageWithVariant;
|
||||||
|
premiumPackage: AnalysisPackageWithVariant;
|
||||||
|
}) => {
|
||||||
|
const [addedPackage, setAddedPackage] = useState<string | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleSelect = async ({ variantId }: AnalysisPackageWithVariant) => {
|
||||||
|
setAddedPackage(variantId);
|
||||||
|
try {
|
||||||
|
await handleAddToCart({
|
||||||
|
selectedVariant: { id: variantId },
|
||||||
|
countryCode,
|
||||||
|
});
|
||||||
|
setAddedPackage(null);
|
||||||
|
toast.success(
|
||||||
|
<Trans i18nKey={'order-analysis-package:analysisPackageAddedToCart'} />,
|
||||||
|
);
|
||||||
|
router.push(pathsConfig.app.cart);
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(
|
||||||
|
<Trans
|
||||||
|
i18nKey={'order-analysis-package:analysisPackageAddToCartError'}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
setAddedPackage(null);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className="w-[30vw] py-6" />
|
||||||
|
<AddToCartButton
|
||||||
|
onClick={() => handleSelect(standardPackage)}
|
||||||
|
disabled={!!addedPackage}
|
||||||
|
isLoading={addedPackage === standardPackage.variantId}
|
||||||
|
/>
|
||||||
|
<AddToCartButton
|
||||||
|
onClick={() => handleSelect(standardPlusPackage)}
|
||||||
|
disabled={!!addedPackage}
|
||||||
|
isLoading={addedPackage === standardPlusPackage.variantId}
|
||||||
|
/>
|
||||||
|
<AddToCartButton
|
||||||
|
onClick={() => handleSelect(premiumPackage)}
|
||||||
|
disabled={!!addedPackage}
|
||||||
|
isLoading={addedPackage === premiumPackage.variantId}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ComparePackagesAddToCartButtons;
|
||||||
@@ -3,7 +3,7 @@ import { JSX } from 'react';
|
|||||||
import { StoreProduct } from '@medusajs/types';
|
import { StoreProduct } from '@medusajs/types';
|
||||||
import { QuestionMarkCircledIcon } from '@radix-ui/react-icons';
|
import { QuestionMarkCircledIcon } from '@radix-ui/react-icons';
|
||||||
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
|
||||||
import { Check, X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
import { PackageHeader } from '@kit/shared/components/package-header';
|
import { PackageHeader } from '@kit/shared/components/package-header';
|
||||||
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
|
import { AnalysisPackageWithVariant } from '@kit/shared/components/select-analysis-package';
|
||||||
@@ -26,6 +26,10 @@ import {
|
|||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
import { CheckWithBackground } from './check-with-background';
|
||||||
|
import ComparePackagesAddToCartButtons from './compare-packages-add-to-cart-buttons';
|
||||||
|
import DefaultPackageFeaturesRows from './default-package-features-rows';
|
||||||
|
|
||||||
export type AnalysisPackageElement = Pick<
|
export type AnalysisPackageElement = Pick<
|
||||||
StoreProduct,
|
StoreProduct,
|
||||||
'title' | 'id' | 'description'
|
'title' | 'id' | 'description'
|
||||||
@@ -35,14 +39,6 @@ export type AnalysisPackageElement = Pick<
|
|||||||
isIncludedInPremium: boolean;
|
isIncludedInPremium: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CheckWithBackground = () => {
|
|
||||||
return (
|
|
||||||
<div className="bg-primary w-min rounded-full p-1 text-white">
|
|
||||||
<Check className="size-3 stroke-2" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const PackageTableHead = async ({
|
const PackageTableHead = async ({
|
||||||
product,
|
product,
|
||||||
}: {
|
}: {
|
||||||
@@ -53,7 +49,7 @@ const PackageTableHead = async ({
|
|||||||
const { title, price, nrOfAnalyses } = product;
|
const { title, price, nrOfAnalyses } = product;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableHead className="py-2">
|
<TableHead className="xs:content-normal content-start py-2">
|
||||||
<PackageHeader
|
<PackageHeader
|
||||||
title={t(title)}
|
title={t(title)}
|
||||||
tagColor="bg-cyan"
|
tagColor="bg-cyan"
|
||||||
@@ -69,10 +65,12 @@ const ComparePackagesModal = async ({
|
|||||||
analysisPackages,
|
analysisPackages,
|
||||||
analysisPackageElements,
|
analysisPackageElements,
|
||||||
triggerElement,
|
triggerElement,
|
||||||
|
countryCode,
|
||||||
}: {
|
}: {
|
||||||
analysisPackages: AnalysisPackageWithVariant[];
|
analysisPackages: AnalysisPackageWithVariant[];
|
||||||
analysisPackageElements: AnalysisPackageElement[];
|
analysisPackageElements: AnalysisPackageElement[];
|
||||||
triggerElement: JSX.Element;
|
triggerElement: JSX.Element;
|
||||||
|
countryCode: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
|
|
||||||
@@ -92,7 +90,7 @@ const ComparePackagesModal = async ({
|
|||||||
<DialogContent
|
<DialogContent
|
||||||
className="min-h-screen max-w-fit min-w-screen"
|
className="min-h-screen max-w-fit min-w-screen"
|
||||||
customClose={
|
customClose={
|
||||||
<div className="inline-flex place-items-center-safe gap-1 align-middle">
|
<div className="absolute top-6 right-0 flex place-items-center-safe sm:top-0">
|
||||||
<p className="text-sm font-medium text-black">
|
<p className="text-sm font-medium text-black">
|
||||||
{t('common:close')}
|
{t('common:close')}
|
||||||
</p>
|
</p>
|
||||||
@@ -106,11 +104,13 @@ const ComparePackagesModal = async ({
|
|||||||
</VisuallyHidden>
|
</VisuallyHidden>
|
||||||
<div className="m-auto">
|
<div className="m-auto">
|
||||||
<div className="space-y-6 text-center">
|
<div className="space-y-6 text-center">
|
||||||
<h3>{t('product:healthPackageComparison.label')}</h3>
|
<h3 className="sm:text-xxl text-lg">
|
||||||
<p className="text-muted-foreground mx-auto w-3/5 text-sm">
|
{t('product:healthPackageComparison.label')}
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground text-sm sm:mx-auto sm:w-3/5">
|
||||||
{t('product:healthPackageComparison.description')}
|
{t('product:healthPackageComparison.description')}
|
||||||
</p>
|
</p>
|
||||||
<div className="max-h-[80vh] overflow-y-auto rounded-md border">
|
<div className="max-h-[50vh] overflow-y-auto rounded-md border sm:max-h-[70vh]">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -121,6 +121,8 @@ const ComparePackagesModal = async ({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
<DefaultPackageFeaturesRows />
|
||||||
|
|
||||||
{analysisPackageElements.map(
|
{analysisPackageElements.map(
|
||||||
({
|
({
|
||||||
title,
|
title,
|
||||||
@@ -136,12 +138,14 @@ const ComparePackagesModal = async ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={id}>
|
<TableRow key={id}>
|
||||||
<TableCell className="py-6 sm:max-w-[30vw]">
|
<TableCell className="relative py-6 sm:w-[30vw]">
|
||||||
{title}{' '}
|
{title}{' '}
|
||||||
{description && (
|
{description && (
|
||||||
<InfoTooltip
|
<InfoTooltip
|
||||||
content={description}
|
content={description}
|
||||||
icon={<QuestionMarkCircledIcon />}
|
icon={
|
||||||
|
<QuestionMarkCircledIcon className="absolute top-2 right-0 size-5 sm:static sm:ml-2 sm:size-4" />
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -164,6 +168,12 @@ const ComparePackagesModal = async ({
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ComparePackagesAddToCartButtons
|
||||||
|
countryCode={countryCode}
|
||||||
|
standardPackage={standardPackage}
|
||||||
|
premiumPackage={premiumPackage}
|
||||||
|
standardPlusPackage={standardPlusPackage}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { InfoTooltip } from '@/packages/shared/src/components/ui/info-tooltip';
|
||||||
|
import { QuestionMarkCircledIcon } from '@radix-ui/react-icons';
|
||||||
|
|
||||||
|
import { Trans } from '@kit/ui/makerkit/trans';
|
||||||
|
import { TableCell, TableRow } from '@kit/ui/shadcn/table';
|
||||||
|
|
||||||
|
import { CheckWithBackground } from './check-with-background';
|
||||||
|
|
||||||
|
const DefaultPackageFeaturesRows = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableRow key="digital-doctor-feedback">
|
||||||
|
<TableCell className="relative max-w-[30vw] py-6">
|
||||||
|
<Trans i18nKey="order-analysis-package:digitalDoctorFeedback" />
|
||||||
|
<InfoTooltip
|
||||||
|
content={
|
||||||
|
<Trans i18nKey="order-analysis-package:digitalDoctorFeedbackInfo" />
|
||||||
|
}
|
||||||
|
icon={
|
||||||
|
<QuestionMarkCircledIcon className="absolute top-2 right-0 size-5 sm:static sm:ml-2 sm:size-4" />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center" className="py-6">
|
||||||
|
<CheckWithBackground />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center" className="py-6">
|
||||||
|
<CheckWithBackground />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center" className="py-6">
|
||||||
|
<CheckWithBackground />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
|
||||||
|
<TableRow key="give-analyses">
|
||||||
|
<TableCell className="py-6 sm:max-w-[30vw]">
|
||||||
|
<Trans i18nKey="order-analysis-package:giveAnalyses" />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center" className="py-6">
|
||||||
|
<CheckWithBackground />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center" className="py-6">
|
||||||
|
<CheckWithBackground />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center" className="py-6">
|
||||||
|
<CheckWithBackground />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DefaultPackageFeaturesRows;
|
||||||
@@ -9,12 +9,12 @@ import { ShoppingCart } from 'lucide-react';
|
|||||||
|
|
||||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||||
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
|
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
|
||||||
import { Search } from '@kit/shared/components/ui/search';
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Card } from '@kit/ui/shadcn/card';
|
import { Card } from '@kit/ui/shadcn/card';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { UserNotifications } from '../_components/user-notifications';
|
import { UserNotifications } from '../_components/user-notifications';
|
||||||
|
import { getAccountBalanceSummary } from '../_lib/server/balance-actions';
|
||||||
import { type UserWorkspace } from '../_lib/server/load-user-workspace';
|
import { type UserWorkspace } from '../_lib/server/load-user-workspace';
|
||||||
|
|
||||||
export async function HomeMenuNavigation(props: {
|
export async function HomeMenuNavigation(props: {
|
||||||
@@ -23,13 +23,9 @@ export async function HomeMenuNavigation(props: {
|
|||||||
}) {
|
}) {
|
||||||
const { language } = await createI18nServerInstance();
|
const { language } = await createI18nServerInstance();
|
||||||
const { workspace, user, accounts } = props.workspace;
|
const { workspace, user, accounts } = props.workspace;
|
||||||
const totalValue = props.cart?.total
|
const balanceSummary = workspace?.id
|
||||||
? formatCurrency({
|
? await getAccountBalanceSummary(workspace.id)
|
||||||
currencyCode: props.cart.currency_code,
|
: null;
|
||||||
locale: language,
|
|
||||||
value: props.cart.total,
|
|
||||||
})
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const cartQuantityTotal =
|
const cartQuantityTotal =
|
||||||
props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
|
props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
|
||||||
@@ -47,29 +43,32 @@ export async function HomeMenuNavigation(props: {
|
|||||||
/> */}
|
/> */}
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-3">
|
<div className="flex items-center justify-end gap-3">
|
||||||
{/* TODO: add wallet functionality
|
|
||||||
<Card className="px-6 py-2">
|
<Card className="px-6 py-2">
|
||||||
<span>€ {Number(0).toFixed(2).replace('.', ',')}</span>
|
<span>
|
||||||
|
{formatCurrency({
|
||||||
|
value: balanceSummary?.totalBalance || 0,
|
||||||
|
locale: language,
|
||||||
|
currencyCode: 'EUR',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
</Card>
|
</Card>
|
||||||
*/}
|
|
||||||
{hasCartItems && (
|
|
||||||
<Button
|
|
||||||
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
|
|
||||||
variant="ghost"
|
|
||||||
>
|
|
||||||
<span className="flex items-center text-nowrap">{totalValue}</span>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Link href={pathsConfig.app.cart}>
|
<Link href={pathsConfig.app.cart}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
|
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
|
||||||
>
|
>
|
||||||
<ShoppingCart className="stroke-[1.5px]" />
|
<ShoppingCart className="stroke-[1.5px]" />
|
||||||
<Trans
|
<Trans i18nKey="common:shoppingCart" />{' '}
|
||||||
i18nKey="common:shoppingCartCount"
|
{hasCartItems && (
|
||||||
values={{ count: cartQuantityTotal }}
|
<>
|
||||||
/>
|
(
|
||||||
|
<span className="text-success font-bold">
|
||||||
|
{cartQuantityTotal}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<UserNotifications userId={user.id} />
|
<UserNotifications userId={user.id} />
|
||||||
|
|||||||
@@ -25,16 +25,22 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@kit/ui/dropdown-menu';
|
} from '@kit/ui/dropdown-menu';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
|
import { cn } from '@kit/ui/shadcn';
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/shadcn/avatar';
|
||||||
|
import { Button } from '@kit/ui/shadcn/button';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
// home imports
|
// home imports
|
||||||
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
|
import type { UserWorkspace } from '../_lib/server/load-user-workspace';
|
||||||
|
import { UserNotifications } from './user-notifications';
|
||||||
|
|
||||||
|
const PERSONAL_ACCOUNT_SLUG = 'personal';
|
||||||
|
|
||||||
export function HomeMobileNavigation(props: {
|
export function HomeMobileNavigation(props: {
|
||||||
workspace: UserWorkspace;
|
workspace: UserWorkspace;
|
||||||
cart: StoreCart | null;
|
cart: StoreCart | null;
|
||||||
}) {
|
}) {
|
||||||
const user = props.workspace.user;
|
const { user, accounts } = props.workspace;
|
||||||
|
|
||||||
const signOut = useSignOut();
|
const signOut = useSignOut();
|
||||||
const { data: personalAccountData } = usePersonalAccountData(user.id);
|
const { data: personalAccountData } = usePersonalAccountData(user.id);
|
||||||
@@ -85,10 +91,31 @@ export function HomeMobileNavigation(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger>
|
<div className="flex justify-between gap-3">
|
||||||
<Menu className={'h-9'} />
|
<Link href={pathsConfig.app.cart}>
|
||||||
</DropdownMenuTrigger>
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
|
||||||
|
>
|
||||||
|
<ShoppingCart className="stroke-[1.5px]" />
|
||||||
|
{hasCartItems && (
|
||||||
|
<>
|
||||||
|
(
|
||||||
|
<span className="text-success font-bold">
|
||||||
|
{cartQuantityTotal}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<UserNotifications userId={user.id} />
|
||||||
|
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Menu className="h-6 w-6" />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</div>
|
||||||
<DropdownMenuContent sideOffset={10} className={'w-screen rounded-none'}>
|
<DropdownMenuContent sideOffset={10} className={'w-screen rounded-none'}>
|
||||||
<If condition={props.cart && hasCartItems}>
|
<If condition={props.cart && hasCartItems}>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
@@ -148,6 +175,46 @@ export function HomeMobileNavigation(props: {
|
|||||||
</If>
|
</If>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<If condition={accounts.length > 0}>
|
||||||
|
<span className="text-muted-foreground px-2 text-xs">
|
||||||
|
<Trans
|
||||||
|
i18nKey={'teams:yourTeams'}
|
||||||
|
values={{ teamsCount: accounts.length }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{accounts.map((account) => (
|
||||||
|
<DropdownMenuItem key={account.value} asChild>
|
||||||
|
<Link
|
||||||
|
className={'s-full flex cursor-pointer items-center space-x-2'}
|
||||||
|
href={`${pathsConfig.app.home}/${account.value}`}
|
||||||
|
>
|
||||||
|
<div className={'flex items-center'}>
|
||||||
|
<Avatar className={'h-5 w-5 rounded-xs ' + account.image}>
|
||||||
|
<AvatarImage
|
||||||
|
{...(account.image && { src: account.image })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AvatarFallback
|
||||||
|
className={cn('rounded-md', {
|
||||||
|
['bg-background']:
|
||||||
|
PERSONAL_ACCOUNT_SLUG === account.value,
|
||||||
|
['group-hover:bg-background']:
|
||||||
|
PERSONAL_ACCOUNT_SLUG !== account.value,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{account.label ? account.label[0] : ''}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<span className={'pl-3'}>{account.label}</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</If>
|
||||||
|
|
||||||
<SignOutDropdownItem onSignOut={() => signOut.mutateAsync()} />
|
<SignOutDropdownItem onSignOut={() => signOut.mutateAsync()} />
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import Link from 'next/link';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { pathsConfig } from '@/packages/shared/src/config';
|
import { pathsConfig } from '@/packages/shared/src/config';
|
||||||
import { ComponentInstanceIcon } from '@radix-ui/react-icons';
|
import { ComponentInstanceIcon } from '@radix-ui/react-icons';
|
||||||
import { ChevronRight, HeartPulse } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default function OrderBlock({
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col sm:gap-4">
|
||||||
{analysisOrder && (
|
{analysisOrder && (
|
||||||
<OrderItemsTable
|
<OrderItemsTable
|
||||||
items={itemsAnalysisPackage}
|
items={itemsAnalysisPackage}
|
||||||
@@ -61,6 +61,7 @@ export default function OrderBlock({
|
|||||||
id: analysisOrder.id,
|
id: analysisOrder.id,
|
||||||
status: analysisOrder.status,
|
status: analysisOrder.status,
|
||||||
}}
|
}}
|
||||||
|
isPackage
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{itemsTtoService && (
|
{itemsTtoService && (
|
||||||
@@ -82,6 +83,8 @@ export default function OrderBlock({
|
|||||||
items={itemsOther}
|
items={itemsOther}
|
||||||
title="orders:table.otherOrders"
|
title="orders:table.otherOrders"
|
||||||
order={{
|
order={{
|
||||||
|
medusaOrderId: analysisOrder?.medusa_order_id,
|
||||||
|
id: analysisOrder?.id,
|
||||||
status: analysisOrder?.status,
|
status: analysisOrder?.status,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
import type { Order } from '~/lib/types/order';
|
import type { Order } from '~/lib/types/order';
|
||||||
|
|
||||||
import { cancelTtoBooking } from '../../_lib/server/actions';
|
import { cancelTtoBooking } from '../../_lib/server/actions';
|
||||||
|
import MobileTableRow from '../cart/mobile-table-row';
|
||||||
import { logAnalysisResultsNavigateAction } from './actions';
|
import { logAnalysisResultsNavigateAction } from './actions';
|
||||||
|
|
||||||
export type OrderItemType = 'analysisOrder' | 'ttoService';
|
export type OrderItemType = 'analysisOrder' | 'ttoService';
|
||||||
@@ -32,11 +33,13 @@ export default function OrderItemsTable({
|
|||||||
title,
|
title,
|
||||||
order,
|
order,
|
||||||
type = 'analysisOrder',
|
type = 'analysisOrder',
|
||||||
|
isPackage = false,
|
||||||
}: {
|
}: {
|
||||||
items: StoreOrderLineItem[];
|
items: StoreOrderLineItem[];
|
||||||
title: string;
|
title: string;
|
||||||
order: Order;
|
order: Order;
|
||||||
type?: OrderItemType;
|
type?: OrderItemType;
|
||||||
|
isPackage?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||||
@@ -63,52 +66,111 @@ export default function OrderItemsTable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className="border-separate rounded-lg border">
|
<>
|
||||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
<Table className="border-separate rounded-lg border p-2 sm:hidden">
|
||||||
<TableRow>
|
<TableBody>
|
||||||
<TableHead className="px-6">
|
{items
|
||||||
<Trans i18nKey={title} />
|
.sort((a, b) =>
|
||||||
</TableHead>
|
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||||
<TableHead className="px-6">
|
)
|
||||||
<Trans i18nKey="orders:table.createdAt" />
|
.map((orderItem) => (
|
||||||
</TableHead>
|
<div key={`${orderItem.id}-mobile`}>
|
||||||
{order.location && (
|
<MobileTableRow
|
||||||
<TableHead className="px-6">
|
titleKey={title}
|
||||||
<Trans i18nKey="orders:table.location" />
|
value={orderItem.product_title || ''}
|
||||||
</TableHead>
|
|
||||||
)}
|
|
||||||
<TableHead className="px-6">
|
|
||||||
<Trans i18nKey="orders:table.status" />
|
|
||||||
</TableHead>
|
|
||||||
{isAnalysisOrder && <TableHead className="px-6"></TableHead>}
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{items
|
|
||||||
.sort((a, b) =>
|
|
||||||
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
|
||||||
)
|
|
||||||
.map((orderItem) => (
|
|
||||||
<TableRow className="w-full" key={orderItem.id}>
|
|
||||||
<TableCell className="w-[100%] px-6 text-left">
|
|
||||||
<p className="txt-medium-plus text-ui-fg-base">
|
|
||||||
{orderItem.product_title}
|
|
||||||
</p>
|
|
||||||
</TableCell>
|
|
||||||
|
|
||||||
<TableCell className="px-6 whitespace-nowrap">
|
|
||||||
{formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')}
|
|
||||||
</TableCell>
|
|
||||||
{order.location && (
|
|
||||||
<TableCell className="min-w-[180px] px-6">
|
|
||||||
{order.location}
|
|
||||||
</TableCell>
|
|
||||||
)}
|
|
||||||
<TableCell className="min-w-[180px] px-6">
|
|
||||||
<Trans
|
|
||||||
i18nKey={`orders:status.${type}.${order?.status ?? 'CONFIRMED'}`}
|
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
<MobileTableRow
|
||||||
|
titleKey="orders:table.createdAt"
|
||||||
|
value={formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')}
|
||||||
|
/>
|
||||||
|
{order.location && (
|
||||||
|
<MobileTableRow
|
||||||
|
titleKey="orders:table.location"
|
||||||
|
value={order.location}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<MobileTableRow
|
||||||
|
titleKey="orders:table.status"
|
||||||
|
value={
|
||||||
|
isPackage
|
||||||
|
? `orders:status.analysisPackageOrder.${order?.status ?? 'CONFIRMED'}`
|
||||||
|
: `orders:status.${type}.${order?.status ?? 'CONFIRMED'}`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell />
|
||||||
|
<TableCell className="flex w-full items-center justify-end p-0 pt-2">
|
||||||
|
<Button size="sm" onClick={openDetailedView}>
|
||||||
|
<Trans i18nKey="analysis-results:view" />
|
||||||
|
</Button>
|
||||||
|
{isTtoservice && order.bookingCode && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="bg-warning/90 hover:bg-warning"
|
||||||
|
onClick={() => setIsConfirmOpen(true)}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="analysis-results:cancel" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<Table className="hidden border-separate rounded-lg border sm:block">
|
||||||
|
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="px-6">
|
||||||
|
<Trans i18nKey={title} />
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="px-6">
|
||||||
|
<Trans i18nKey="orders:table.createdAt" />
|
||||||
|
</TableHead>
|
||||||
|
{order.location && (
|
||||||
|
<TableHead className="px-6">
|
||||||
|
<Trans i18nKey="orders:table.location" />
|
||||||
|
</TableHead>
|
||||||
|
)}
|
||||||
|
<TableHead className="px-6">
|
||||||
|
<Trans i18nKey="orders:table.status" />
|
||||||
|
</TableHead>
|
||||||
|
{isAnalysisOrder && <TableHead className="px-6"></TableHead>}
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{items
|
||||||
|
.sort((a, b) =>
|
||||||
|
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||||
|
)
|
||||||
|
.map((orderItem) => (
|
||||||
|
<TableRow className="w-full" key={orderItem.id}>
|
||||||
|
<TableCell className="w-[100%] px-6 text-left">
|
||||||
|
<p className="txt-medium-plus text-ui-fg-base">
|
||||||
|
{orderItem.product_title}
|
||||||
|
</p>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
<TableCell className="px-6 whitespace-nowrap">
|
||||||
|
{formatDate(orderItem.created_at, 'dd.MM.yyyy HH:mm')}
|
||||||
|
</TableCell>
|
||||||
|
{order.location && (
|
||||||
|
<TableCell className="min-w-[180px] px-6">
|
||||||
|
{order.location}
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
<TableCell className="min-w-[180px] px-6">
|
||||||
|
{isPackage ? (
|
||||||
|
<Trans
|
||||||
|
i18nKey={`orders:status.analysisPackageOrder.${order?.status ?? 'CONFIRMED'}`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Trans
|
||||||
|
i18nKey={`orders:status.${type}.${order?.status ?? 'CONFIRMED'}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="px-6 text-right">
|
<TableCell className="px-6 text-right">
|
||||||
<Button size="sm" onClick={openDetailedView}>
|
<Button size="sm" onClick={openDetailedView}>
|
||||||
@@ -144,6 +206,6 @@ export default function OrderItemsTable({
|
|||||||
descriptionKey="orders:confirmBookingCancel.description"
|
descriptionKey="orders:confirmBookingCancel.description"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Table>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { InfoTooltip } from '@/packages/shared/src/components/ui/info-tooltip';
|
import { InfoTooltip } from '@/packages/shared/src/components/ui/info-tooltip';
|
||||||
import { HeartPulse } from 'lucide-react';
|
|
||||||
|
|
||||||
import { Button } from '@kit/ui/shadcn/button';
|
import { Button } from '@kit/ui/shadcn/button';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
|
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
|
||||||
import { createNotificationsApi } from '@/packages/features/notifications/src/server/api';
|
|
||||||
import { getSupabaseServerAdminClient } from '@/packages/supabase/src/clients/server-admin-client';
|
|
||||||
import { listProductTypes } from '@lib/data';
|
import { listProductTypes } from '@lib/data';
|
||||||
import { initiateMultiPaymentSession, placeOrder } from '@lib/data/cart';
|
import { initiateMultiPaymentSession, placeOrder } from '@lib/data/cart';
|
||||||
import type { StoreCart, StoreOrder } from '@medusajs/types';
|
import type { StoreCart, StoreOrder } from '@medusajs/types';
|
||||||
@@ -346,7 +344,6 @@ const sendEmail = async ({
|
|||||||
partnerLocationName: string;
|
partnerLocationName: string;
|
||||||
language: string;
|
language: string;
|
||||||
}) => {
|
}) => {
|
||||||
const client = getSupabaseServerAdminClient();
|
|
||||||
try {
|
try {
|
||||||
const { renderSynlabAnalysisPackageEmail } = await import(
|
const { renderSynlabAnalysisPackageEmail } = await import(
|
||||||
'@kit/email-templates'
|
'@kit/email-templates'
|
||||||
@@ -372,10 +369,6 @@ const sendEmail = async ({
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw new Error(`Failed to send email, message=${error}`);
|
throw new Error(`Failed to send email, message=${error}`);
|
||||||
});
|
});
|
||||||
await createNotificationsApi(client).createNotification({
|
|
||||||
account_id: account.id,
|
|
||||||
body: html,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to send email, message=${error}`);
|
throw new Error(`Failed to send email, message=${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const isValidOpenAiEnv = async () => {
|
|||||||
const client = new OpenAI();
|
const client = new OpenAI();
|
||||||
await client.models.list();
|
await client.models.list();
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import {
|
import { pathsConfig } from '@/packages/shared/src/config';
|
||||||
getTeamAccountSidebarConfig,
|
|
||||||
pathsConfig,
|
|
||||||
} from '@/packages/shared/src/config';
|
|
||||||
|
|
||||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||||
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
|
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
|
||||||
@@ -28,25 +25,6 @@ export function TeamAccountNavigationMenu(props: {
|
|||||||
[rawAccounts],
|
[rawAccounts],
|
||||||
);
|
);
|
||||||
|
|
||||||
const routes = getTeamAccountSidebarConfig(account.slug).routes.reduce<
|
|
||||||
Array<{
|
|
||||||
path: string;
|
|
||||||
label: string;
|
|
||||||
Icon?: React.ReactNode;
|
|
||||||
end?: boolean | ((path: string) => boolean);
|
|
||||||
}>
|
|
||||||
>((acc, item) => {
|
|
||||||
if ('children' in item) {
|
|
||||||
return [...acc, ...item.children];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('divider' in item) {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...acc, item];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'flex w-full flex-1 justify-between'}>
|
<div className={'flex w-full flex-1 justify-between'}>
|
||||||
<div className={'flex items-center space-x-8'}>
|
<div className={'flex items-center space-x-8'}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
@@ -43,11 +43,13 @@ export default function TeamAccountStatistics({
|
|||||||
accountBenefitStatistics,
|
accountBenefitStatistics,
|
||||||
expensesOverview,
|
expensesOverview,
|
||||||
}: TeamAccountStatisticsProps) {
|
}: TeamAccountStatisticsProps) {
|
||||||
const currentDate = new Date();
|
const date = useMemo<DateRange | undefined>(() => {
|
||||||
const [date, setDate] = useState<DateRange | undefined>({
|
const currentDate = new Date();
|
||||||
from: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1),
|
return {
|
||||||
to: new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0),
|
from: new Date(currentDate.getFullYear(), currentDate.getMonth(), 1),
|
||||||
});
|
to: new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0),
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
const {
|
const {
|
||||||
i18n: { language },
|
i18n: { language },
|
||||||
} = useTranslation();
|
} = useTranslation();
|
||||||
|
|||||||
@@ -29,10 +29,13 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
|
|||||||
|
|
||||||
const account = await api.getTeamAccount(accountSlug);
|
const account = await api.getTeamAccount(accountSlug);
|
||||||
const { members } = await api.getMembers(accountSlug);
|
const { members } = await api.getMembers(accountSlug);
|
||||||
|
const eligibleMembersCount = members.filter(
|
||||||
|
({ is_eligible_for_benefits }) => !!is_eligible_for_benefits,
|
||||||
|
).length;
|
||||||
const [expensesOverview, companyParams] = await Promise.all([
|
const [expensesOverview, companyParams] = await Promise.all([
|
||||||
loadTeamAccountBenefitExpensesOverview({
|
loadTeamAccountBenefitExpensesOverview({
|
||||||
companyId: account.id,
|
companyId: account.id,
|
||||||
employeeCount: members.length,
|
employeeCount: eligibleMembersCount,
|
||||||
}),
|
}),
|
||||||
api.getTeamAccountParams(account.id),
|
api.getTeamAccountParams(account.id),
|
||||||
]);
|
]);
|
||||||
@@ -42,7 +45,7 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
|
|||||||
<HealthBenefitForm
|
<HealthBenefitForm
|
||||||
account={account}
|
account={account}
|
||||||
companyParams={companyParams}
|
companyParams={companyParams}
|
||||||
employeeCount={members.length}
|
employeeCount={eligibleMembersCount}
|
||||||
expensesOverview={expensesOverview}
|
expensesOverview={expensesOverview}
|
||||||
/>
|
/>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
|
|||||||
@@ -99,11 +99,7 @@ export async function loadAccountMembersBenefitsUsage(
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (data ?? []) as unknown as {
|
return data ?? [];
|
||||||
personal_account_id: string;
|
|
||||||
benefit_amount: number;
|
|
||||||
benefit_unused_amount: number;
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ async function TeamAccountMembersPage({ params }: TeamAccountMembersPageProps) {
|
|||||||
|
|
||||||
const canManageRoles = account.permissions.includes('roles.manage');
|
const canManageRoles = account.permissions.includes('roles.manage');
|
||||||
const canManageInvitations = account.permissions.includes('invites.manage');
|
const canManageInvitations = account.permissions.includes('invites.manage');
|
||||||
|
const canUpdateBenefit = account.permissions.includes('benefit.manage');
|
||||||
|
|
||||||
const isPrimaryOwner = account.primary_owner_user_id === user.id;
|
const isPrimaryOwner = account.primary_owner_user_id === user.id;
|
||||||
const currentUserRoleHierarchy = account.role_hierarchy_level;
|
const currentUserRoleHierarchy = account.role_hierarchy_level;
|
||||||
@@ -103,6 +104,7 @@ async function TeamAccountMembersPage({ params }: TeamAccountMembersPageProps) {
|
|||||||
members={members}
|
members={members}
|
||||||
isPrimaryOwner={isPrimaryOwner}
|
isPrimaryOwner={isPrimaryOwner}
|
||||||
canManageRoles={canManageRoles}
|
canManageRoles={canManageRoles}
|
||||||
|
canUpdateBenefit={canUpdateBenefit}
|
||||||
membersBenefitsUsage={membersBenefitsUsage}
|
membersBenefitsUsage={membersBenefitsUsage}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ async function SelectPackagePage() {
|
|||||||
<Scale className="size-4 stroke-[1.5px]" />
|
<Scale className="size-4 stroke-[1.5px]" />
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
countryCode={countryCode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SelectAnalysisPackages
|
<SelectAnalysisPackages
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ async function getAssignedOrderIds() {
|
|||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('doctor_analysis_feedback')
|
.from('doctor_analysis_feedback')
|
||||||
.select('analysis_order_id')
|
.select('analysis_order_id')
|
||||||
|
.not('status', 'is', 'COMPLETED')
|
||||||
.not('doctor_user_id', 'is', null)
|
.not('doctor_user_id', 'is', null)
|
||||||
.throwOnError();
|
.throwOnError();
|
||||||
|
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export async function canCreateAnalysisResponseElement({
|
|||||||
|
|
||||||
if (existingAnalysisResponseElement.response_value && !responseValue) {
|
if (existingAnalysisResponseElement.response_value && !responseValue) {
|
||||||
log(
|
log(
|
||||||
`Analysis response element id=${analysisElementOriginalId} already exists for order with response value ${existingAnalysisResponseElement.response_value} but new response has no value`,
|
`Analysis response element id=${analysisElementOriginalId} ${existingAnalysisResponseElement.response_value} but new response has no value`,
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -371,8 +371,13 @@ export async function readPrivateMessageResponse({
|
|||||||
const hasInvalidOrderId = isNaN(analysisOrderId);
|
const hasInvalidOrderId = isNaN(analysisOrderId);
|
||||||
|
|
||||||
if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) {
|
if (hasInvalidOrderId || !messageResponse || !patientPersonalCode) {
|
||||||
|
console.log({
|
||||||
|
privateMessageContent,
|
||||||
|
saadetis: privateMessageContent?.Saadetis,
|
||||||
|
messageResponse,
|
||||||
|
});
|
||||||
console.error(
|
console.error(
|
||||||
`Invalid order id or message response or patient personal code, medipostExternalOrderId=${medipostExternalOrderId}, privateMessageId=${privateMessageId}`,
|
`Invalid !order id or message response or patient personal code, medipostExternalOrderId=${medipostExternalOrderId}, privateMessageId=${privateMessageId}`,
|
||||||
);
|
);
|
||||||
await upsertMedipostActionLog({
|
await upsertMedipostActionLog({
|
||||||
action: 'sync_analysis_results_from_medipost',
|
action: 'sync_analysis_results_from_medipost',
|
||||||
@@ -397,7 +402,7 @@ export async function readPrivateMessageResponse({
|
|||||||
try {
|
try {
|
||||||
analysisOrder = await getAnalysisOrder({ analysisOrderId });
|
analysisOrder = await getAnalysisOrder({ analysisOrderId });
|
||||||
medusaOrderId = analysisOrder.medusa_order_id;
|
medusaOrderId = analysisOrder.medusa_order_id;
|
||||||
} catch (e) {
|
} catch {
|
||||||
if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) {
|
if (IS_ENABLED_DELETE_PRIVATE_MESSAGE) {
|
||||||
await deletePrivateMessage(privateMessageId);
|
await deletePrivateMessage(privateMessageId);
|
||||||
}
|
}
|
||||||
@@ -568,7 +573,6 @@ export async function sendOrderToMedipost({
|
|||||||
phone: account.phone ?? '',
|
phone: account.phone ?? '',
|
||||||
},
|
},
|
||||||
orderId: medreportOrder.id,
|
orderId: medreportOrder.id,
|
||||||
orderCreatedAt: new Date(medreportOrder.created_at),
|
|
||||||
comment: '',
|
comment: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ export async function composeOrderXML({
|
|||||||
analysisElements,
|
analysisElements,
|
||||||
person,
|
person,
|
||||||
orderId,
|
orderId,
|
||||||
orderCreatedAt,
|
|
||||||
comment,
|
comment,
|
||||||
}: {
|
}: {
|
||||||
analyses: AnalysesWithGroupsAndElements;
|
analyses: AnalysesWithGroupsAndElements;
|
||||||
@@ -45,7 +44,6 @@ export async function composeOrderXML({
|
|||||||
phone: string;
|
phone: string;
|
||||||
};
|
};
|
||||||
orderId: number;
|
orderId: number;
|
||||||
orderCreatedAt: Date;
|
|
||||||
comment?: string;
|
comment?: string;
|
||||||
}) {
|
}) {
|
||||||
const analysisGroups: Tables<{ schema: 'medreport' }, 'analysis_groups'>[] =
|
const analysisGroups: Tables<{ schema: 'medreport' }, 'analysis_groups'>[] =
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
||||||
import { MontonioOrderHandlerService } from '@/packages/billing/montonio/src';
|
import { MontonioOrderHandlerService } from '@/packages/billing/montonio/src';
|
||||||
|
import { getLogger } from '@/packages/shared/src/logger';
|
||||||
import { addToCart, deleteLineItem, retrieveCart } from '@lib/data/cart';
|
import { addToCart, deleteLineItem, retrieveCart } from '@lib/data/cart';
|
||||||
import { getCartId } from '@lib/data/cookies';
|
import { getCartId } from '@lib/data/cookies';
|
||||||
import { StoreCartLineItem, StoreProductVariant } from '@medusajs/types';
|
import { StoreCartLineItem, StoreProductVariant } from '@medusajs/types';
|
||||||
@@ -44,12 +45,17 @@ export async function handleAddToCart({
|
|||||||
selectedVariant: Pick<StoreProductVariant, 'id'>;
|
selectedVariant: Pick<StoreProductVariant, 'id'>;
|
||||||
countryCode: string;
|
countryCode: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
const logger = await getLogger();
|
||||||
} catch (e) {
|
const ctx = {
|
||||||
console.error('medusa card error: ', e);
|
countryCode,
|
||||||
}
|
selectedVariant,
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.info(ctx, 'Adding to cart...');
|
||||||
|
|
||||||
const { account } = await loadCurrentUserAccount();
|
const { account } = await loadCurrentUserAccount();
|
||||||
if (!account) {
|
if (!account) {
|
||||||
|
logger.error(ctx, 'Account not found');
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export async function renderBookTimeFailedEmail({
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text>
|
<Text>
|
||||||
Broneeringu {reservationId} Connected Online'i saatmine ei
|
Broneeringu {reservationId} Connected Online'i saatmine ei
|
||||||
õnnestunud, kliendile tuleb teha tagasimakse.
|
õnnestunud, kliendile tuleb teha tagasimakse.
|
||||||
</Text>
|
</Text>
|
||||||
<Text>Saadud error: {error}</Text>
|
<Text>Saadud error: {error}</Text>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export async function renderNewJobsAvailableEmail({
|
|||||||
</Text>
|
</Text>
|
||||||
<ul className="list-none text-[16px] leading-[24px]">
|
<ul className="list-none text-[16px] leading-[24px]">
|
||||||
{analysisResponseIds.map((analysisResponseId, index) => (
|
{analysisResponseIds.map((analysisResponseId, index) => (
|
||||||
<li>
|
<li key={index}>
|
||||||
<Link
|
<Link
|
||||||
key={analysisResponseId}
|
key={analysisResponseId}
|
||||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisResponseId}`}
|
href={`${process.env.NEXT_PUBLIC_SITE_URL}/doctor/analysis/${analysisResponseId}`}
|
||||||
|
|||||||
@@ -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."
|
||||||
|
}
|
||||||
@@ -43,17 +43,9 @@ export function PersonalAccountDropdown({
|
|||||||
showProfileName = true,
|
showProfileName = true,
|
||||||
paths,
|
paths,
|
||||||
features,
|
features,
|
||||||
account,
|
|
||||||
accounts = [],
|
accounts = [],
|
||||||
}: {
|
}: {
|
||||||
user: User;
|
user: User;
|
||||||
|
|
||||||
account?: {
|
|
||||||
id: string | null;
|
|
||||||
name: string | null;
|
|
||||||
picture_url: string | null;
|
|
||||||
application_role: ApplicationRole | null;
|
|
||||||
};
|
|
||||||
accounts: {
|
accounts: {
|
||||||
label: string | null;
|
label: string | null;
|
||||||
value: string | null;
|
value: string | null;
|
||||||
@@ -102,8 +94,8 @@ export function PersonalAccountDropdown({
|
|||||||
const hasDoctorRole =
|
const hasDoctorRole =
|
||||||
personalAccountData?.application_role === ApplicationRoleEnum.Doctor;
|
personalAccountData?.application_role === ApplicationRoleEnum.Doctor;
|
||||||
|
|
||||||
return hasDoctorRole && hasTotpFactor;
|
return hasDoctorRole;
|
||||||
}, [personalAccountData, hasTotpFactor]);
|
}, [personalAccountData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ class AccountsApi {
|
|||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('accounts_memberships')
|
.from('accounts_memberships')
|
||||||
.select('account_id', { count: 'exact', head: true })
|
.select('account_id', { count: 'exact', head: true })
|
||||||
.eq('account_id', accountId);
|
.eq('user_id', accountId);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
import type { AccountBalanceEntry } from '../../types/account-balance-entry';
|
import type { AccountBalanceEntry } from '../../types/account-balance-entry';
|
||||||
|
import { createAccountsApi } from '../api';
|
||||||
|
|
||||||
export type AccountBalanceSummary = {
|
export type AccountBalanceSummary = {
|
||||||
totalBalance: number;
|
totalBalance: number;
|
||||||
@@ -88,6 +89,11 @@ export class AccountBalanceService {
|
|||||||
* Get balance summary for dashboard display
|
* Get balance summary for dashboard display
|
||||||
*/
|
*/
|
||||||
async getBalanceSummary(accountId: string): Promise<AccountBalanceSummary> {
|
async getBalanceSummary(accountId: string): Promise<AccountBalanceSummary> {
|
||||||
|
const api = createAccountsApi(this.supabase);
|
||||||
|
|
||||||
|
const hasAccountTeamMembership =
|
||||||
|
await api.hasAccountTeamMembership(accountId);
|
||||||
|
|
||||||
const [balance, entries] = await Promise.all([
|
const [balance, entries] = await Promise.all([
|
||||||
this.getAccountBalance(accountId),
|
this.getAccountBalance(accountId),
|
||||||
this.getAccountBalanceEntries(accountId, { limit: 5 }),
|
this.getAccountBalanceEntries(accountId, { limit: 5 }),
|
||||||
@@ -113,6 +119,14 @@ export class AccountBalanceService {
|
|||||||
const expiringSoon =
|
const expiringSoon =
|
||||||
expiringData?.reduce((sum, entry) => sum + (entry.amount || 0), 0) || 0;
|
expiringData?.reduce((sum, entry) => sum + (entry.amount || 0), 0) || 0;
|
||||||
|
|
||||||
|
if (!hasAccountTeamMembership) {
|
||||||
|
return {
|
||||||
|
totalBalance: 0,
|
||||||
|
expiringSoon,
|
||||||
|
recentEntries: entries.entries,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalBalance: balance,
|
totalBalance: balance,
|
||||||
expiringSoon,
|
expiringSoon,
|
||||||
@@ -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> {
|
async processPeriodicBenefitDistributions(): Promise<void> {
|
||||||
console.info('Processing periodic benefit distributions...');
|
console.info('Processing periodic benefit distributions...');
|
||||||
const { error } = await this.supabase
|
const { error } = await this.supabase
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { Tables } from '@kit/supabase/database';
|
|||||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { createTeamAccountsApi } from '@kit/team-accounts/api';
|
import { createTeamAccountsApi } from '@kit/team-accounts/api';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
|
||||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
@@ -12,14 +11,6 @@ import { Heading } from '@kit/ui/heading';
|
|||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||||
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
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 { AdminBanUserDialog } from './admin-ban-user-dialog';
|
||||||
import { AdminDeleteAccountDialog } from './admin-delete-account-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) {
|
async function getMemberships(userId: string) {
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,13 @@ export function MultiFactorChallengeContainer({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const verifyMFAChallenge = useVerifyMFAChallenge({
|
const verifyMFAChallenge = useVerifyMFAChallenge({
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
router.replace(paths.redirectPath);
|
try {
|
||||||
|
await fetch('/api/after-mfa', { method: 'POST' });
|
||||||
|
router.replace(paths.redirectPath);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -42,14 +42,14 @@ export const selectJobAction = doctorAction(
|
|||||||
|
|
||||||
revalidateDoctorAnalysis();
|
revalidateDoctorAnalysis();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
logger.error('Failed to select job', e);
|
logger.error({ error }, 'Failed to select job');
|
||||||
if (e instanceof Error) {
|
if (error instanceof Error) {
|
||||||
revalidateDoctorAnalysis();
|
revalidateDoctorAnalysis();
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
reason:
|
reason:
|
||||||
e['message'] === ErrorReason.JOB_ASSIGNED
|
error['message'] === ErrorReason.JOB_ASSIGNED
|
||||||
? ErrorReason.JOB_ASSIGNED
|
? ErrorReason.JOB_ASSIGNED
|
||||||
: ErrorReason.UNKNOWN,
|
: ErrorReason.UNKNOWN,
|
||||||
};
|
};
|
||||||
@@ -133,16 +133,16 @@ export const giveFeedbackAction = doctorAction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (e: any) {
|
} catch (error) {
|
||||||
if (isCompleted) {
|
if (isCompleted) {
|
||||||
await createNotificationLog({
|
await createNotificationLog({
|
||||||
action: NotificationAction.PATIENT_DOCTOR_FEEDBACK_RECEIVED,
|
action: NotificationAction.PATIENT_DOCTOR_FEEDBACK_RECEIVED,
|
||||||
status: 'FAIL',
|
status: 'FAIL',
|
||||||
comment: e?.message,
|
comment: error instanceof Error ? error.message : '',
|
||||||
relatedRecordId: analysisOrderId,
|
relatedRecordId: analysisOrderId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
logger.error('Failed to give feedback', e);
|
logger.error({ error }, 'Failed to give feedback');
|
||||||
return { success: false, reason: ErrorReason.UNKNOWN };
|
return { success: false, reason: ErrorReason.UNKNOWN };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const getOpenResponsesAction = doctorAction(
|
|||||||
const data = await getOpenResponses({ page, pageSize });
|
const data = await getOpenResponses({ page, pageSize });
|
||||||
return { success: true, data };
|
return { success: true, data };
|
||||||
} catch (error) {
|
} 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.' };
|
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({
|
export const AnalysisResponsesSchema = z.object({
|
||||||
user_id: z.string(),
|
user_id: z.string(),
|
||||||
analysis_order_id: AnalysisOrderIdSchema,
|
analysis_order: AnalysisOrderIdSchema,
|
||||||
});
|
});
|
||||||
export type AnalysisResponses = z.infer<typeof AnalysisResponsesSchema>;
|
export type AnalysisResponses = z.infer<typeof AnalysisResponsesSchema>;
|
||||||
|
|
||||||
@@ -56,8 +56,8 @@ export const AnalysisResponseSchema = z.object({
|
|||||||
analysis_response_id: z.number(),
|
analysis_response_id: z.number(),
|
||||||
analysis_element_original_id: z.string(),
|
analysis_element_original_id: z.string(),
|
||||||
unit: z.string().nullable(),
|
unit: z.string().nullable(),
|
||||||
response_value: z.number(),
|
response_value: z.number().nullable(),
|
||||||
response_time: z.string(),
|
response_time: z.string().nullable(),
|
||||||
norm_upper: z.number().nullable(),
|
norm_upper: z.number().nullable(),
|
||||||
norm_upper_included: z.boolean().nullable(),
|
norm_upper_included: z.boolean().nullable(),
|
||||||
norm_lower: z.number().nullable(),
|
norm_lower: z.number().nullable(),
|
||||||
@@ -74,8 +74,8 @@ export const AnalysisResponseSchema = z.object({
|
|||||||
analysis_response_id: z.number(),
|
analysis_response_id: z.number(),
|
||||||
analysis_element_original_id: z.string(),
|
analysis_element_original_id: z.string(),
|
||||||
unit: z.string().nullable(),
|
unit: z.string().nullable(),
|
||||||
response_value: z.number(),
|
response_value: z.number().nullable(),
|
||||||
response_time: z.string(),
|
response_time: z.string().nullable(),
|
||||||
norm_upper: z.number().nullable(),
|
norm_upper: z.number().nullable(),
|
||||||
norm_upper_included: z.boolean().nullable(),
|
norm_upper_included: z.boolean().nullable(),
|
||||||
norm_lower: z.number().nullable(),
|
norm_lower: z.number().nullable(),
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ export const ElementSchema = z.object({
|
|||||||
analysis_response_id: z.number(),
|
analysis_response_id: z.number(),
|
||||||
analysis_element_original_id: z.string(),
|
analysis_element_original_id: z.string(),
|
||||||
unit: z.string().nullable(),
|
unit: z.string().nullable(),
|
||||||
response_value: z.number(),
|
response_value: z.number().nullable(),
|
||||||
response_time: z.string(),
|
response_time: z.string().nullable(),
|
||||||
norm_upper: z.number().nullable(),
|
norm_upper: z.number().nullable(),
|
||||||
norm_upper_included: z.boolean().nullable(),
|
norm_upper_included: z.boolean().nullable(),
|
||||||
norm_lower: z.number().nullable(),
|
norm_lower: z.number().nullable(),
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'server-only';
|
import 'server-only';
|
||||||
|
|
||||||
|
import { listOrdersByIds, retrieveOrder } from '@lib/data/orders';
|
||||||
import { isBefore } from 'date-fns';
|
import { isBefore } from 'date-fns';
|
||||||
|
|
||||||
import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates';
|
import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates';
|
||||||
|
import { getLogger } from '@kit/shared/logger';
|
||||||
import { getFullName } from '@kit/shared/utils';
|
import { getFullName } from '@kit/shared/utils';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
import { createUserAnalysesApi } from '@kit/user-analyses/api';
|
import { createUserAnalysesApi } from '@kit/user-analyses/api';
|
||||||
@@ -31,7 +33,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
|
|||||||
|
|
||||||
const [
|
const [
|
||||||
{ data: doctorFeedbackItems },
|
{ data: doctorFeedbackItems },
|
||||||
{ data: medusaOrderItems },
|
medusaOrders,
|
||||||
{ data: analysisResponseElements },
|
{ data: analysisResponseElements },
|
||||||
{ data: accounts },
|
{ data: accounts },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
@@ -43,11 +45,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
|
|||||||
'analysis_order_id',
|
'analysis_order_id',
|
||||||
analysisResponses.map((r) => r.analysis_order_id.id),
|
analysisResponses.map((r) => r.analysis_order_id.id),
|
||||||
),
|
),
|
||||||
supabase
|
listOrdersByIds(medusaOrderIds),
|
||||||
.schema('public')
|
|
||||||
.from('order_item')
|
|
||||||
.select('order_id, item_id(product_title, product_type)')
|
|
||||||
.in('order_id', medusaOrderIds),
|
|
||||||
supabase
|
supabase
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('analysis_response_elements')
|
.from('analysis_response_elements')
|
||||||
@@ -56,10 +54,15 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
|
|||||||
supabase
|
supabase
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('accounts')
|
.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),
|
.in('primary_owner_user_id', userIds),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (!analysisResponseElements || analysisResponseElements?.length === 0) {
|
||||||
|
console.info(`${analysisResponseIds} has no response elements`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const doctorUserIds =
|
const doctorUserIds =
|
||||||
doctorFeedbackItems
|
doctorFeedbackItems
|
||||||
?.map((item) => item.doctor_user_id)
|
?.map((item) => item.doctor_user_id)
|
||||||
@@ -69,7 +72,7 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
|
|||||||
? await supabase
|
? await supabase
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('accounts')
|
.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)
|
.in('primary_owner_user_id', doctorUserIds)
|
||||||
: { data: [] };
|
: { data: [] };
|
||||||
|
|
||||||
@@ -82,21 +85,26 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
|
|||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
const firstSampleGivenAt = responseElements.length
|
const firstSampleGivenAt = responseElements.length
|
||||||
? responseElements.reduce((earliest, current) =>
|
? responseElements.reduce((earliest, current) => {
|
||||||
new Date(current.response_time) < new Date(earliest.response_time)
|
if (current.response_time && earliest.response_time) {
|
||||||
? current
|
if (
|
||||||
: earliest,
|
new Date(current.response_time) < new Date(earliest.response_time)
|
||||||
)?.response_time
|
) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
return earliest;
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}).response_time
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const medusaOrder = medusaOrderItems?.find(
|
const medusaOrder = medusaOrders?.find(
|
||||||
({ order_id }) =>
|
({ id }) => id === analysisResponse.analysis_order_id.medusa_order_id,
|
||||||
order_id === analysisResponse.analysis_order_id.medusa_order_id,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const patientAccount = allAccounts?.find(
|
const patientAccount = allAccounts?.find(
|
||||||
({ primary_owner_user_id }) =>
|
({ primary_owner_user_id, slug }) =>
|
||||||
analysisResponse.user_id === primary_owner_user_id,
|
analysisResponse.user_id === primary_owner_user_id && !slug,
|
||||||
);
|
);
|
||||||
|
|
||||||
const feedback = doctorFeedbackItems?.find(
|
const feedback = doctorFeedbackItems?.find(
|
||||||
@@ -110,9 +118,10 @@ async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const order = {
|
const order = {
|
||||||
title: medusaOrder?.item_id.product_title,
|
title: medusaOrder?.items?.[0]?.product_title,
|
||||||
isPackage:
|
isPackage:
|
||||||
medusaOrder?.item_id.product_type?.toLowerCase() === 'analysis package',
|
medusaOrder?.items?.[0]?.product_type?.toLowerCase() ===
|
||||||
|
'analysis package',
|
||||||
analysisOrderId: analysisResponse.analysis_order_id.id,
|
analysisOrderId: analysisResponse.analysis_order_id.id,
|
||||||
status: analysisResponse.order_status,
|
status: analysisResponse.order_status,
|
||||||
};
|
};
|
||||||
@@ -177,6 +186,7 @@ export async function getUserInProgressResponses({
|
|||||||
`,
|
`,
|
||||||
{ count: 'exact' },
|
{ count: 'exact' },
|
||||||
)
|
)
|
||||||
|
.neq('status', 'ON_HOLD')
|
||||||
.in('analysis_order_id', analysisOrderIds)
|
.in('analysis_order_id', analysisOrderIds)
|
||||||
.range(offset, offset + pageSize - 1)
|
.range(offset, offset + pageSize - 1)
|
||||||
.order('created_at', { ascending: false });
|
.order('created_at', { ascending: false });
|
||||||
@@ -365,47 +375,50 @@ export async function getOtherResponses({
|
|||||||
export async function getAnalysisResultsForDoctor(
|
export async function getAnalysisResultsForDoctor(
|
||||||
analysisResponseId: number,
|
analysisResponseId: number,
|
||||||
): Promise<AnalysisResultDetails> {
|
): Promise<AnalysisResultDetails> {
|
||||||
|
const logger = await getLogger();
|
||||||
|
const ctx = {
|
||||||
|
action: 'get-analysis-results-for-doctor',
|
||||||
|
analysisResponseId,
|
||||||
|
};
|
||||||
const supabase = getSupabaseServerClient();
|
const supabase = getSupabaseServerClient();
|
||||||
|
|
||||||
const { data: analysisResponseElements, error } = await supabase
|
const { data: analysisResponsesData, error: analysisResponsesError } =
|
||||||
.schema('medreport')
|
await supabase
|
||||||
.from(`analysis_response_elements`)
|
.schema('medreport')
|
||||||
.select(
|
.from(`analysis_response_elements`)
|
||||||
`*,
|
.select(
|
||||||
analysis_responses(user_id, analysis_order_id(id,medusa_order_id, analysis_element_ids))`,
|
`*,
|
||||||
)
|
analysis_responses(user_id, analysis_order:analysis_order_id(id,medusa_order_id, analysis_element_ids))`,
|
||||||
.eq('analysis_response_id', analysisResponseId);
|
)
|
||||||
|
.eq('analysis_response_id', analysisResponseId);
|
||||||
|
|
||||||
if (error) {
|
if (analysisResponsesError) {
|
||||||
throw new Error('Something went wrong.');
|
logger.error(
|
||||||
|
{ ...ctx, analysisResponsesError },
|
||||||
|
'No order response for this analysis response id',
|
||||||
|
);
|
||||||
|
throw new Error('No order for this analysis id');
|
||||||
}
|
}
|
||||||
|
const firstAnalysisResponse = analysisResponsesData?.[0];
|
||||||
const firstAnalysisResponse = analysisResponseElements?.[0];
|
|
||||||
const userId = firstAnalysisResponse?.analysis_responses.user_id;
|
const userId = firstAnalysisResponse?.analysis_responses.user_id;
|
||||||
const medusaOrderId =
|
const medusaOrderId =
|
||||||
firstAnalysisResponse?.analysis_responses?.analysis_order_id
|
firstAnalysisResponse?.analysis_responses?.analysis_order?.medusa_order_id;
|
||||||
?.medusa_order_id;
|
|
||||||
|
|
||||||
if (!analysisResponseElements?.length || !userId || !medusaOrderId) {
|
if (!analysisResponsesData?.length || !userId || !medusaOrderId) {
|
||||||
throw new Error('Failed to retrieve full analysis data.');
|
throw new Error('Failed to retrieve full analysis data.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseElementAnalysisElementOriginalIds =
|
const responseElementAnalysisElementOriginalIds = analysisResponsesData.map(
|
||||||
analysisResponseElements.map(
|
({ analysis_element_original_id }) => analysis_element_original_id,
|
||||||
({ analysis_element_original_id }) => analysis_element_original_id,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
{ data: medusaOrderItems, error: medusaOrderError },
|
medusaOrder,
|
||||||
{ data: accountWithParams, error: accountError },
|
{ data: accountWithParams, error: accountError },
|
||||||
{ data: doctorFeedback, error: feedbackError },
|
{ data: doctorFeedback, error: feedbackError },
|
||||||
{ data: previousAnalyses, error: previousAnalysesError },
|
{ data: previousAnalyses, error: previousAnalysesError },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
supabase
|
retrieveOrder(medusaOrderId, true, '*items'),
|
||||||
.schema('public')
|
|
||||||
.from('order_item')
|
|
||||||
.select(`order_id, item_id(product_title, product_type)`)
|
|
||||||
.eq('order_id', medusaOrderId),
|
|
||||||
supabase
|
supabase
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
@@ -422,7 +435,7 @@ export async function getAnalysisResultsForDoctor(
|
|||||||
.select(`*`)
|
.select(`*`)
|
||||||
.eq(
|
.eq(
|
||||||
'analysis_order_id',
|
'analysis_order_id',
|
||||||
firstAnalysisResponse.analysis_responses.analysis_order_id.id,
|
firstAnalysisResponse.analysis_responses.analysis_order.id,
|
||||||
)
|
)
|
||||||
.limit(1),
|
.limit(1),
|
||||||
supabase
|
supabase
|
||||||
@@ -452,12 +465,7 @@ export async function getAnalysisResultsForDoctor(
|
|||||||
.order('response_time'),
|
.order('response_time'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (
|
if (!medusaOrder || accountError || feedbackError || previousAnalysesError) {
|
||||||
medusaOrderError ||
|
|
||||||
accountError ||
|
|
||||||
feedbackError ||
|
|
||||||
previousAnalysesError
|
|
||||||
) {
|
|
||||||
throw new Error('Something went wrong.');
|
throw new Error('Something went wrong.');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,15 +486,20 @@ export async function getAnalysisResultsForDoctor(
|
|||||||
} = accountWithParams[0];
|
} = accountWithParams[0];
|
||||||
|
|
||||||
const analysisResponseElementsWithPreviousData = [];
|
const analysisResponseElementsWithPreviousData = [];
|
||||||
for (const analysisResponseElement of analysisResponseElements) {
|
for (const analysisResponseElement of analysisResponsesData) {
|
||||||
const latestPreviousAnalysis = previousAnalyses.find(
|
const latestPreviousAnalysis = previousAnalyses.find(
|
||||||
({ analysis_element_original_id, response_time }) =>
|
({ analysis_element_original_id, response_time }) => {
|
||||||
analysis_element_original_id ===
|
if (response_time && analysisResponseElement.response_time) {
|
||||||
analysisResponseElement.analysis_element_original_id &&
|
return (
|
||||||
isBefore(
|
analysis_element_original_id ===
|
||||||
new Date(response_time),
|
analysisResponseElement.analysis_element_original_id &&
|
||||||
new Date(analysisResponseElement.response_time),
|
isBefore(
|
||||||
),
|
new Date(response_time),
|
||||||
|
new Date(analysisResponseElement.response_time),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
analysisResponseElementsWithPreviousData.push({
|
analysisResponseElementsWithPreviousData.push({
|
||||||
...analysisResponseElement,
|
...analysisResponseElement,
|
||||||
@@ -497,12 +510,12 @@ export async function getAnalysisResultsForDoctor(
|
|||||||
return {
|
return {
|
||||||
analysisResponse: analysisResponseElementsWithPreviousData,
|
analysisResponse: analysisResponseElementsWithPreviousData,
|
||||||
order: {
|
order: {
|
||||||
title: medusaOrderItems?.[0]?.item_id.product_title ?? '-',
|
title: medusaOrder.items?.[0]?.product_title ?? '-',
|
||||||
isPackage:
|
isPackage:
|
||||||
medusaOrderItems?.[0]?.item_id.product_type?.toLowerCase() ===
|
medusaOrder.items?.[0]?.product_type?.toLowerCase() ===
|
||||||
'analysis package',
|
'analysis package',
|
||||||
analysisOrderId:
|
analysisOrderId:
|
||||||
firstAnalysisResponse.analysis_responses.analysis_order_id.id,
|
firstAnalysisResponse.analysis_responses.analysis_order.id,
|
||||||
},
|
},
|
||||||
doctorFeedback: doctorFeedback?.[0],
|
doctorFeedback: doctorFeedback?.[0],
|
||||||
patient: {
|
patient: {
|
||||||
@@ -525,8 +538,15 @@ export async function selectJob(analysisOrderId: number, userId: string) {
|
|||||||
const {
|
const {
|
||||||
data: { user },
|
data: { user },
|
||||||
} = await supabase.auth.getUser();
|
} = await supabase.auth.getUser();
|
||||||
|
const logger = await getLogger();
|
||||||
|
const ctx = {
|
||||||
|
action: 'select-job',
|
||||||
|
patientUserId: userId,
|
||||||
|
currentUserId: user?.id,
|
||||||
|
};
|
||||||
|
|
||||||
if (!user?.id) {
|
if (!user?.id) {
|
||||||
|
logger.error(ctx, 'No user logged in');
|
||||||
throw new Error('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;
|
const jobAssignedToUserId = existingFeedback?.[0]?.doctor_user_id;
|
||||||
|
|
||||||
if (!!jobAssignedToUserId && jobAssignedToUserId !== user.id) {
|
if (!!jobAssignedToUserId && jobAssignedToUserId !== user.id) {
|
||||||
|
logger.error(ctx, 'Job assigned to a different user');
|
||||||
throw new Error(ErrorReason.JOB_ASSIGNED);
|
throw new Error(ErrorReason.JOB_ASSIGNED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,6 +578,10 @@ export async function selectJob(analysisOrderId: number, userId: string) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (error || existingFeedbackError) {
|
if (error || existingFeedbackError) {
|
||||||
|
logger.error(
|
||||||
|
{ ...ctx, error, existingFeedbackError },
|
||||||
|
'Failed updating doctor feedback',
|
||||||
|
);
|
||||||
throw new Error('Something went wrong');
|
throw new Error('Something went wrong');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@kit/tsconfig/base.json",
|
"extends": "../../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import { getRegion } from './regions';
|
|||||||
* @param cartId - optional - The ID of the cart to retrieve.
|
* @param cartId - optional - The ID of the cart to retrieve.
|
||||||
* @returns The cart object if found, or null if not found.
|
* @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());
|
const id = cartId || (await getCartId());
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@@ -135,13 +137,21 @@ export async function addToCart({
|
|||||||
quantity: number;
|
quantity: number;
|
||||||
countryCode: string;
|
countryCode: string;
|
||||||
}) {
|
}) {
|
||||||
|
const logger = await getLogger();
|
||||||
|
const ctx = {
|
||||||
|
variantId,
|
||||||
|
quantity,
|
||||||
|
countryCode,
|
||||||
|
};
|
||||||
if (!variantId) {
|
if (!variantId) {
|
||||||
|
logger.error(ctx, 'Missing variant ID when adding to cart');
|
||||||
throw new Error('Missing variant ID when adding to cart');
|
throw new Error('Missing variant ID when adding to cart');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cart = await getOrSetCart(countryCode);
|
const cart = await getOrSetCart(countryCode);
|
||||||
|
|
||||||
if (!cart) {
|
if (!cart) {
|
||||||
|
logger.error(ctx, 'Error retrieving or creating cart');
|
||||||
throw new Error('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';
|
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 = {
|
const headers = {
|
||||||
...(await getAuthHeaders()),
|
...(await getAuthHeaders()),
|
||||||
};
|
};
|
||||||
@@ -19,8 +23,7 @@ export const retrieveOrder = async (id: string, allowCache = true) => {
|
|||||||
.fetch<HttpTypes.StoreOrderResponse>(`/store/orders/${id}`, {
|
.fetch<HttpTypes.StoreOrderResponse>(`/store/orders/${id}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
query: {
|
query: {
|
||||||
fields:
|
fields,
|
||||||
'*payment_collections.payments,*items,*items.metadata,*items.variant,*items.product',
|
|
||||||
},
|
},
|
||||||
headers,
|
headers,
|
||||||
next,
|
next,
|
||||||
@@ -59,7 +62,18 @@ export const listOrders = async (
|
|||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
})
|
})
|
||||||
.then(({ orders }) => orders)
|
.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 (
|
export const createTransferRequest = async (
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function useNotificationsStream(params: {
|
|||||||
'postgres_changes',
|
'postgres_changes',
|
||||||
{
|
{
|
||||||
event: 'INSERT',
|
event: 'INSERT',
|
||||||
schema: 'public',
|
schema: 'medreport',
|
||||||
filter: `account_id=in.(${params.accountIds.join(', ')})`,
|
filter: `account_id=in.(${params.accountIds.join(', ')})`,
|
||||||
table: 'notifications',
|
table: 'notifications',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -50,4 +50,13 @@ class NotificationsApi {
|
|||||||
createNotification(params: Notification['Insert']) {
|
createNotification(params: Notification['Insert']) {
|
||||||
return this.service.createNotification(params);
|
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;
|
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>,
|
Component: LayoutOrPageComponent<Params>,
|
||||||
) {
|
) {
|
||||||
return async function AdminGuardServerComponentWrapper(params: Params) {
|
return async function AdminGuardServerComponentWrapper(params: Params) {
|
||||||
//@ts-ignore
|
// @ts-expect-error incorrectly typed params
|
||||||
const { account } = await params.params;
|
const { account } = await params.params;
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
const [isUserSuperAdmin, isUserCompanyAdmin] = await Promise.all([
|
const [isUserSuperAdmin, isUserCompanyAdmin] = await Promise.all([
|
||||||
|
|||||||
@@ -25,15 +25,17 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
import { RemoveMemberDialog } from './remove-member-dialog';
|
import { RemoveMemberDialog } from './remove-member-dialog';
|
||||||
import { RoleBadge } from './role-badge';
|
import { RoleBadge } from './role-badge';
|
||||||
import { TransferOwnershipDialog } from './transfer-ownership-dialog';
|
import { TransferOwnershipDialog } from './transfer-ownership-dialog';
|
||||||
|
import UpdateEmployeeBenefitDialog from './update-employee-benefit-dialog';
|
||||||
import { UpdateMemberRoleDialog } from './update-member-role-dialog';
|
import { UpdateMemberRoleDialog } from './update-member-role-dialog';
|
||||||
|
|
||||||
type Members =
|
type Members =
|
||||||
Database['medreport']['Functions']['get_account_members']['Returns'];
|
Database['medreport']['Functions']['get_account_members']['Returns'];
|
||||||
|
|
||||||
interface Permissions {
|
interface Permissions {
|
||||||
canUpdateRole: (roleHierarchy: number) => boolean;
|
canUpdateRole: boolean;
|
||||||
canRemoveFromAccount: (roleHierarchy: number) => boolean;
|
canRemoveFromAccount: (roleHierarchy: number) => boolean;
|
||||||
canTransferOwnership: boolean;
|
canTransferOwnership: boolean;
|
||||||
|
canUpdateBenefit: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AccountMembersTableProps = {
|
type AccountMembersTableProps = {
|
||||||
@@ -43,6 +45,7 @@ type AccountMembersTableProps = {
|
|||||||
userRoleHierarchy: number;
|
userRoleHierarchy: number;
|
||||||
isPrimaryOwner: boolean;
|
isPrimaryOwner: boolean;
|
||||||
canManageRoles: boolean;
|
canManageRoles: boolean;
|
||||||
|
canUpdateBenefit: boolean;
|
||||||
membersBenefitsUsage: {
|
membersBenefitsUsage: {
|
||||||
personal_account_id: string;
|
personal_account_id: string;
|
||||||
benefit_amount: number;
|
benefit_amount: number;
|
||||||
@@ -57,23 +60,21 @@ export function AccountMembersTable({
|
|||||||
isPrimaryOwner,
|
isPrimaryOwner,
|
||||||
userRoleHierarchy,
|
userRoleHierarchy,
|
||||||
canManageRoles,
|
canManageRoles,
|
||||||
|
canUpdateBenefit,
|
||||||
membersBenefitsUsage,
|
membersBenefitsUsage,
|
||||||
}: AccountMembersTableProps) {
|
}: AccountMembersTableProps) {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const { t } = useTranslation('teams');
|
const { t } = useTranslation('teams');
|
||||||
|
|
||||||
const permissions = {
|
const permissions = {
|
||||||
canUpdateRole: (targetRole: number) => {
|
canUpdateRole: canManageRoles,
|
||||||
return (
|
|
||||||
isPrimaryOwner || (canManageRoles && userRoleHierarchy < targetRole)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
canRemoveFromAccount: (targetRole: number) => {
|
canRemoveFromAccount: (targetRole: number) => {
|
||||||
return (
|
return (
|
||||||
isPrimaryOwner || (canManageRoles && userRoleHierarchy < targetRole)
|
isPrimaryOwner || (canManageRoles && userRoleHierarchy < targetRole)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
canTransferOwnership: isPrimaryOwner,
|
canTransferOwnership: isPrimaryOwner,
|
||||||
|
canUpdateBenefit,
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = useGetColumns(permissions, {
|
const columns = useGetColumns(permissions, {
|
||||||
@@ -211,8 +212,7 @@ function useGetColumns(
|
|||||||
{
|
{
|
||||||
header: t('roleLabel'),
|
header: t('roleLabel'),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const { role, primary_owner_user_id, user_id } = row.original;
|
const { role } = row.original;
|
||||||
const isPrimaryOwner = primary_owner_user_id === user_id;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@@ -221,16 +221,6 @@ function useGetColumns(
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<RoleBadge role={role} />
|
<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>
|
</span>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -248,7 +238,6 @@ function useGetColumns(
|
|||||||
<ActionsDropdown
|
<ActionsDropdown
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
member={row.original}
|
member={row.original}
|
||||||
currentUserId={params.currentUserId}
|
|
||||||
currentTeamAccountId={params.currentAccountId}
|
currentTeamAccountId={params.currentAccountId}
|
||||||
currentRoleHierarchy={params.currentRoleHierarchy}
|
currentRoleHierarchy={params.currentRoleHierarchy}
|
||||||
/>
|
/>
|
||||||
@@ -262,29 +251,22 @@ function useGetColumns(
|
|||||||
function ActionsDropdown({
|
function ActionsDropdown({
|
||||||
permissions,
|
permissions,
|
||||||
member,
|
member,
|
||||||
currentUserId,
|
|
||||||
currentTeamAccountId,
|
currentTeamAccountId,
|
||||||
currentRoleHierarchy,
|
currentRoleHierarchy,
|
||||||
}: {
|
}: {
|
||||||
permissions: Permissions;
|
permissions: Permissions;
|
||||||
member: Members[0];
|
member: Members[0];
|
||||||
currentUserId: string;
|
|
||||||
currentTeamAccountId: string;
|
currentTeamAccountId: string;
|
||||||
currentRoleHierarchy: number;
|
currentRoleHierarchy: number;
|
||||||
}) {
|
}) {
|
||||||
const [isRemoving, setIsRemoving] = useState(false);
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
const [isTransferring, setIsTransferring] = useState(false);
|
const [isTransferring, setIsTransferring] = useState(false);
|
||||||
const [isUpdatingRole, setIsUpdatingRole] = 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;
|
const isPrimaryOwner = member.primary_owner_user_id === member.user_id;
|
||||||
|
|
||||||
if (isCurrentUser || isPrimaryOwner) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberRoleHierarchy = member.role_hierarchy_level;
|
const memberRoleHierarchy = member.role_hierarchy_level;
|
||||||
const canUpdateRole = permissions.canUpdateRole(memberRoleHierarchy);
|
|
||||||
|
|
||||||
const canRemoveFromAccount =
|
const canRemoveFromAccount =
|
||||||
permissions.canRemoveFromAccount(memberRoleHierarchy);
|
permissions.canRemoveFromAccount(memberRoleHierarchy);
|
||||||
@@ -292,9 +274,10 @@ function ActionsDropdown({
|
|||||||
// if has no permission to update role, transfer ownership or remove from account
|
// if has no permission to update role, transfer ownership or remove from account
|
||||||
// do not render the dropdown menu
|
// do not render the dropdown menu
|
||||||
if (
|
if (
|
||||||
!canUpdateRole &&
|
!permissions.canUpdateRole &&
|
||||||
!permissions.canTransferOwnership &&
|
!permissions.canTransferOwnership &&
|
||||||
!canRemoveFromAccount
|
!canRemoveFromAccount &&
|
||||||
|
!permissions.canUpdateBenefit
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -309,23 +292,29 @@ function ActionsDropdown({
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<If condition={canUpdateRole}>
|
<If condition={permissions.canUpdateRole}>
|
||||||
<DropdownMenuItem onClick={() => setIsUpdatingRole(true)}>
|
<DropdownMenuItem onClick={() => setIsUpdatingRole(true)}>
|
||||||
<Trans i18nKey={'teams:updateRole'} />
|
<Trans i18nKey={'teams:updateRole'} />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={permissions.canTransferOwnership}>
|
<If condition={permissions.canTransferOwnership && !isPrimaryOwner}>
|
||||||
<DropdownMenuItem onClick={() => setIsTransferring(true)}>
|
<DropdownMenuItem onClick={() => setIsTransferring(true)}>
|
||||||
<Trans i18nKey={'teams:transferOwnership'} />
|
<Trans i18nKey={'teams:transferOwnership'} />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={canRemoveFromAccount}>
|
<If condition={canRemoveFromAccount && !isPrimaryOwner}>
|
||||||
<DropdownMenuItem onClick={() => setIsRemoving(true)}>
|
<DropdownMenuItem onClick={() => setIsRemoving(true)}>
|
||||||
<Trans i18nKey={'teams:removeMember'} />
|
<Trans i18nKey={'teams:removeMember'} />
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
|
<If condition={permissions.canUpdateBenefit}>
|
||||||
|
<DropdownMenuItem onClick={() => setIsUpdatingBenefit(true)}>
|
||||||
|
<Trans i18nKey={'teams:updateBenefit'} />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</If>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
@@ -358,6 +347,16 @@ function ActionsDropdown({
|
|||||||
userId={member.user_id}
|
userId={member.user_id}
|
||||||
/>
|
/>
|
||||||
</If>
|
</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 { InviteMembersSchema } from '../../schema/invite-members.schema';
|
||||||
import { createInvitationsAction } from '../../server/actions/team-invitations-server-actions';
|
import { createInvitationsAction } from '../../server/actions/team-invitations-server-actions';
|
||||||
import { MembershipRoleSelector } from './membership-role-selector';
|
|
||||||
import { RolesDataProvider } from './roles-data-provider';
|
import { RolesDataProvider } from './roles-data-provider';
|
||||||
|
|
||||||
type InviteModel = ReturnType<typeof createEmptyInviteModel>;
|
type InviteModel = ReturnType<typeof createEmptyInviteModel>;
|
||||||
|
|
||||||
type Role = string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The maximum number of invites that can be sent at once.
|
* The maximum number of invites that can be sent at once.
|
||||||
* Useful to avoid spamming the server with too large payloads
|
* Useful to avoid spamming the server with too large payloads
|
||||||
@@ -66,10 +63,7 @@ export function InviteMembersDialogContainer({
|
|||||||
<Dialog open={isOpen} onOpenChange={setIsOpen} modal>
|
<Dialog open={isOpen} onOpenChange={setIsOpen} modal>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent
|
<DialogContent onInteractOutside={(e) => e.preventDefault()}>
|
||||||
className="max-w-[800px]"
|
|
||||||
onInteractOutside={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans i18nKey={'teams:inviteMembersHeading'} />
|
<Trans i18nKey={'teams:inviteMembersHeading'} />
|
||||||
@@ -81,10 +75,9 @@ export function InviteMembersDialogContainer({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<RolesDataProvider maxRoleHierarchy={userRoleHierarchy}>
|
<RolesDataProvider maxRoleHierarchy={userRoleHierarchy}>
|
||||||
{(roles) => (
|
{() => (
|
||||||
<InviteMembersForm
|
<InviteMembersForm
|
||||||
pending={pending}
|
pending={pending}
|
||||||
roles={roles}
|
|
||||||
onSubmit={(data) => {
|
onSubmit={(data) => {
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
const promise = createInvitationsAction({
|
const promise = createInvitationsAction({
|
||||||
@@ -111,12 +104,10 @@ export function InviteMembersDialogContainer({
|
|||||||
|
|
||||||
function InviteMembersForm({
|
function InviteMembersForm({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
roles,
|
|
||||||
pending,
|
pending,
|
||||||
}: {
|
}: {
|
||||||
onSubmit: (data: { invitations: InviteModel[] }) => void;
|
onSubmit: (data: { invitations: InviteModel[] }) => void;
|
||||||
pending: boolean;
|
pending: boolean;
|
||||||
roles: string[];
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('teams');
|
const { t } = useTranslation('teams');
|
||||||
|
|
||||||
@@ -148,12 +139,11 @@ function InviteMembersForm({
|
|||||||
const personalCodeInputName =
|
const personalCodeInputName =
|
||||||
`invitations.${index}.personal_code` as const;
|
`invitations.${index}.personal_code` as const;
|
||||||
const emailInputName = `invitations.${index}.email` as const;
|
const emailInputName = `invitations.${index}.email` as const;
|
||||||
const roleInputName = `invitations.${index}.role` as const;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-test={'invite-member-form-item'} key={field.id}>
|
<div data-test="invite-member-form-item" key={field.id}>
|
||||||
<div className={'flex items-end gap-x-1 md:space-x-2'}>
|
<div className="flex items-end gap-x-1 md:space-x-2">
|
||||||
<div className={'w-4/12'}>
|
<div className="w-5/12">
|
||||||
<FormField
|
<FormField
|
||||||
name={personalCodeInputName}
|
name={personalCodeInputName}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
@@ -178,7 +168,7 @@ function InviteMembersForm({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={'w-4/12'}>
|
<div className={'w-5/12'}>
|
||||||
<FormField
|
<FormField
|
||||||
name={emailInputName}
|
name={emailInputName}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
@@ -205,37 +195,7 @@ function InviteMembersForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'w-4/12'}>
|
<div className={'flex w-1/12 items-end justify-end'}>
|
||||||
<FormField
|
|
||||||
name={roleInputName}
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<If condition={isFirst}>
|
|
||||||
<FormLabel>
|
|
||||||
<Trans i18nKey={'teams:roleLabel'} />
|
|
||||||
</FormLabel>
|
|
||||||
</If>
|
|
||||||
|
|
||||||
<FormControl>
|
|
||||||
<MembershipRoleSelector
|
|
||||||
triggerClassName={'m-0'}
|
|
||||||
roles={roles}
|
|
||||||
value={field.value}
|
|
||||||
onChange={(role) => {
|
|
||||||
form.setValue(field.name, role);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'flex w-[40px] items-end justify-end'}>
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -303,5 +263,5 @@ function InviteMembersForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createEmptyInviteModel() {
|
function createEmptyInviteModel() {
|
||||||
return { email: '', role: 'member' as Role, personal_code: '' };
|
return { email: '', personal_code: '' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,19 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
type Role = string;
|
type Role = string;
|
||||||
|
|
||||||
const roles = {
|
const roles = {
|
||||||
owner: '',
|
owner: 'bg-yellow-400 text-black',
|
||||||
member:
|
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', {
|
const roleClassNameBuilder = cva(
|
||||||
variants: {
|
'px-2.5 py-1 font-medium capitalize shadow-none',
|
||||||
role: roles,
|
{
|
||||||
|
variants: {
|
||||||
|
role: roles,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
export function RoleBadge({ role }: { role: Role }) {
|
export function RoleBadge({ role }: { role: Role }) {
|
||||||
// @ts-expect-error: hard to type this since users can add custom roles
|
// @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({
|
const InviteSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
role: z.string().min(1).max(100),
|
|
||||||
personal_code: z
|
personal_code: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^[1-6]\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}\d$/, {
|
.regex(/^[1-6]\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}\d$/, {
|
||||||
|
|||||||
@@ -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 { z } from 'zod';
|
||||||
|
|
||||||
import { AccountBalanceService } from '@kit/accounts/services/account-balance.service';
|
|
||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
import { createNotificationsApi } from '@kit/notifications/api';
|
import { createNotificationsApi } from '@kit/notifications/api';
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
@@ -149,7 +148,6 @@ export const updateInvitationAction = enhanceAction(
|
|||||||
export const acceptInvitationAction = enhanceAction(
|
export const acceptInvitationAction = enhanceAction(
|
||||||
async (data: FormData, user) => {
|
async (data: FormData, user) => {
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
const accountBalanceService = new AccountBalanceService();
|
|
||||||
|
|
||||||
const { inviteToken, nextPath } = AcceptInvitationSchema.parse(
|
const { inviteToken, nextPath } = AcceptInvitationSchema.parse(
|
||||||
Object.fromEntries(data),
|
Object.fromEntries(data),
|
||||||
@@ -173,9 +171,6 @@ export const acceptInvitationAction = enhanceAction(
|
|||||||
throw new Error('Failed to accept invitation');
|
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
|
// Increase the seats for the account
|
||||||
await perSeatBillingService.increaseSeats(accountId);
|
await perSeatBillingService.increaseSeats(accountId);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { revalidatePath } from 'next/cache';
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
|
import { AccountBalanceService } from '@kit/accounts/services/account-balance.service';
|
||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
import { createOtpApi } from '@kit/otp';
|
import { createOtpApi } from '@kit/otp';
|
||||||
import { getLogger } from '@kit/shared/logger';
|
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 { RemoveMemberSchema } from '../../schema/remove-member.schema';
|
||||||
import { TransferOwnershipConfirmationSchema } from '../../schema/transfer-ownership-confirmation.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 { UpdateMemberRoleSchema } from '../../schema/update-member-role.schema';
|
||||||
import { createAccountMembersService } from '../services/account-members.service';
|
import { createAccountMembersService } from '../services/account-members.service';
|
||||||
|
|
||||||
@@ -144,3 +146,64 @@ export const transferOwnershipAction = enhanceAction(
|
|||||||
schema: TransferOwnershipConfirmationSchema,
|
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
|
const response = await this.client
|
||||||
.schema('medreport')
|
.schema('medreport')
|
||||||
.rpc('add_invitations_to_account', {
|
.rpc('add_invitations_to_account', {
|
||||||
invitations,
|
invitations: invitations.map((invitation) => ({
|
||||||
|
...invitation,
|
||||||
|
role: 'member',
|
||||||
|
})),
|
||||||
account_slug: accountSlug,
|
account_slug: accountSlug,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
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 type { UuringuVastus } from '@kit/shared/types/medipost-analysis';
|
||||||
import { toArray } from '@kit/shared/utils';
|
import { toArray } from '@kit/shared/utils';
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
AnalysisOrder,
|
AnalysisOrder,
|
||||||
AnalysisOrderStatus,
|
AnalysisOrderStatus,
|
||||||
@@ -463,13 +468,19 @@ class UserAnalysesApi {
|
|||||||
medusaOrderId?: string;
|
medusaOrderId?: string;
|
||||||
orderStatus: AnalysisOrderStatus;
|
orderStatus: AnalysisOrderStatus;
|
||||||
}) {
|
}) {
|
||||||
|
const logger = await getLogger();
|
||||||
const orderIdParam = orderId;
|
const orderIdParam = orderId;
|
||||||
const medusaOrderIdParam = medusaOrderId;
|
const medusaOrderIdParam = medusaOrderId;
|
||||||
|
const ctx = {
|
||||||
|
action: 'update-analysis-order-status',
|
||||||
|
orderId,
|
||||||
|
medusaOrderId,
|
||||||
|
orderStatus,
|
||||||
|
};
|
||||||
|
|
||||||
console.info(
|
logger.info(ctx, 'Updating order');
|
||||||
`Updating order id=${orderId} medusaOrderId=${medusaOrderId} status=${orderStatus}`,
|
|
||||||
);
|
|
||||||
if (!orderIdParam && !medusaOrderIdParam) {
|
if (!orderIdParam && !medusaOrderIdParam) {
|
||||||
|
logger.error(ctx, 'Missing orderId or medusaOrderId');
|
||||||
throw new Error('Either orderId or medusaOrderId must be provided');
|
throw new Error('Either orderId or medusaOrderId must be provided');
|
||||||
}
|
}
|
||||||
await this.client
|
await this.client
|
||||||
@@ -481,6 +492,39 @@ class UserAnalysesApi {
|
|||||||
})
|
})
|
||||||
.throwOnError();
|
.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>) {
|
export function createUserAnalysesApi(client: SupabaseClient<Database>) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user