Merge branch 'main' into MED-57
This commit is contained in:
@@ -31,7 +31,10 @@
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./components/*": "./src/components/*.tsx"
|
||||
"./services/*": "./src/lib/server/services/*.ts",
|
||||
"./actions/*": "./src/lib/server/actions/*.ts",
|
||||
"./schema/*": "./src/lib/server/schema/*.ts",
|
||||
"./lib/*": "./src/lib/*.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
@@ -40,4 +43,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { isDoctor } from '../lib/server/utils/is-doctor';
|
||||
|
||||
|
||||
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
|
||||
|
||||
/**
|
||||
* DoctorGuard is a server component wrapper that checks if the user is a doctor before rendering the component.
|
||||
* If the user is not a doctor, we redirect to a 404.
|
||||
* @param Component - The Page or Layout component to wrap
|
||||
*/
|
||||
export function DoctorGuard<Params extends object>(
|
||||
Component: LayoutOrPageComponent<Params>,
|
||||
) {
|
||||
return async function DoctorGuardServerComponentWrapper(params: Params) {
|
||||
const client = getSupabaseServerClient();
|
||||
const isUserDoctor = await isDoctor(client);
|
||||
|
||||
// if the user is not a super-admin, we redirect to a 404
|
||||
if (!isUserDoctor) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <Component {...params} />;
|
||||
};
|
||||
}
|
||||
21
packages/features/doctor/src/lib/helpers.ts
Normal file
21
packages/features/doctor/src/lib/helpers.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { formatDate, getPersonParameters } from '@kit/shared/utils';
|
||||
|
||||
export function getResultSetName(
|
||||
title: string,
|
||||
isPackage: boolean,
|
||||
nrOfElements: number,
|
||||
) {
|
||||
return !isPackage && nrOfElements > 1 ? 'doctor:analyses' : title;
|
||||
}
|
||||
|
||||
export function getDOBWithAgeStringFromPersonalCode(
|
||||
personalCode: string | null,
|
||||
) {
|
||||
if (!personalCode) return 'common:unknown';
|
||||
|
||||
const person = getPersonParameters(personalCode);
|
||||
|
||||
if (!person) return 'common:unknown';
|
||||
|
||||
return `${formatDate(person.dob.toString())} (${person.age})`;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
'use server';
|
||||
|
||||
import { revalidatePath, revalidateTag } from 'next/cache';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
import {
|
||||
DoctorAnalysisFeedbackTable,
|
||||
DoctorJobSelect,
|
||||
DoctorJobUnselect,
|
||||
doctorAnalysisFeedbackSchema,
|
||||
doctorJobSelectSchema,
|
||||
doctorJobUnselectSchema,
|
||||
} from '../schema/doctor-analysis.schema';
|
||||
import {
|
||||
selectJob,
|
||||
submitFeedback,
|
||||
unselectJob,
|
||||
} from '../services/doctor-analysis.service';
|
||||
import { doctorAction } from '../utils/doctor-action';
|
||||
|
||||
/**
|
||||
* @name selectJobAction
|
||||
* @description Creates a feedback item that ties doctor to the job and enables giving feedback.
|
||||
*/
|
||||
export const selectJobAction = doctorAction(
|
||||
enhanceAction(
|
||||
async ({ analysisOrderId, userId }: DoctorJobSelect) => {
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ analysisOrderId }, `Selecting new job`);
|
||||
|
||||
await selectJob(analysisOrderId, userId);
|
||||
|
||||
logger.info({ analysisOrderId }, `Successfully selected`);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
{
|
||||
schema: doctorJobSelectSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @name unselectJobAction
|
||||
* @description Removes the current user from a feedback item.
|
||||
*/
|
||||
export const unselectJobAction = doctorAction(
|
||||
enhanceAction(
|
||||
async ({ analysisOrderId }: DoctorJobUnselect) => {
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info({ analysisOrderId }, `Removing doctor from job`);
|
||||
|
||||
await unselectJob(analysisOrderId);
|
||||
|
||||
logger.info(
|
||||
{ analysisOrderId },
|
||||
`Successfully removed current doctor from job`,
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
{
|
||||
schema: doctorJobUnselectSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @name giveFeedbackAction
|
||||
* @description Creates or updates doctor analysis feedback.
|
||||
*/
|
||||
export const giveFeedbackAction = doctorAction(
|
||||
enhanceAction(
|
||||
async ({
|
||||
feedbackValue,
|
||||
userId,
|
||||
analysisOrderId,
|
||||
status,
|
||||
}: {
|
||||
feedbackValue: string;
|
||||
userId: DoctorAnalysisFeedbackTable['user_id'];
|
||||
analysisOrderId: DoctorAnalysisFeedbackTable['analysis_order_id'];
|
||||
status: DoctorAnalysisFeedbackTable['status'];
|
||||
}) => {
|
||||
const logger = await getLogger();
|
||||
|
||||
logger.info(
|
||||
{ analysisOrderId },
|
||||
`Submitting feedback for analysis order...`,
|
||||
);
|
||||
|
||||
const result = await submitFeedback(
|
||||
analysisOrderId,
|
||||
userId,
|
||||
feedbackValue,
|
||||
status,
|
||||
);
|
||||
logger.info({ analysisOrderId }, `Successfully submitted feedback`);
|
||||
|
||||
return result;
|
||||
},
|
||||
{
|
||||
schema: doctorAnalysisFeedbackSchema,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,69 @@
|
||||
'use server';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
import {
|
||||
getOpenResponses,
|
||||
getOtherResponses,
|
||||
getUserDoneResponses,
|
||||
getUserInProgressResponses,
|
||||
} from '@kit/doctor/services/doctor-analysis.service';
|
||||
import { doctorAction } from '../utils/doctor-action';
|
||||
|
||||
export const getUserDoneResponsesAction = doctorAction(
|
||||
async ({ page, pageSize }: { page: number; pageSize: number }) => {
|
||||
const logger = await getLogger();
|
||||
try {
|
||||
const data = await getUserDoneResponses({ page, pageSize });
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
logger.error(`Error fetching data for user completed jobs`, error);
|
||||
return { success: false, error: 'Failed to fetch data from the server.' };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const getUserInProgressResponsesAction = doctorAction(
|
||||
async ({ page, pageSize }: { page: number; pageSize: number }) => {
|
||||
const logger = await getLogger();
|
||||
try {
|
||||
const data = await getUserInProgressResponses({ page, pageSize });
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
logger.error(`Error fetching data for user's in progress jobs`, error);
|
||||
return { success: false, error: 'Failed to fetch data from the server.' };
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const getOtherResponsesAction = doctorAction(
|
||||
async ({ page, pageSize }: { page: number; pageSize: number }) => {
|
||||
const logger = await getLogger();
|
||||
try {
|
||||
const data = await getOtherResponses({ page, pageSize });
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error fetching data for other analysis response jobs`,
|
||||
error,
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
error: 'Failed to fetch data from the server.',
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
export const getOpenResponsesAction = doctorAction(
|
||||
async ({ page, pageSize }: { page: number; pageSize: number }) => {
|
||||
const logger = await getLogger();
|
||||
try {
|
||||
const data = await getOpenResponses({ page, pageSize });
|
||||
return { success: true, data };
|
||||
} catch (error) {
|
||||
logger.error(`Error fetching open analysis response jobs`, error);
|
||||
return { success: false, error: 'Failed to fetch data from the server.' };
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,78 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const AnalysisOrderIdSchema = z.object({
|
||||
id: z.number(),
|
||||
medusa_order_id: z.string(),
|
||||
analysis_element_ids: z.array(z.number()).nullable(),
|
||||
});
|
||||
export type AnalysisOrderId = z.infer<typeof AnalysisOrderIdSchema>;
|
||||
|
||||
export const DoctorFeedbackSchema = z
|
||||
.object({
|
||||
id: z.number(),
|
||||
analysis_order_id: z.number(),
|
||||
doctor_user_id: z.string().nullable(),
|
||||
user_id: z.string(),
|
||||
value: z.string().nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string().nullable(),
|
||||
created_by: z.string(),
|
||||
updated_by: z.string().nullable(),
|
||||
status: z.string(),
|
||||
})
|
||||
.optional()
|
||||
.nullable();
|
||||
export type DoctorFeedback = z.infer<typeof DoctorFeedbackSchema>;
|
||||
|
||||
export const OrderSchema = z.object({
|
||||
title: z.string(),
|
||||
isPackage: z.boolean(),
|
||||
analysisOrderId: z.number(),
|
||||
});
|
||||
export type Order = z.infer<typeof OrderSchema>;
|
||||
|
||||
export const PatientSchema = z.object({
|
||||
userId: z.string(),
|
||||
accountId: z.string(),
|
||||
firstName: z.string(),
|
||||
lastName: z.string().nullable(),
|
||||
personalCode: z.string().nullable(),
|
||||
phone: z.string().nullable(),
|
||||
email: z.string().nullable(),
|
||||
height: z.number().optional().nullable(),
|
||||
weight: z.number().optional().nullable(),
|
||||
});
|
||||
export type Patient = z.infer<typeof PatientSchema>;
|
||||
|
||||
export const AnalysisResponsesSchema = z.object({
|
||||
user_id: z.string(),
|
||||
analysis_order_id: AnalysisOrderIdSchema,
|
||||
});
|
||||
export type AnalysisResponses = z.infer<typeof AnalysisResponsesSchema>;
|
||||
|
||||
export const AnalysisResponseSchema = z.object({
|
||||
id: z.number(),
|
||||
analysis_response_id: z.number(),
|
||||
analysis_element_original_id: z.string(),
|
||||
unit: z.string().nullable(),
|
||||
response_value: z.number(),
|
||||
response_time: z.string(),
|
||||
norm_upper: z.number().nullable(),
|
||||
norm_upper_included: z.boolean().nullable(),
|
||||
norm_lower: z.number().nullable(),
|
||||
norm_lower_included: z.boolean().nullable(),
|
||||
norm_status: z.number().nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string().nullable(),
|
||||
analysis_name: z.string().nullable(),
|
||||
analysis_responses: AnalysisResponsesSchema,
|
||||
});
|
||||
export type AnalysisResponse = z.infer<typeof AnalysisResponseSchema>;
|
||||
|
||||
export const AnalysisResultDetailsSchema = z.object({
|
||||
analysisResponse: z.array(AnalysisResponseSchema),
|
||||
order: OrderSchema,
|
||||
doctorFeedback: DoctorFeedbackSchema,
|
||||
patient: PatientSchema,
|
||||
});
|
||||
export type AnalysisResultDetails = z.infer<typeof AnalysisResultDetailsSchema>;
|
||||
@@ -0,0 +1,135 @@
|
||||
import z from 'zod/v3';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export const doctorJobSelectSchema = z.object({
|
||||
userId: z.string().uuid(),
|
||||
analysisOrderId: z.number(),
|
||||
});
|
||||
export type DoctorJobSelect = z.infer<typeof doctorJobSelectSchema>;
|
||||
|
||||
export const doctorJobUnselectSchema = z.object({
|
||||
analysisOrderId: z.number(),
|
||||
});
|
||||
export type DoctorJobUnselect = z.infer<typeof doctorJobUnselectSchema>;
|
||||
|
||||
export const FeedbackStatus = z.enum(['STARTED', 'DRAFT', 'COMPLETED']);
|
||||
export const doctorAnalysisFeedbackFormSchema = z.object({
|
||||
feedbackValue: z.string().min(15),
|
||||
userId: z.string().uuid(),
|
||||
});
|
||||
export type DoctorAnalysisFeedbackForm = z.infer<
|
||||
typeof doctorAnalysisFeedbackFormSchema
|
||||
>;
|
||||
export const doctorAnalysisFeedbackSchema = z.object({
|
||||
feedbackValue: z.string().min(15),
|
||||
userId: z.string().uuid(),
|
||||
analysisOrderId: z.number(),
|
||||
status: FeedbackStatus,
|
||||
});
|
||||
export type DoctorAnalysisFeedback = z.infer<
|
||||
typeof doctorAnalysisFeedbackSchema
|
||||
>;
|
||||
|
||||
export type DoctorAnalysisFeedbackTable =
|
||||
Database['medreport']['Tables']['doctor_analysis_feedback']['Row'];
|
||||
|
||||
export const AnalysisOrderIdSchema = z.object({
|
||||
id: z.number(),
|
||||
medusa_order_id: z.string(),
|
||||
analysis_element_ids: z.array(z.number()).nullable(),
|
||||
});
|
||||
export type AnalysisOrderId = z.infer<typeof AnalysisOrderIdSchema>;
|
||||
|
||||
export const ElementSchema = z.object({
|
||||
id: z.number(),
|
||||
analysis_response_id: z.number(),
|
||||
analysis_element_original_id: z.string(),
|
||||
unit: z.string().nullable(),
|
||||
response_value: z.number(),
|
||||
response_time: z.string(),
|
||||
norm_upper: z.number().nullable(),
|
||||
norm_upper_included: z.boolean().nullable(),
|
||||
norm_lower: z.number().nullable(),
|
||||
norm_lower_included: z.boolean().nullable(),
|
||||
norm_status: z.number().nullable(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string().nullable(),
|
||||
analysis_name: z.string().nullable(),
|
||||
});
|
||||
export type Element = z.infer<typeof ElementSchema>;
|
||||
|
||||
export const FeedbackSchema = z.object({
|
||||
analysis_order_id: z.number(),
|
||||
user_id: z.string(),
|
||||
doctor_user_id: z.string().nullable(),
|
||||
status: z.string(),
|
||||
});
|
||||
export type Feedback = z.infer<typeof FeedbackSchema>;
|
||||
|
||||
export const OrderSchema = z.object({
|
||||
title: z.string().optional().nullable(),
|
||||
isPackage: z.boolean(),
|
||||
analysisOrderId: z.number(),
|
||||
});
|
||||
export type Order = z.infer<typeof OrderSchema>;
|
||||
|
||||
export const AccountSchema = z.object({
|
||||
name: z.string(),
|
||||
last_name: z.string().nullable(),
|
||||
id: z.string(),
|
||||
primary_owner_user_id: z.string(),
|
||||
});
|
||||
export type Account = z.infer<typeof AccountSchema>;
|
||||
|
||||
export const ResponseTableSchema = z.object({
|
||||
id: z.number(),
|
||||
analysis_order_id: AnalysisOrderIdSchema,
|
||||
order_number: z.string(),
|
||||
order_status: z.string(),
|
||||
user_id: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string().nullable(),
|
||||
elements: z.array(ElementSchema),
|
||||
firstSampleGivenAt: z.string().nullable(),
|
||||
patient: AccountSchema,
|
||||
order: OrderSchema,
|
||||
doctor: AccountSchema.optional().nullable(),
|
||||
feedback: FeedbackSchema.optional(),
|
||||
});
|
||||
export type ResponseTable = z.infer<typeof ResponseTableSchema>;
|
||||
|
||||
export type AnalysisResponseBase = {
|
||||
analysis_order_id: number & {
|
||||
id: number;
|
||||
medusa_order_id: string;
|
||||
analysis_element_ids: number[] | null;
|
||||
};
|
||||
created_at: string;
|
||||
id: number;
|
||||
order_number: string;
|
||||
order_status: Database['medreport']['Tables']['analysis_orders']['Row']['status'];
|
||||
updated_at: string | null;
|
||||
user_id: string;
|
||||
};
|
||||
|
||||
export interface PaginationParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface PaginatedData<T> {
|
||||
data: T[];
|
||||
pagination: {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
totalCount: number;
|
||||
pageSize: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ServerActionResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
@@ -0,0 +1,583 @@
|
||||
import 'server-only';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { AnalysisResultDetails } from '../schema/doctor-analysis-detail-view.schema';
|
||||
import {
|
||||
AnalysisResponseBase,
|
||||
DoctorAnalysisFeedbackTable,
|
||||
PaginatedData,
|
||||
PaginationParams,
|
||||
ResponseTable,
|
||||
} from '../schema/doctor-analysis.schema';
|
||||
|
||||
async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
if (!analysisResponses?.length) return [];
|
||||
|
||||
const analysisResponseIds = analysisResponses.map((r) => r.id);
|
||||
const medusaOrderIds = analysisResponses.map(
|
||||
(r) => r.analysis_order_id.medusa_order_id,
|
||||
);
|
||||
const userIds = analysisResponses.map((response) => response.user_id);
|
||||
|
||||
const [
|
||||
{ data: doctorFeedbackItems },
|
||||
{ data: medusaOrderItems },
|
||||
{ data: analysisResponseElements },
|
||||
{ data: accounts },
|
||||
] = await Promise.all([
|
||||
supabase
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.select('analysis_order_id, user_id, doctor_user_id, status')
|
||||
.in(
|
||||
'analysis_order_id',
|
||||
analysisResponses.map((r) => r.analysis_order_id.id),
|
||||
),
|
||||
supabase
|
||||
.schema('public')
|
||||
.from('order_item')
|
||||
.select('order_id, item_id(product_title, product_type)')
|
||||
.in('order_id', medusaOrderIds),
|
||||
supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_response_elements')
|
||||
.select('*')
|
||||
.in('analysis_response_id', analysisResponseIds),
|
||||
supabase
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('name, last_name, id, primary_owner_user_id')
|
||||
.in('primary_owner_user_id', userIds),
|
||||
]);
|
||||
|
||||
const doctorUserIds =
|
||||
doctorFeedbackItems
|
||||
?.map((item) => item.doctor_user_id)
|
||||
.filter((value) => value !== null) || [];
|
||||
|
||||
const { data: doctorAccounts } = doctorUserIds.length
|
||||
? await supabase
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('name, last_name, id, primary_owner_user_id')
|
||||
.in('primary_owner_user_id', doctorUserIds)
|
||||
: { data: [] };
|
||||
|
||||
const allAccounts = [...(accounts ?? []), ...(doctorAccounts ?? [])];
|
||||
|
||||
return analysisResponses.map((analysisResponse) => {
|
||||
const responseElements =
|
||||
analysisResponseElements?.filter(
|
||||
(element) => element.analysis_response_id === analysisResponse.id,
|
||||
) || [];
|
||||
|
||||
const firstSampleGivenAt = responseElements.length
|
||||
? responseElements.reduce((earliest, current) =>
|
||||
new Date(current.response_time) < new Date(earliest.response_time)
|
||||
? current
|
||||
: earliest,
|
||||
)?.response_time
|
||||
: null;
|
||||
|
||||
const medusaOrder = medusaOrderItems?.find(
|
||||
({ order_id }) =>
|
||||
order_id === analysisResponse.analysis_order_id.medusa_order_id,
|
||||
);
|
||||
|
||||
const patientAccount = allAccounts?.find(
|
||||
({ primary_owner_user_id }) =>
|
||||
analysisResponse.user_id === primary_owner_user_id,
|
||||
);
|
||||
|
||||
const feedback = doctorFeedbackItems?.find(
|
||||
({ analysis_order_id }) =>
|
||||
analysis_order_id === analysisResponse.analysis_order_id.id,
|
||||
);
|
||||
|
||||
const doctorAccount = allAccounts?.find(
|
||||
({ primary_owner_user_id }) =>
|
||||
feedback?.doctor_user_id === primary_owner_user_id,
|
||||
);
|
||||
|
||||
const order = {
|
||||
title: medusaOrder?.item_id.product_title,
|
||||
isPackage:
|
||||
medusaOrder?.item_id.product_type?.toLowerCase() === 'analysis package',
|
||||
analysisOrderId: analysisResponse.analysis_order_id.id,
|
||||
status: analysisResponse.order_status,
|
||||
};
|
||||
|
||||
if (!patientAccount || !analysisResponse) {
|
||||
throw new Error('Invalid data');
|
||||
}
|
||||
|
||||
return {
|
||||
...analysisResponse,
|
||||
elements: responseElements,
|
||||
firstSampleGivenAt,
|
||||
patient: patientAccount,
|
||||
doctor: doctorAccount,
|
||||
order,
|
||||
feedback,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function getUserInProgressResponses({
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
}: PaginationParams): Promise<PaginatedData<ResponseTable>> {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
throw new Error('No user logged in.');
|
||||
}
|
||||
|
||||
const { data: inProgressFeedback } = await supabase
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.select('analysis_order_id')
|
||||
.eq('doctor_user_id', user.id)
|
||||
.neq('status', 'COMPLETED');
|
||||
|
||||
if (!inProgressFeedback?.length) {
|
||||
return {
|
||||
data: [],
|
||||
pagination: { currentPage: page, totalPages: 0, totalCount: 0, pageSize },
|
||||
};
|
||||
}
|
||||
|
||||
const analysisOrderIds = inProgressFeedback.map((f) => f.analysis_order_id);
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const {
|
||||
data: analysisResponses,
|
||||
error,
|
||||
count,
|
||||
} = await supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_responses')
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
analysis_order_id(id, medusa_order_id, analysis_element_ids)
|
||||
`,
|
||||
{ count: 'exact' },
|
||||
)
|
||||
.in('analysis_order_id', analysisOrderIds)
|
||||
.range(offset, offset + pageSize - 1)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
throw new Error('Something went wrong');
|
||||
}
|
||||
|
||||
const enrichedData = await enrichAnalysisData(analysisResponses);
|
||||
const totalCount = count || 0;
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
|
||||
return {
|
||||
data: enrichedData,
|
||||
pagination: { currentPage: page, totalPages, totalCount, pageSize },
|
||||
};
|
||||
}
|
||||
|
||||
export async function getUserDoneResponses({
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
}: PaginationParams): Promise<PaginatedData<ResponseTable>> {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
throw new Error('No user logged in.');
|
||||
}
|
||||
|
||||
const { data: completedFeedback } = await supabase
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.select('analysis_order_id')
|
||||
.eq('doctor_user_id', user.id)
|
||||
.eq('status', 'COMPLETED');
|
||||
|
||||
if (!completedFeedback?.length) {
|
||||
return {
|
||||
data: [],
|
||||
pagination: { currentPage: page, totalPages: 0, totalCount: 0, pageSize },
|
||||
};
|
||||
}
|
||||
|
||||
const analysisOrderIds = completedFeedback.map((f) => f.analysis_order_id);
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const {
|
||||
data: analysisResponses,
|
||||
error,
|
||||
count,
|
||||
} = await supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_responses')
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
analysis_order_id(id, medusa_order_id, status, analysis_element_ids)
|
||||
`,
|
||||
{ count: 'exact' },
|
||||
)
|
||||
.in('analysis_order_id', analysisOrderIds)
|
||||
.range(offset, offset + pageSize - 1)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const enrichedData = await enrichAnalysisData(analysisResponses);
|
||||
const totalCount = count || 0;
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
|
||||
return {
|
||||
data: enrichedData,
|
||||
pagination: { currentPage: page, totalPages, totalCount, pageSize },
|
||||
};
|
||||
}
|
||||
|
||||
export async function getOpenResponses({
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
}: PaginationParams): Promise<PaginatedData<ResponseTable>> {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
const { data: assignedOrderIds } = await supabase
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.select('analysis_order_id')
|
||||
.not('doctor_user_id', 'is', null);
|
||||
|
||||
const assignedIds = assignedOrderIds?.map((f) => f.analysis_order_id) || [];
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
let query = supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_responses')
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
analysis_order_id(id, medusa_order_id, analysis_element_ids)
|
||||
`,
|
||||
{ count: 'exact' },
|
||||
)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (assignedIds.length > 0) {
|
||||
query = query.not('analysis_order_id', 'in', `(${assignedIds.join(',')})`);
|
||||
}
|
||||
|
||||
const {
|
||||
data: analysisResponses,
|
||||
error,
|
||||
count,
|
||||
} = await query.range(offset, offset + pageSize - 1);
|
||||
if (error) throw error;
|
||||
|
||||
const enrichedData = await enrichAnalysisData(analysisResponses);
|
||||
const totalCount = count || 0;
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
|
||||
return {
|
||||
data: enrichedData,
|
||||
pagination: { currentPage: page, totalPages, totalCount, pageSize },
|
||||
};
|
||||
}
|
||||
|
||||
export async function getOtherResponses({
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
}: PaginationParams): Promise<PaginatedData<ResponseTable>> {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (!user) {
|
||||
throw new Error('No user logged in.');
|
||||
}
|
||||
|
||||
const { data: otherFeedback } = await supabase
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.select('analysis_order_id')
|
||||
.neq('doctor_user_id', user.id);
|
||||
|
||||
if (!otherFeedback?.length) {
|
||||
return {
|
||||
data: [],
|
||||
pagination: { currentPage: page, totalPages: 0, totalCount: 0, pageSize },
|
||||
};
|
||||
}
|
||||
|
||||
const analysisOrderIds = otherFeedback.map((f) => f.analysis_order_id);
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const {
|
||||
data: analysisResponses,
|
||||
error,
|
||||
count,
|
||||
} = await supabase
|
||||
.schema('medreport')
|
||||
.from('analysis_responses')
|
||||
.select(
|
||||
`
|
||||
*,
|
||||
analysis_order_id(id, medusa_order_id, analysis_element_ids)
|
||||
`,
|
||||
{ count: 'exact' },
|
||||
)
|
||||
.in('analysis_order_id', analysisOrderIds)
|
||||
.range(offset, offset + pageSize - 1)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const enrichedData = await enrichAnalysisData(analysisResponses);
|
||||
const totalCount = count || 0;
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
|
||||
return {
|
||||
data: enrichedData,
|
||||
pagination: { currentPage: page, totalPages, totalCount, pageSize },
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAnalysisResultsForDoctor(
|
||||
id: number,
|
||||
): Promise<AnalysisResultDetails> {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
const { data: analysisResponse, error } = await supabase
|
||||
.schema('medreport')
|
||||
.from(`analysis_response_elements`)
|
||||
.select(
|
||||
`*,
|
||||
analysis_responses(user_id, analysis_order_id(id,medusa_order_id, analysis_element_ids))`,
|
||||
)
|
||||
.eq('analysis_response_id', id);
|
||||
|
||||
if (error) {
|
||||
throw new Error('Something went wrong.');
|
||||
}
|
||||
|
||||
const firstAnalysisResponse = analysisResponse?.[0];
|
||||
const userId = firstAnalysisResponse?.analysis_responses.user_id;
|
||||
const medusaOrderId =
|
||||
firstAnalysisResponse?.analysis_responses?.analysis_order_id
|
||||
?.medusa_order_id;
|
||||
|
||||
if (!analysisResponse?.length || !userId || !medusaOrderId) {
|
||||
throw new Error('Failed to retrieve full analysis data.');
|
||||
}
|
||||
|
||||
const [
|
||||
{ data: medusaOrderItems, error: medusaOrderError },
|
||||
{ data: accountWithParams, error: accountError },
|
||||
{ data: doctorFeedback, error: feedbackError },
|
||||
] = await Promise.all([
|
||||
supabase
|
||||
.schema('public')
|
||||
.from('order_item')
|
||||
.select(`order_id, item_id(product_title, product_type)`)
|
||||
.eq('order_id', medusaOrderId),
|
||||
supabase
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select(
|
||||
`primary_owner_user_id, id, name, last_name, personal_code, phone, email,
|
||||
account_params(height,weight)`,
|
||||
)
|
||||
.eq('is_personal_account', true)
|
||||
.eq('primary_owner_user_id', userId)
|
||||
.limit(1),
|
||||
await supabase
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.select(`*`)
|
||||
.eq(
|
||||
'analysis_order_id',
|
||||
firstAnalysisResponse.analysis_responses.analysis_order_id.id,
|
||||
)
|
||||
.limit(1),
|
||||
]);
|
||||
|
||||
if (medusaOrderError || accountError || feedbackError) {
|
||||
throw new Error('Something went wrong.');
|
||||
}
|
||||
|
||||
if (!accountWithParams?.[0]) {
|
||||
throw new Error('Account not found.');
|
||||
}
|
||||
|
||||
const {
|
||||
primary_owner_user_id,
|
||||
id: accountId,
|
||||
name,
|
||||
email,
|
||||
last_name,
|
||||
personal_code,
|
||||
phone,
|
||||
account_params,
|
||||
} = accountWithParams[0];
|
||||
|
||||
return {
|
||||
analysisResponse,
|
||||
order: {
|
||||
title: medusaOrderItems?.[0]?.item_id.product_title ?? '-',
|
||||
isPackage:
|
||||
medusaOrderItems?.[0]?.item_id.product_type?.toLowerCase() ===
|
||||
'analysis package',
|
||||
analysisOrderId:
|
||||
firstAnalysisResponse.analysis_responses.analysis_order_id.id,
|
||||
},
|
||||
doctorFeedback: doctorFeedback?.[0],
|
||||
patient: {
|
||||
userId: primary_owner_user_id,
|
||||
accountId,
|
||||
firstName: name,
|
||||
lastName: last_name,
|
||||
personalCode: personal_code,
|
||||
phone,
|
||||
email,
|
||||
height: account_params?.[0]?.height,
|
||||
weight: account_params?.[0]?.weight,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function selectJob(analysisOrderId: number, userId: string) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (!user?.id) {
|
||||
throw new Error('No user logged in.');
|
||||
}
|
||||
|
||||
const { data: existingFeedback, error: existingFeedbackError } =
|
||||
await supabase
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.select('doctor_user_id')
|
||||
.eq('analysis_order_id', analysisOrderId)
|
||||
.limit(1);
|
||||
|
||||
const jobAssignedToUserId = existingFeedback?.[0]?.doctor_user_id;
|
||||
|
||||
if (!!jobAssignedToUserId && jobAssignedToUserId !== user.id) {
|
||||
throw new Error('Job already assigned to another doctor.');
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.upsert(
|
||||
{
|
||||
doctor_user_id: user.id,
|
||||
user_id: userId,
|
||||
analysis_order_id: analysisOrderId,
|
||||
},
|
||||
{ onConflict: 'analysis_order_id' },
|
||||
);
|
||||
|
||||
if (error || existingFeedbackError) {
|
||||
throw new Error('Something went wrong');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function unselectJob(analysisOrderId: number) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (!user?.id) {
|
||||
throw new Error('No user logged in.');
|
||||
}
|
||||
|
||||
const { data: currentDoctorFeedback, error: currentDoctorFeedbackError } =
|
||||
await supabase
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.select('id,doctor_user_id')
|
||||
.eq('analysis_order_id', analysisOrderId)
|
||||
.eq('doctor_user_id', user.id)
|
||||
.limit(1);
|
||||
|
||||
if (!currentDoctorFeedback) {
|
||||
throw new Error(
|
||||
'Current user not assigned to give feedback to this set of analyses.',
|
||||
);
|
||||
}
|
||||
|
||||
if (currentDoctorFeedbackError) {
|
||||
throw new Error('Error retrieving job data');
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.update({
|
||||
doctor_user_id: null,
|
||||
})
|
||||
.eq('analysis_order_id', analysisOrderId)
|
||||
.eq('doctor_user_id', user.id);
|
||||
|
||||
if (error) {
|
||||
throw new Error('Something went wrong');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function submitFeedback(
|
||||
analysisOrderId: DoctorAnalysisFeedbackTable['analysis_order_id'],
|
||||
userId: DoctorAnalysisFeedbackTable['user_id'],
|
||||
value: DoctorAnalysisFeedbackTable['value'],
|
||||
status: DoctorAnalysisFeedbackTable['status'],
|
||||
) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (!user?.id) {
|
||||
throw new Error('No user logged in.');
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.upsert(
|
||||
{
|
||||
doctor_user_id: user?.id,
|
||||
user_id: userId,
|
||||
analysis_order_id: analysisOrderId,
|
||||
value,
|
||||
status,
|
||||
},
|
||||
{ onConflict: 'analysis_order_id' },
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw new Error('Something went wrong');
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { isDoctor } from './is-doctor';
|
||||
|
||||
/**
|
||||
* @name doctorAction
|
||||
* @description Wrap a server action to ensure the user is a doctor.
|
||||
* @param fn
|
||||
*/
|
||||
export function doctorAction<Args, Response>(
|
||||
fn: (params: Args) => Promise<Response>,
|
||||
) {
|
||||
return async (params: Args): Promise<Response> => {
|
||||
const isUserDoctor = await isDoctor(getSupabaseServerClient());
|
||||
|
||||
if (!isUserDoctor) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return await fn(params);
|
||||
};
|
||||
}
|
||||
@@ -3,8 +3,23 @@
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./components/*": "./src/components/*.tsx",
|
||||
"./services/*": "./src/lib/server/services/*.ts",
|
||||
"./actions/*": "./src/lib/server/actions/*.ts"
|
||||
},
|
||||
"include": [
|
||||
"*.ts",
|
||||
"src",
|
||||
"app"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@components/*": [
|
||||
"./src/lib/*"
|
||||
],
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import Image from 'next/image';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { MedReportLogo } from '@/components/med-report-logo';
|
||||
import { MedReportLogo } from '@kit/shared/components/med-report-logo';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
Reference in New Issue
Block a user