MED-137: add doctor other jobs view (#55)

* add doctor jobs view

* change translation

* another translation change

* clean up

* add analaysis detail view to paths config

* translation

* merge fix

* fix path

* move components to shared

* refactor

* imports

* clean up
This commit is contained in:
Helena
2025-08-25 11:12:57 +03:00
committed by GitHub
parent ee86bb8829
commit 195af1db3d
156 changed files with 2823 additions and 364 deletions

View File

@@ -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 @@
]
}
}
}
}

View File

@@ -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} />;
};
}

View 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})`;
}

View File

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

View File

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

View File

@@ -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>;

View File

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

View File

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

View File

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

View File

@@ -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/*"
],
}
}

View File

@@ -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';