Merge branch 'main' into MED-85

This commit is contained in:
2025-08-27 08:31:26 +03:00
55 changed files with 1356 additions and 547 deletions

View File

@@ -1,6 +1,6 @@
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
import { revalidatePath } from 'next/cache';
import { enhanceAction } from '@kit/next/actions';
import { getLogger } from '@kit/shared/logger';
@@ -13,6 +13,7 @@ import {
doctorJobSelectSchema,
doctorJobUnselectSchema,
} from '../schema/doctor-analysis.schema';
import { ErrorReason } from '../schema/error.type';
import {
selectJob,
submitFeedback,
@@ -29,13 +30,28 @@ export const selectJobAction = doctorAction(
async ({ analysisOrderId, userId }: DoctorJobSelect) => {
const logger = await getLogger();
logger.info({ analysisOrderId }, `Selecting new job`);
try {
logger.info({ analysisOrderId }, `Selecting new job`);
await selectJob(analysisOrderId, userId);
await selectJob(analysisOrderId, userId);
logger.info({ analysisOrderId }, `Successfully selected`);
logger.info({ analysisOrderId }, `Successfully selected`);
return { success: true };
revalidateDoctorAnalysis();
return { success: true };
} catch (e) {
logger.error('Failed to select job', e);
if (e instanceof Error) {
revalidateDoctorAnalysis();
return {
success: false,
reason:
e['message'] === ErrorReason.JOB_ASSIGNED
? ErrorReason.JOB_ASSIGNED
: ErrorReason.UNKNOWN,
};
}
return { success: false, reason: ErrorReason.UNKNOWN };
}
},
{
schema: doctorJobSelectSchema,
@@ -51,17 +67,21 @@ export const unselectJobAction = doctorAction(
enhanceAction(
async ({ analysisOrderId }: DoctorJobUnselect) => {
const logger = await getLogger();
try {
logger.info({ analysisOrderId }, `Removing doctor from job`);
logger.info({ analysisOrderId }, `Removing doctor from job`);
await unselectJob(analysisOrderId);
await unselectJob(analysisOrderId);
logger.info(
{ analysisOrderId },
`Successfully removed current doctor from job`,
);
return { success: true };
logger.info(
{ analysisOrderId },
`Successfully removed current doctor from job`,
);
revalidateDoctorAnalysis();
return { success: true };
} catch (e) {
logger.error('Failed to unselect job', e);
return { success: false, reason: ErrorReason.UNKNOWN };
}
},
{
schema: doctorJobUnselectSchema,
@@ -88,23 +108,28 @@ export const giveFeedbackAction = doctorAction(
}) => {
const logger = await getLogger();
logger.info(
{ analysisOrderId },
`Submitting feedback for analysis order...`,
);
try {
logger.info(
{ analysisOrderId },
`Submitting feedback for analysis order...`,
);
const result = await submitFeedback(
analysisOrderId,
userId,
feedbackValue,
status,
);
logger.info({ analysisOrderId }, `Successfully submitted feedback`);
await submitFeedback(analysisOrderId, userId, feedbackValue, status);
logger.info({ analysisOrderId }, `Successfully submitted feedback`);
return result;
revalidateDoctorAnalysis();
return { success: true };
} catch (e) {
logger.error('Failed to give feedback', e);
return { success: false, reason: ErrorReason.UNKNOWN };
}
},
{
schema: doctorAnalysisFeedbackSchema,
},
),
);
function revalidateDoctorAnalysis() {
revalidatePath('/doctor/analysis');
}

View File

@@ -66,6 +66,27 @@ export const AnalysisResponseSchema = z.object({
updated_at: z.string().nullable(),
analysis_name: z.string().nullable(),
analysis_responses: AnalysisResponsesSchema,
comment: z.string().nullable(),
latestPreviousAnalysis: 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(),
comment: z.string().nullable(),
})
.optional()
.nullable(),
});
export type AnalysisResponse = z.infer<typeof AnalysisResponseSchema>;

View File

@@ -15,18 +15,19 @@ 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),
feedbackValue: z.string().min(10, { message: 'doctor:feedbackLengthError' }),
userId: z.string().uuid(),
});
export type DoctorAnalysisFeedbackForm = z.infer<
typeof doctorAnalysisFeedbackFormSchema
>;
export const doctorAnalysisFeedbackSchema = z.object({
feedbackValue: z.string().min(15),
feedbackValue: z.string(),
userId: z.string().uuid(),
analysisOrderId: z.number(),
status: FeedbackStatus,
});
export type DoctorAnalysisFeedback = z.infer<
typeof doctorAnalysisFeedbackSchema
>;

View File

@@ -0,0 +1,4 @@
export enum ErrorReason {
JOB_ASSIGNED = 'JOB_ASSIGNED',
UNKNOWN = 'UNKNOWN',
}

View File

@@ -1,5 +1,7 @@
import 'server-only';
import { isBefore } from 'date-fns';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { AnalysisResultDetails } from '../schema/doctor-analysis-detail-view.schema';
@@ -10,6 +12,7 @@ import {
PaginationParams,
ResponseTable,
} from '../schema/doctor-analysis.schema';
import { ErrorReason } from '../schema/error.type';
async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
const supabase = getSupabaseServerClient();
@@ -360,7 +363,7 @@ export async function getAnalysisResultsForDoctor(
): Promise<AnalysisResultDetails> {
const supabase = getSupabaseServerClient();
const { data: analysisResponse, error } = await supabase
const { data: analysisResponseElements, error } = await supabase
.schema('medreport')
.from(`analysis_response_elements`)
.select(
@@ -373,20 +376,26 @@ export async function getAnalysisResultsForDoctor(
throw new Error('Something went wrong.');
}
const firstAnalysisResponse = analysisResponse?.[0];
const firstAnalysisResponse = analysisResponseElements?.[0];
const userId = firstAnalysisResponse?.analysis_responses.user_id;
const medusaOrderId =
firstAnalysisResponse?.analysis_responses?.analysis_order_id
?.medusa_order_id;
if (!analysisResponse?.length || !userId || !medusaOrderId) {
if (!analysisResponseElements?.length || !userId || !medusaOrderId) {
throw new Error('Failed to retrieve full analysis data.');
}
const responseElementAnalysisElementOriginalIds =
analysisResponseElements.map(
({ analysis_element_original_id }) => analysis_element_original_id,
);
const [
{ data: medusaOrderItems, error: medusaOrderError },
{ data: accountWithParams, error: accountError },
{ data: doctorFeedback, error: feedbackError },
{ data: previousAnalyses, error: previousAnalysesError },
] = await Promise.all([
supabase
.schema('public')
@@ -403,7 +412,7 @@ export async function getAnalysisResultsForDoctor(
.eq('is_personal_account', true)
.eq('primary_owner_user_id', userId)
.limit(1),
await supabase
supabase
.schema('medreport')
.from('doctor_analysis_feedback')
.select(`*`)
@@ -412,9 +421,39 @@ export async function getAnalysisResultsForDoctor(
firstAnalysisResponse.analysis_responses.analysis_order_id.id,
)
.limit(1),
supabase
.schema('medreport')
.from('analysis_response_elements')
.select(
`
*,
analysis_responses!inner(
user_id
)
`,
)
.in(
'analysis_element_original_id',
responseElementAnalysisElementOriginalIds,
)
.not(
'analysis_response_id',
'eq',
firstAnalysisResponse.analysis_response_id,
)
.eq(
'analysis_responses.user_id',
firstAnalysisResponse.analysis_responses.user_id,
)
.order('response_time'),
]);
if (medusaOrderError || accountError || feedbackError) {
if (
medusaOrderError ||
accountError ||
feedbackError ||
previousAnalysesError
) {
throw new Error('Something went wrong.');
}
@@ -433,8 +472,25 @@ export async function getAnalysisResultsForDoctor(
account_params,
} = accountWithParams[0];
const analysisResponseElementsWithPreviousData = [];
for (const analysisResponseElement of analysisResponseElements) {
const latestPreviousAnalysis = previousAnalyses.find(
({ analysis_element_original_id, response_time }) =>
analysis_element_original_id ===
analysisResponseElement.analysis_element_original_id &&
isBefore(
new Date(response_time),
new Date(analysisResponseElement.response_time),
),
);
analysisResponseElementsWithPreviousData.push({
...analysisResponseElement,
latestPreviousAnalysis,
});
}
return {
analysisResponse,
analysisResponse: analysisResponseElementsWithPreviousData,
order: {
title: medusaOrderItems?.[0]?.item_id.product_title ?? '-',
isPackage:
@@ -479,7 +535,7 @@ export async function selectJob(analysisOrderId: number, userId: string) {
const jobAssignedToUserId = existingFeedback?.[0]?.doctor_user_id;
if (!!jobAssignedToUserId && jobAssignedToUserId !== user.id) {
throw new Error('Job already assigned to another doctor.');
throw new Error(ErrorReason.JOB_ASSIGNED);
}
const { data, error } = await supabase

View File

@@ -291,6 +291,7 @@ export class TeamAccountsApi {
.single();
if (error) {
console.warn('Error fetching company params', error);
throw error;
}

View File

@@ -18,7 +18,6 @@ import { VersionUpdater } from '@kit/ui/version-updater';
import { i18nResolver } from '../../../../lib/i18n/i18n.resolver';
import { getI18nSettings } from '../../../../lib/i18n/i18n.settings';
import { ReactQueryProvider } from './react-query-provider';
const captchaSiteKey = authConfig.captchaTokenSiteKey;

View File

@@ -13,7 +13,7 @@ export function InfoTooltip({
content,
icon,
}: {
content?: string;
content?: string | null;
icon?: JSX.Element;
}) {
if (!content) return null;

View File

@@ -42,6 +42,33 @@ export type Database = {
}
Relationships: []
}
doctor_page_views: {
Row: {
action: Database["audit"]["Enums"]["doctor_page_view_action"]
created_at: string
data_owner_user_id: string | null
id: number
viewed_record_key: string | null
viewer_user_id: string
}
Insert: {
action: Database["audit"]["Enums"]["doctor_page_view_action"]
created_at?: string
data_owner_user_id?: string | null
id?: number
viewed_record_key?: string | null
viewer_user_id: string
}
Update: {
action?: Database["audit"]["Enums"]["doctor_page_view_action"]
created_at?: string
data_owner_user_id?: string | null
id?: number
viewed_record_key?: string | null
viewer_user_id?: string
}
Relationships: []
}
log_entries: {
Row: {
changed_at: string
@@ -204,6 +231,12 @@ export type Database = {
[_ in never]: never
}
Enums: {
doctor_page_view_action:
| "VIEW_ANALYSIS_RESULTS"
| "VIEW_DASHBOARD"
| "VIEW_OPEN_JOBS"
| "VIEW_OWN_JOBS"
| "VIEW_DONE_JOBS"
request_status: "SUCCESS" | "FAIL"
sync_status: "SUCCESS" | "FAIL"
}
@@ -574,6 +607,7 @@ export type Database = {
analysis_element_original_id: string
analysis_name: string | null
analysis_response_id: number
comment: string | null
created_at: string
id: number
norm_lower: number | null
@@ -591,6 +625,7 @@ export type Database = {
analysis_element_original_id: string
analysis_name?: string | null
analysis_response_id: number
comment?: string | null
created_at?: string
id?: number
norm_lower?: number | null
@@ -608,6 +643,7 @@ export type Database = {
analysis_element_original_id?: string
analysis_name?: string | null
analysis_response_id?: number
comment?: string | null
created_at?: string
id?: number
norm_lower?: number | null
@@ -1748,7 +1784,9 @@ export type Database = {
Returns: Json
}
create_team_account: {
Args: { account_name: string; new_personal_code: string }
Args:
| { account_name: string }
| { account_name: string; new_personal_code: string }
Returns: {
application_role: Database["medreport"]["Enums"]["application_role"]
city: string | null
@@ -1935,6 +1973,22 @@ export type Database = {
}
Returns: undefined
}
update_analysis_order_status: {
Args: {
order_id: number
medusa_order_id_param: string
status_param: Database["medreport"]["Enums"]["analysis_order_status"]
}
Returns: {
analysis_element_ids: number[] | null
analysis_ids: number[] | null
created_at: string
id: number
medusa_order_id: string
status: Database["medreport"]["Enums"]["analysis_order_status"]
user_id: string
}
}
upsert_order: {
Args: {
target_account_id: string
@@ -7905,6 +7959,13 @@ export type CompositeTypes<
export const Constants = {
audit: {
Enums: {
doctor_page_view_action: [
"VIEW_ANALYSIS_RESULTS",
"VIEW_DASHBOARD",
"VIEW_OPEN_JOBS",
"VIEW_OWN_JOBS",
"VIEW_DONE_JOBS",
],
request_status: ["SUCCESS", "FAIL"],
sync_status: ["SUCCESS", "FAIL"],
},

View File

@@ -21,6 +21,7 @@ export function useSignUpWithEmailAndPassword() {
const response = await client.auth.signUp({
...credentials,
options: {
emailRedirectTo,
captchaToken,
},
});