Merge branch 'develop' of https://github.com/MR-medreport/MRB2B into MED-103
This commit is contained in:
@@ -1,23 +1,9 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
import { AnalysisResultDetails, UserAnalysis } from '../types/accounts';
|
||||
import PersonalCode from '~/lib/utils';
|
||||
|
||||
export type AccountWithParams =
|
||||
Database['medreport']['Tables']['accounts']['Row'] & {
|
||||
accountParams:
|
||||
| (Pick<
|
||||
Database['medreport']['Tables']['account_params']['Row'],
|
||||
'weight' | 'height'
|
||||
> & {
|
||||
isSmoker:
|
||||
| Database['medreport']['Tables']['account_params']['Row']['is_smoker']
|
||||
| null;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
import { AccountWithParams } from '../types/accounts';
|
||||
|
||||
/**
|
||||
* Class representing an API for interacting with user accounts.
|
||||
@@ -25,7 +11,7 @@ export type AccountWithParams =
|
||||
* @param {SupabaseClient<Database>} client - The Supabase client instance.
|
||||
*/
|
||||
class AccountsApi {
|
||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||
constructor(private readonly client: SupabaseClient<Database>) { }
|
||||
|
||||
/**
|
||||
* @name getAccount
|
||||
@@ -218,89 +204,6 @@ class AccountsApi {
|
||||
return response.data?.customer_id;
|
||||
}
|
||||
|
||||
async getUserAnalysis(
|
||||
analysisOrderId: number,
|
||||
): Promise<AnalysisResultDetails | null> {
|
||||
const authUser = await this.client.auth.getUser();
|
||||
const { data, error: userError } = authUser;
|
||||
|
||||
if (userError) {
|
||||
console.error('Failed to get user', userError);
|
||||
throw userError;
|
||||
}
|
||||
|
||||
const { user } = data;
|
||||
|
||||
const { data: analysisResponse } = await this.client
|
||||
.schema('medreport')
|
||||
.from('analysis_responses')
|
||||
.select(
|
||||
`*,
|
||||
elements:analysis_response_elements(analysis_name,norm_status,response_value,unit,norm_lower_included,norm_upper_included,norm_lower,norm_upper,response_time),
|
||||
order:analysis_order_id(medusa_order_id, status, created_at),
|
||||
summary:analysis_order_id(doctor_analysis_feedback(*))`,
|
||||
)
|
||||
.eq('user_id', user.id)
|
||||
.eq('analysis_order_id', analysisOrderId)
|
||||
.throwOnError();
|
||||
|
||||
const responseWithElements = analysisResponse?.[0];
|
||||
if (!responseWithElements) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const feedback = responseWithElements.summary.doctor_analysis_feedback?.[0];
|
||||
|
||||
return {
|
||||
...responseWithElements,
|
||||
summary:
|
||||
feedback?.status === 'COMPLETED'
|
||||
? responseWithElements.summary.doctor_analysis_feedback?.[0]
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
async getUserAnalyses(): Promise<UserAnalysis | null> {
|
||||
const authUser = await this.client.auth.getUser();
|
||||
const { data, error: userError } = authUser;
|
||||
|
||||
if (userError) {
|
||||
console.error('Failed to get user', userError);
|
||||
throw userError;
|
||||
}
|
||||
|
||||
const { user } = data;
|
||||
|
||||
const { data: analysisResponses } = await this.client
|
||||
.schema('medreport')
|
||||
.from('analysis_responses')
|
||||
.select('*')
|
||||
.eq('user_id', user.id);
|
||||
|
||||
if (!analysisResponses) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const analysisResponseIds = analysisResponses.map((r) => r.id);
|
||||
|
||||
const { data: analysisResponseElements } = await this.client
|
||||
.schema('medreport')
|
||||
.from('analysis_response_elements')
|
||||
.select('*')
|
||||
.in('analysis_response_id', analysisResponseIds);
|
||||
|
||||
if (!analysisResponseElements) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return analysisResponses.map((r) => ({
|
||||
...r,
|
||||
elements: analysisResponseElements.filter(
|
||||
(e) => e.analysis_response_id === r.id,
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
async hasAccountTeamMembership(accountId?: string) {
|
||||
if (!accountId) {
|
||||
return false;
|
||||
@@ -318,23 +221,6 @@ class AccountsApi {
|
||||
|
||||
return (count ?? 0) > 0;
|
||||
}
|
||||
|
||||
async fetchBmiThresholds() {
|
||||
// Fetch BMI
|
||||
const { data, error } = await this.client
|
||||
.schema('medreport')
|
||||
.from('bmi_thresholds')
|
||||
.select(
|
||||
'age_min,age_max,underweight_max,normal_min,normal_max,overweight_min,strong_min,obesity_min',
|
||||
)
|
||||
.order('age_min', { ascending: true });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching BMI thresholds:', error);
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export function createAccountsApi(client: SupabaseClient<Database>) {
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export type UserAnalysisElement =
|
||||
Database['medreport']['Tables']['analysis_response_elements']['Row'];
|
||||
export type UserAnalysisResponse =
|
||||
Database['medreport']['Tables']['analysis_responses']['Row'] & {
|
||||
elements: UserAnalysisElement[];
|
||||
};
|
||||
export type UserAnalysis = UserAnalysisResponse[];
|
||||
|
||||
export type ApplicationRole =
|
||||
Database['medreport']['Tables']['accounts']['Row']['application_role'];
|
||||
export enum ApplicationRoleEnum {
|
||||
@@ -18,50 +8,16 @@ export enum ApplicationRoleEnum {
|
||||
SuperAdmin = 'super_admin',
|
||||
}
|
||||
|
||||
export const ElementSchema = z.object({
|
||||
unit: z.string(),
|
||||
norm_lower: z.number(),
|
||||
norm_upper: z.number(),
|
||||
norm_status: z.number(),
|
||||
analysis_name: z.string(),
|
||||
response_time: z.string(),
|
||||
response_value: z.number(),
|
||||
norm_lower_included: z.boolean(),
|
||||
norm_upper_included: z.boolean(),
|
||||
});
|
||||
export type Element = z.infer<typeof ElementSchema>;
|
||||
|
||||
export const OrderSchema = z.object({
|
||||
status: z.string(),
|
||||
medusa_order_id: z.string(),
|
||||
created_at: z.coerce.date(),
|
||||
});
|
||||
export type Order = z.infer<typeof OrderSchema>;
|
||||
|
||||
export const SummarySchema = z.object({
|
||||
id: z.number(),
|
||||
value: z.string(),
|
||||
status: z.string(),
|
||||
user_id: z.string(),
|
||||
created_at: z.coerce.date(),
|
||||
created_by: z.string(),
|
||||
updated_at: z.coerce.date().nullable(),
|
||||
updated_by: z.string(),
|
||||
doctor_user_id: z.string().nullable(),
|
||||
analysis_order_id: z.number(),
|
||||
});
|
||||
export type Summary = z.infer<typeof SummarySchema>;
|
||||
|
||||
export const AnalysisResultDetailsSchema = z.object({
|
||||
id: z.number(),
|
||||
analysis_order_id: z.number(),
|
||||
order_number: z.string(),
|
||||
order_status: z.string(),
|
||||
user_id: z.string(),
|
||||
created_at: z.coerce.date(),
|
||||
updated_at: z.coerce.date().nullable(),
|
||||
elements: z.array(ElementSchema),
|
||||
order: OrderSchema,
|
||||
summary: SummarySchema.nullable(),
|
||||
});
|
||||
export type AnalysisResultDetails = z.infer<typeof AnalysisResultDetailsSchema>;
|
||||
export type AccountWithParams =
|
||||
Database['medreport']['Tables']['accounts']['Row'] & {
|
||||
accountParams:
|
||||
| (Pick<
|
||||
Database['medreport']['Tables']['account_params']['Row'],
|
||||
'weight' | 'height'
|
||||
> & {
|
||||
isSmoker:
|
||||
| Database['medreport']['Tables']['account_params']['Row']['is_smoker']
|
||||
| null;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
|
||||
3
packages/features/accounts/src/types/analysis-orders.ts
Normal file
3
packages/features/accounts/src/types/analysis-orders.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
|
||||
export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||
139
packages/features/accounts/src/types/analysis-results.ts
Normal file
139
packages/features/accounts/src/types/analysis-results.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export type UserAnalysisElement =
|
||||
Database['medreport']['Tables']['analysis_response_elements']['Row'];
|
||||
export type UserAnalysisResponse =
|
||||
Database['medreport']['Tables']['analysis_responses']['Row'] & {
|
||||
elements: UserAnalysisElement[];
|
||||
};
|
||||
export type UserAnalysis = UserAnalysisResponse[];
|
||||
|
||||
const ElementSchema = z.object({
|
||||
unit: z.string(),
|
||||
norm_lower: z.number(),
|
||||
norm_upper: z.number(),
|
||||
norm_status: z.number(),
|
||||
analysis_name: z.string(),
|
||||
response_time: z.string(),
|
||||
response_value: z.number(),
|
||||
response_value_is_negative: z.boolean(),
|
||||
norm_lower_included: z.boolean(),
|
||||
norm_upper_included: z.boolean(),
|
||||
status: z.string(),
|
||||
analysis_element_original_id: z.string(),
|
||||
original_response_element: z.object({
|
||||
|
||||
}),
|
||||
});
|
||||
|
||||
const OrderSchema = z.object({
|
||||
status: z.string(),
|
||||
medusa_order_id: z.string(),
|
||||
created_at: z.coerce.date(),
|
||||
});
|
||||
|
||||
const DoctorAnalysisFeedbackSchema = z.object({
|
||||
id: z.number(),
|
||||
status: z.string(),
|
||||
user_id: z.string(),
|
||||
created_at: z.coerce.date(),
|
||||
created_by: z.string(),
|
||||
});
|
||||
|
||||
const SummarySchema = z.object({
|
||||
id: z.number(),
|
||||
value: z.string(),
|
||||
status: z.string(),
|
||||
user_id: z.string(),
|
||||
created_at: z.coerce.date(),
|
||||
created_by: z.string(),
|
||||
updated_at: z.coerce.date().nullable(),
|
||||
updated_by: z.string(),
|
||||
doctor_user_id: z.string().nullable(),
|
||||
analysis_order_id: z.number(),
|
||||
doctor_analysis_feedback: z.array(DoctorAnalysisFeedbackSchema),
|
||||
});
|
||||
|
||||
export const AnalysisResultDetailsSchema = z.object({
|
||||
id: z.number(),
|
||||
analysis_order_id: z.number(),
|
||||
order_number: z.string(),
|
||||
order_status: z.string(),
|
||||
user_id: z.string(),
|
||||
created_at: z.coerce.date(),
|
||||
updated_at: z.coerce.date().nullable(),
|
||||
elements: z.array(ElementSchema),
|
||||
order: OrderSchema,
|
||||
summary: SummarySchema.nullable(),
|
||||
});
|
||||
export type AnalysisResultDetails = z.infer<typeof AnalysisResultDetailsSchema>;
|
||||
|
||||
export type AnalysisResultDetailsElementResults = {
|
||||
unit: string | null;
|
||||
normLower: number | null;
|
||||
normUpper: number | null;
|
||||
normStatus: number | null;
|
||||
responseTime: string | null;
|
||||
responseValue: number | null;
|
||||
responseValueIsNegative: boolean | null;
|
||||
responseValueIsWithinNorm: boolean | null;
|
||||
normLowerIncluded: boolean;
|
||||
normUpperIncluded: boolean;
|
||||
status: string;
|
||||
analysisElementOriginalId: string;
|
||||
nestedElements: {
|
||||
analysisElementOriginalId: string;
|
||||
normLower?: number | null;
|
||||
normLowerIncluded: boolean;
|
||||
normStatus: number;
|
||||
normUpper?: number | null;
|
||||
normUpperIncluded: boolean;
|
||||
responseTime: string;
|
||||
responseValue: number;
|
||||
status: number;
|
||||
unit: string;
|
||||
}[];
|
||||
labComment?: string | null;
|
||||
};
|
||||
|
||||
export type AnalysisResultDetailsElement = {
|
||||
analysisIdOriginal: string;
|
||||
isWaitingForResults: boolean;
|
||||
analysisName: string;
|
||||
results: AnalysisResultDetailsElementResults;
|
||||
};
|
||||
|
||||
export type AnalysisResultDetailsMapped = {
|
||||
id: number;
|
||||
order: {
|
||||
status: string;
|
||||
medusaOrderId: string;
|
||||
createdAt: Date | string;
|
||||
};
|
||||
elements: {
|
||||
id: string;
|
||||
unit: string;
|
||||
norm_lower: number;
|
||||
norm_upper: number;
|
||||
norm_status: number;
|
||||
analysis_name: string;
|
||||
response_time: string;
|
||||
response_value: number;
|
||||
norm_lower_included: boolean;
|
||||
norm_upper_included: boolean;
|
||||
status: string;
|
||||
analysis_element_original_id: string;
|
||||
}[];
|
||||
orderedAnalysisElementIds: number[];
|
||||
orderedAnalysisElements: AnalysisResultDetailsElement[];
|
||||
summary: {
|
||||
id: number;
|
||||
status: string;
|
||||
user_id: string;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
value?: string;
|
||||
} | null;
|
||||
};
|
||||
3
packages/features/user-analyses/eslint.config.mjs
Normal file
3
packages/features/user-analyses/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import eslintConfigBase from '@kit/eslint-config/base.js';
|
||||
|
||||
export default eslintConfigBase;
|
||||
33
packages/features/user-analyses/package.json
Normal file
33
packages/features/user-analyses/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@kit/user-analyses",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"format": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"exports": {
|
||||
"./api": "./src/server/api.ts",
|
||||
"./types/*": "./src/types/*.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"nanoid": "^5.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/shared": "workspace:*",
|
||||
"@kit/supabase": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
312
packages/features/user-analyses/src/server/api.ts
Normal file
312
packages/features/user-analyses/src/server/api.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import type { UuringElement, UuringuVastus } from '@kit/shared/types/medipost-analysis';
|
||||
|
||||
import type { AnalysisResultDetails, AnalysisResultDetailsMapped, UserAnalysis } from '../types/analysis-results';
|
||||
import type { AnalysisOrder } from '../types/analysis-orders';
|
||||
|
||||
/**
|
||||
* Class representing an API for interacting with user accounts.
|
||||
* @constructor
|
||||
* @param {SupabaseClient<Database>} client - The Supabase client instance.
|
||||
*/
|
||||
class UserAnalysesApi {
|
||||
constructor(private readonly client: SupabaseClient<Database>) { }
|
||||
|
||||
async getAnalysisOrder({
|
||||
medusaOrderId,
|
||||
analysisOrderId,
|
||||
}: {
|
||||
medusaOrderId?: string;
|
||||
analysisOrderId?: number;
|
||||
}) {
|
||||
const query = this.client
|
||||
.schema('medreport')
|
||||
.from('analysis_orders')
|
||||
.select('*')
|
||||
if (medusaOrderId) {
|
||||
query.eq('medusa_order_id', medusaOrderId);
|
||||
} else if (analysisOrderId) {
|
||||
query.eq('id', analysisOrderId);
|
||||
} else {
|
||||
throw new Error('Either medusaOrderId or orderId must be provided');
|
||||
}
|
||||
|
||||
const { data: order, error } = await query.single();
|
||||
if (error) {
|
||||
throw new Error(`Failed to get order by medusaOrderId=${medusaOrderId} or analysisOrderId=${analysisOrderId}, message=${error.message}, data=${JSON.stringify(order)}`);
|
||||
}
|
||||
return order as AnalysisOrder;
|
||||
}
|
||||
|
||||
async getUserAnalysis(
|
||||
analysisOrderId: number,
|
||||
): Promise<AnalysisResultDetailsMapped | null> {
|
||||
const authUser = await this.client.auth.getUser();
|
||||
const { data, error: userError } = authUser;
|
||||
|
||||
if (userError) {
|
||||
console.error('Failed to get user', userError);
|
||||
throw userError;
|
||||
}
|
||||
|
||||
const { user } = data;
|
||||
|
||||
const analysisOrder = await this.getAnalysisOrder({ analysisOrderId });
|
||||
const orderedAnalysisElementIds = analysisOrder.analysis_element_ids ?? [];
|
||||
if (orderedAnalysisElementIds.length === 0) {
|
||||
console.error('No ordered analysis element ids found for analysis order id=', analysisOrderId);
|
||||
return null;
|
||||
}
|
||||
const { data: orderedAnalysisElements, error: orderedAnalysisElementsError } = await this.client
|
||||
.schema('medreport')
|
||||
.from('analysis_elements')
|
||||
.select('analysis_id_original,analysis_name_lab')
|
||||
.in('id', orderedAnalysisElementIds);
|
||||
if (orderedAnalysisElementsError) {
|
||||
console.error('Failed to get ordered analysis elements for analysis order id=', analysisOrderId, orderedAnalysisElementsError);
|
||||
throw orderedAnalysisElementsError;
|
||||
}
|
||||
|
||||
const orderedAnalysisElementOriginalIds = orderedAnalysisElements.map(({ analysis_id_original }) => analysis_id_original);
|
||||
if (orderedAnalysisElementOriginalIds.length === 0) {
|
||||
console.error('No ordered analysis element original ids found for analysis order id=', analysisOrderId);
|
||||
return null;
|
||||
}
|
||||
|
||||
const { data: analysisResponse } = await this.client
|
||||
.schema('medreport')
|
||||
.from('analysis_responses')
|
||||
.select(
|
||||
`*,
|
||||
elements:analysis_response_elements(analysis_name,norm_status,response_value,unit,norm_lower_included,norm_upper_included,norm_lower,norm_upper,response_time,status,analysis_element_original_id,original_response_element,response_value_is_negative,response_value_is_within_norm),
|
||||
summary:analysis_order_id(doctor_analysis_feedback(*))`,
|
||||
)
|
||||
.eq('user_id', user.id)
|
||||
.eq('analysis_order_id', analysisOrderId)
|
||||
.throwOnError();
|
||||
|
||||
const responseWithElements = analysisResponse?.[0] as AnalysisResultDetails | null;
|
||||
if (!responseWithElements) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const analysisResponseElements = responseWithElements.elements;
|
||||
|
||||
const feedback = responseWithElements.summary?.doctor_analysis_feedback?.[0];
|
||||
|
||||
const mappedOrderedAnalysisElements = orderedAnalysisElements.map(({ analysis_id_original, analysis_name_lab }) => {
|
||||
return this.getOrderedAnalysisElements({
|
||||
analysisIdOriginal: analysis_id_original,
|
||||
analysisNameLab: analysis_name_lab,
|
||||
analysisResponseElements,
|
||||
});
|
||||
}).sort((a, b) => a.analysisName.localeCompare(b.analysisName));
|
||||
const nestedAnalysisElementIds = mappedOrderedAnalysisElements.map(({ results }) => results?.nestedElements.map(({ analysisElementOriginalId }) => analysisElementOriginalId)).flat().filter(Boolean);
|
||||
if (nestedAnalysisElementIds.length > 0) {
|
||||
const { data: nestedAnalysisElements, error: nestedAnalysisElementsError } = await this.client
|
||||
.schema('medreport')
|
||||
.from('analysis_elements')
|
||||
.select('*')
|
||||
.in('id', nestedAnalysisElementIds);
|
||||
if (!nestedAnalysisElementsError && nestedAnalysisElements) {
|
||||
for (const mappedOrderedAnalysisElement of mappedOrderedAnalysisElements) {
|
||||
const { results } = mappedOrderedAnalysisElement;
|
||||
if (!results) {
|
||||
continue;
|
||||
}
|
||||
for (const nestedElement of results.nestedElements) {
|
||||
const { analysisElementOriginalId } = nestedElement;
|
||||
const nestedAnalysisElement = nestedAnalysisElements.find(({ id }) => id === analysisElementOriginalId);
|
||||
if (!nestedAnalysisElement) {
|
||||
continue;
|
||||
}
|
||||
results.nestedElements.push({
|
||||
...nestedAnalysisElement,
|
||||
analysisElementOriginalId,
|
||||
analysisName: nestedAnalysisElement.analysis_name_lab,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
mappedOrderedAnalysisElements.forEach(({ results }) => {
|
||||
results?.nestedElements.forEach(({ analysisElementOriginalId }) => {
|
||||
const nestedAnalysisElement = nestedAnalysisElements.find(({ id }) => id === analysisElementOriginalId);
|
||||
if (nestedAnalysisElement) {
|
||||
results?.nestedElements.push({
|
||||
...nestedAnalysisElement,
|
||||
analysisElementOriginalId,
|
||||
analysisName: nestedAnalysisElement.analysis_name_lab,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: analysisOrderId,
|
||||
order: {
|
||||
status: analysisOrder.status,
|
||||
medusaOrderId: analysisOrder.medusa_order_id,
|
||||
createdAt: new Date(analysisOrder.created_at),
|
||||
},
|
||||
orderedAnalysisElementIds,
|
||||
orderedAnalysisElements: mappedOrderedAnalysisElements,
|
||||
summary:
|
||||
feedback?.status === 'COMPLETED'
|
||||
? (responseWithElements.summary?.doctor_analysis_feedback?.[0] ?? null)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
getOrderedAnalysisElements({
|
||||
analysisIdOriginal,
|
||||
analysisNameLab,
|
||||
analysisResponseElements,
|
||||
}: {
|
||||
analysisIdOriginal: string;
|
||||
analysisNameLab: string;
|
||||
analysisResponseElements: AnalysisResultDetails['elements'];
|
||||
}) {
|
||||
const elementResponse = analysisResponseElements.find((element) => element.analysis_element_original_id === analysisIdOriginal);
|
||||
if (!elementResponse) {
|
||||
return {
|
||||
analysisIdOriginal,
|
||||
isWaitingForResults: true,
|
||||
analysisName: analysisNameLab,
|
||||
};
|
||||
}
|
||||
const labComment = elementResponse.original_response_element?.UuringuKommentaar;
|
||||
return {
|
||||
analysisIdOriginal,
|
||||
isWaitingForResults: false,
|
||||
analysisName: analysisNameLab,
|
||||
results: {
|
||||
nestedElements: (() => {
|
||||
const nestedElements = elementResponse.original_response_element?.UuringuElement as UuringElement[] | undefined;
|
||||
if (!nestedElements) {
|
||||
return [];
|
||||
}
|
||||
return nestedElements.map((element) => {
|
||||
const elementVastus = element.UuringuVastus as UuringuVastus | undefined;
|
||||
const responseValue = elementVastus?.VastuseVaartus;
|
||||
const responseValueIsNumeric = !isNaN(Number(responseValue));
|
||||
const responseValueIsNegative = responseValue === 'Negatiivne';
|
||||
const responseValueIsWithinNorm = responseValue === 'Normi piires';
|
||||
return {
|
||||
status: element.UuringOlek,
|
||||
unit: element.Mootyhik,
|
||||
normLower: elementVastus?.NormAlum?.['#text'],
|
||||
normUpper: elementVastus?.NormYlem?.['#text'],
|
||||
normStatus: elementVastus?.NormiStaatus,
|
||||
responseTime: elementVastus?.VastuseAeg,
|
||||
response_value: responseValueIsNegative || !responseValueIsNumeric ? null : (responseValue ?? null),
|
||||
response_value_is_negative: responseValueIsNumeric ? null : responseValueIsNegative,
|
||||
response_value_is_within_norm: responseValueIsNumeric ? null : responseValueIsWithinNorm,
|
||||
normLowerIncluded: elementVastus?.NormAlum?.['@_kaasaarvatud'] === 'JAH',
|
||||
normUpperIncluded: elementVastus?.NormYlem?.['@_kaasaarvatud'] === 'JAH',
|
||||
analysisElementOriginalId: element.UuringId,
|
||||
};
|
||||
});
|
||||
})(),
|
||||
labComment,
|
||||
//originalResponseElement: elementResponse.original_response_element ?? null,
|
||||
unit: elementResponse.unit,
|
||||
normLower: elementResponse.norm_lower,
|
||||
normUpper: elementResponse.norm_upper,
|
||||
normStatus: elementResponse.norm_status,
|
||||
responseTime: elementResponse.response_time,
|
||||
responseValue: elementResponse.response_value,
|
||||
responseValueIsNegative: elementResponse.response_value_is_negative === true,
|
||||
responseValueIsWithinNorm: elementResponse.response_value_is_within_norm === true,
|
||||
normLowerIncluded: elementResponse.norm_lower_included,
|
||||
normUpperIncluded: elementResponse.norm_upper_included,
|
||||
status: elementResponse.status,
|
||||
analysisElementOriginalId: elementResponse.analysis_element_original_id,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// @TODO unused currently
|
||||
async getUserAnalyses(): Promise<UserAnalysis | null> {
|
||||
const authUser = await this.client.auth.getUser();
|
||||
const { data, error: userError } = authUser;
|
||||
|
||||
if (userError) {
|
||||
console.error('Failed to get user', userError);
|
||||
throw userError;
|
||||
}
|
||||
|
||||
const { user } = data;
|
||||
|
||||
const { data: analysisResponses } = await this.client
|
||||
.schema('medreport')
|
||||
.from('analysis_responses')
|
||||
.select('*')
|
||||
.eq('user_id', user.id);
|
||||
|
||||
if (!analysisResponses) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const analysisResponseIds = analysisResponses.map((r) => r.id);
|
||||
|
||||
const { data: analysisResponseElements } = await this.client
|
||||
.schema('medreport')
|
||||
.from('analysis_response_elements')
|
||||
.select('*')
|
||||
.in('analysis_response_id', analysisResponseIds);
|
||||
|
||||
if (!analysisResponseElements) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return analysisResponses.map((r) => ({
|
||||
...r,
|
||||
elements: analysisResponseElements.filter(
|
||||
(e) => e.analysis_response_id === r.id,
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
async hasAccountTeamMembership(accountId?: string) {
|
||||
if (!accountId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { count, error } = await this.client
|
||||
.schema('medreport')
|
||||
.from('accounts_memberships')
|
||||
.select('account_id', { count: 'exact', head: true })
|
||||
.eq('account_id', accountId);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (count ?? 0) > 0;
|
||||
}
|
||||
|
||||
async fetchBmiThresholds() {
|
||||
// Fetch BMI
|
||||
const { data, error } = await this.client
|
||||
.schema('medreport')
|
||||
.from('bmi_thresholds')
|
||||
.select(
|
||||
'age_min,age_max,underweight_max,normal_min,normal_max,overweight_min,strong_min,obesity_min',
|
||||
)
|
||||
.order('age_min', { ascending: true });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching BMI thresholds:', error);
|
||||
throw error;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export function createUserAnalysesApi(client: SupabaseClient<Database>) {
|
||||
return new UserAnalysesApi(client);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Tables } from '@kit/supabase/database';
|
||||
|
||||
export type AnalysisOrder = Tables<{ schema: 'medreport' }, 'analysis_orders'>;
|
||||
140
packages/features/user-analyses/src/types/analysis-results.ts
Normal file
140
packages/features/user-analyses/src/types/analysis-results.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
export type UserAnalysisElement =
|
||||
Database['medreport']['Tables']['analysis_response_elements']['Row'];
|
||||
export type UserAnalysisResponse =
|
||||
Database['medreport']['Tables']['analysis_responses']['Row'] & {
|
||||
elements: UserAnalysisElement[];
|
||||
};
|
||||
export type UserAnalysis = UserAnalysisResponse[];
|
||||
|
||||
const ElementSchema = z.object({
|
||||
unit: z.string(),
|
||||
norm_lower: z.number(),
|
||||
norm_upper: z.number(),
|
||||
norm_status: z.number(),
|
||||
analysis_name: z.string(),
|
||||
response_time: z.string(),
|
||||
response_value: z.number(),
|
||||
response_value_is_negative: z.boolean(),
|
||||
response_value_is_within_norm: z.boolean(),
|
||||
norm_lower_included: z.boolean(),
|
||||
norm_upper_included: z.boolean(),
|
||||
status: z.string(),
|
||||
analysis_element_original_id: z.string(),
|
||||
original_response_element: z.object({
|
||||
|
||||
}),
|
||||
});
|
||||
|
||||
const OrderSchema = z.object({
|
||||
status: z.string(),
|
||||
medusa_order_id: z.string(),
|
||||
created_at: z.coerce.date(),
|
||||
});
|
||||
|
||||
const DoctorAnalysisFeedbackSchema = z.object({
|
||||
id: z.number(),
|
||||
status: z.string(),
|
||||
user_id: z.string(),
|
||||
created_at: z.coerce.date(),
|
||||
created_by: z.string(),
|
||||
});
|
||||
|
||||
const SummarySchema = z.object({
|
||||
id: z.number(),
|
||||
value: z.string(),
|
||||
status: z.string(),
|
||||
user_id: z.string(),
|
||||
created_at: z.coerce.date(),
|
||||
created_by: z.string(),
|
||||
updated_at: z.coerce.date().nullable(),
|
||||
updated_by: z.string(),
|
||||
doctor_user_id: z.string().nullable(),
|
||||
analysis_order_id: z.number(),
|
||||
doctor_analysis_feedback: z.array(DoctorAnalysisFeedbackSchema),
|
||||
});
|
||||
|
||||
export const AnalysisResultDetailsSchema = z.object({
|
||||
id: z.number(),
|
||||
analysis_order_id: z.number(),
|
||||
order_number: z.string(),
|
||||
order_status: z.string(),
|
||||
user_id: z.string(),
|
||||
created_at: z.coerce.date(),
|
||||
updated_at: z.coerce.date().nullable(),
|
||||
elements: z.array(ElementSchema),
|
||||
order: OrderSchema,
|
||||
summary: SummarySchema.nullable(),
|
||||
});
|
||||
export type AnalysisResultDetails = z.infer<typeof AnalysisResultDetailsSchema>;
|
||||
|
||||
export type AnalysisResultDetailsElementResults = {
|
||||
unit: string | null;
|
||||
normLower: number | null;
|
||||
normUpper: number | null;
|
||||
normStatus: number | null;
|
||||
responseTime: string | null;
|
||||
responseValue: number | null;
|
||||
responseValueIsNegative: boolean | null;
|
||||
responseValueIsWithinNorm: boolean | null;
|
||||
normLowerIncluded: boolean;
|
||||
normUpperIncluded: boolean;
|
||||
status: string;
|
||||
analysisElementOriginalId: string;
|
||||
nestedElements: {
|
||||
analysisElementOriginalId: string;
|
||||
normLower?: number | null;
|
||||
normLowerIncluded: boolean;
|
||||
normStatus: number;
|
||||
normUpper?: number | null;
|
||||
normUpperIncluded: boolean;
|
||||
responseTime: string;
|
||||
responseValue: number;
|
||||
status: number;
|
||||
unit: string;
|
||||
}[];
|
||||
labComment?: string | null;
|
||||
};
|
||||
|
||||
export type AnalysisResultDetailsElement = {
|
||||
analysisIdOriginal: string;
|
||||
isWaitingForResults: boolean;
|
||||
analysisName: string;
|
||||
results: AnalysisResultDetailsElementResults;
|
||||
};
|
||||
|
||||
export type AnalysisResultDetailsMapped = {
|
||||
id: number;
|
||||
order: {
|
||||
status: string;
|
||||
medusaOrderId: string;
|
||||
createdAt: Date | string;
|
||||
};
|
||||
elements: {
|
||||
id: string;
|
||||
unit: string;
|
||||
norm_lower: number;
|
||||
norm_upper: number;
|
||||
norm_status: number;
|
||||
analysis_name: string;
|
||||
response_time: string;
|
||||
response_value: number;
|
||||
norm_lower_included: boolean;
|
||||
norm_upper_included: boolean;
|
||||
status: string;
|
||||
analysis_element_original_id: string;
|
||||
}[];
|
||||
orderedAnalysisElementIds: number[];
|
||||
orderedAnalysisElements: AnalysisResultDetailsElement[];
|
||||
summary: {
|
||||
id: number;
|
||||
status: string;
|
||||
user_id: string;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
value?: string;
|
||||
} | null;
|
||||
};
|
||||
11
packages/features/user-analyses/tsconfig.json
Normal file
11
packages/features/user-analyses/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
|
||||
"paths": {
|
||||
"~/lib/utils": ["../../../lib/utils.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["*.ts", "*.tsx", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -16,7 +16,8 @@
|
||||
"./events": "./src/events/index.tsx",
|
||||
"./components/*": "./src/components/*.tsx",
|
||||
"./registry": "./src/registry/index.ts",
|
||||
"./config": "./src/config/index.ts"
|
||||
"./config": "./src/config/index.ts",
|
||||
"./types/*": "./src/types/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function SelectAnalysisPackage({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card key={title}>
|
||||
<Card key={title} className="flex flex-col">
|
||||
<CardHeader className="relative">
|
||||
{description && (
|
||||
<ButtonTooltip
|
||||
@@ -103,7 +103,7 @@ export default function SelectAnalysisPackage({
|
||||
className="max-h-48 w-full opacity-10"
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1 text-center">
|
||||
<CardContent className="space-y-2 text-center">
|
||||
<PackageHeader
|
||||
title={title}
|
||||
tagColor="bg-cyan"
|
||||
@@ -113,7 +113,7 @@ export default function SelectAnalysisPackage({
|
||||
/>
|
||||
<CardDescription>{subtitle}</CardDescription>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<CardFooter className="mt-auto">
|
||||
<Button
|
||||
className="w-full text-[10px] sm:text-sm"
|
||||
onClick={handleSelect}
|
||||
|
||||
132
packages/shared/src/types/medipost-analysis.ts
Normal file
132
packages/shared/src/types/medipost-analysis.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
export const NormStatus: Record<number, string> = {
|
||||
0: 'NORMAL',
|
||||
1: 'WARNING',
|
||||
2: 'REQUIRES_ATTENTION',
|
||||
} as const;
|
||||
|
||||
export const AnalysisOrderStatus = {
|
||||
1: 'QUEUED',
|
||||
2: 'ON_HOLD',
|
||||
3: 'PROCESSING',
|
||||
4: 'COMPLETED',
|
||||
5: 'REJECTED',
|
||||
6: 'CANCELLED',
|
||||
} as const;
|
||||
|
||||
export type UuringuVastus = {
|
||||
VastuseVaartus: string; // numeric or text like 'Negatiivne'
|
||||
VastuseAeg: string;
|
||||
NormYlem?: {
|
||||
'#text': number;
|
||||
'@_kaasaarvatud': string;
|
||||
}; // 0..1
|
||||
NormAlum?: {
|
||||
'#text': number;
|
||||
'@_kaasaarvatud': string;
|
||||
};
|
||||
NormiStaatus: keyof typeof NormStatus;
|
||||
ProoviJarjenumber: number;
|
||||
};
|
||||
|
||||
export type UuringElement = {
|
||||
UuringIdOID: string;
|
||||
UuringId: string;
|
||||
TLyhend: string;
|
||||
KNimetus: string;
|
||||
UuringNimi: string;
|
||||
TellijaUuringId: number;
|
||||
TeostajaUuringId: string;
|
||||
UuringOlek: keyof typeof AnalysisOrderStatus;
|
||||
Mootyhik?: string; // 0..1
|
||||
Kood: {
|
||||
HkKood: number;
|
||||
HkKoodiKordaja: number;
|
||||
Koefitsient: number;
|
||||
Hind: number;
|
||||
};
|
||||
UuringuVastus?: UuringuVastus | UuringuVastus[]; // 0..n
|
||||
UuringuKommentaar?: string;
|
||||
}
|
||||
|
||||
export type ResponseUuring = {
|
||||
UuringuElement: UuringElement;
|
||||
UuringuTaitjaAsutuseJnr: number;
|
||||
};
|
||||
|
||||
export type ResponseUuringuGrupp = {
|
||||
UuringuGruppId: string;
|
||||
UuringuGruppNimi: string;
|
||||
Uuring: ResponseUuring | ResponseUuring[]; // 1..n
|
||||
};
|
||||
|
||||
export interface IMedipostResponseXMLBase {
|
||||
'?xml': {
|
||||
'@_version': string;
|
||||
'@_encoding': string;
|
||||
'@_standalone': 'yes' | 'no';
|
||||
};
|
||||
ANSWER?: { CODE: number };
|
||||
}
|
||||
|
||||
export type MedipostOrderResponse = IMedipostResponseXMLBase & {
|
||||
Saadetis: {
|
||||
Pais: {
|
||||
Pakett: {
|
||||
'#text': string;
|
||||
'@_versioon': string;
|
||||
};
|
||||
Saatja: string;
|
||||
Saaja: string;
|
||||
Aeg: string;
|
||||
SaadetisId: string;
|
||||
Email: string;
|
||||
};
|
||||
Vastus?: {
|
||||
ValisTellimuseId: string;
|
||||
Asutus: {
|
||||
'@_tyyp': string; // TEOSTAJA
|
||||
'@_jarjenumber': string;
|
||||
AsutuseId: number;
|
||||
AsutuseNimi: string;
|
||||
AsutuseKood: string;
|
||||
AllyksuseNimi?: string;
|
||||
Telefon: number;
|
||||
}[]; //@_tyyp = TELLIJA 1..1, @_tyyp = TEOSTAJA 1..n
|
||||
Personal: {
|
||||
'@_tyyp': string;
|
||||
'@_jarjenumber': string;
|
||||
PersonalOID: string;
|
||||
PersonalKood: string;
|
||||
PersonalPerekonnaNimi: string;
|
||||
PersonalEesNimi: string;
|
||||
Telefon: number;
|
||||
};
|
||||
Patsient: {
|
||||
IsikukoodiOID: string;
|
||||
Isikukood: string;
|
||||
PerekonnaNimi: string;
|
||||
EesNimi: string;
|
||||
SynniAeg: string;
|
||||
SuguOID: string;
|
||||
Sugu: string;
|
||||
};
|
||||
Proov: Array<{
|
||||
ProovinouIdOID: string;
|
||||
ProovinouId: string;
|
||||
MaterjaliTyypOID: string;
|
||||
MaterjaliTyyp: number;
|
||||
MaterjaliNimi: string;
|
||||
Ribakood: string;
|
||||
Jarjenumber: number;
|
||||
VotmisAeg: string;
|
||||
SaabumisAeg: string;
|
||||
}>;
|
||||
TellimuseNumber: string;
|
||||
TellimuseOlek: keyof typeof AnalysisOrderStatus;
|
||||
UuringuGrupp?: ResponseUuringuGrupp | ResponseUuringuGrupp[];
|
||||
};
|
||||
Tellimus?: {
|
||||
ValisTellimuseId: string;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -206,14 +206,7 @@ class AuthCallbackService {
|
||||
return;
|
||||
}
|
||||
|
||||
// If user already has Medusa account, we're done
|
||||
if (accountData?.medusa_account_id) {
|
||||
console.log('Keycloak user already has Medusa account:', accountData.medusa_account_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const { medusaLoginOrRegister } = await import('../../features/medusa-storefront/src/lib/data/customer');
|
||||
|
||||
const medusaAccountId = await medusaLoginOrRegister({
|
||||
email: user.email,
|
||||
supabaseUserId: user.id,
|
||||
@@ -221,20 +214,24 @@ class AuthCallbackService {
|
||||
lastName: accountData?.last_name ?? '-',
|
||||
});
|
||||
|
||||
// Update the account with the Medusa account ID
|
||||
const { error: updateError } = await this.client
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.update({ medusa_account_id: medusaAccountId })
|
||||
.eq('primary_owner_user_id', user.id)
|
||||
.eq('is_personal_account', true);
|
||||
const currentMedusaAccountId = accountData?.medusa_account_id;
|
||||
if (!currentMedusaAccountId || currentMedusaAccountId !== medusaAccountId) {
|
||||
const { error: updateError } = await this.client
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.update({ medusa_account_id: medusaAccountId })
|
||||
.eq('primary_owner_user_id', user.id)
|
||||
.eq('is_personal_account', true);
|
||||
|
||||
if (updateError) {
|
||||
console.error('Error updating account with Medusa ID:', updateError);
|
||||
return;
|
||||
if (updateError) {
|
||||
console.error('Error updating account with Medusa ID:', updateError);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Successfully set up Medusa account for Keycloak user:', medusaAccountId);
|
||||
} else {
|
||||
console.log('Keycloak user already has Medusa account:', accountData.medusa_account_id);
|
||||
}
|
||||
|
||||
console.log('Successfully set up Medusa account for Keycloak user:', medusaAccountId);
|
||||
} catch (error) {
|
||||
console.error('Error setting up Medusa account for Keycloak user:', error);
|
||||
}
|
||||
|
||||
@@ -688,8 +688,11 @@ export type Database = {
|
||||
norm_upper: number | null
|
||||
norm_upper_included: boolean | null
|
||||
original_response_element: Json
|
||||
response_time: string
|
||||
response_value: number
|
||||
response_time: string | null
|
||||
response_value: number | null
|
||||
response_value_is_negative: boolean | null
|
||||
response_value_is_within_norm: boolean | null
|
||||
status: string | null
|
||||
unit: string | null
|
||||
updated_at: string | null
|
||||
}
|
||||
@@ -706,8 +709,11 @@ export type Database = {
|
||||
norm_upper?: number | null
|
||||
norm_upper_included?: boolean | null
|
||||
original_response_element: Json
|
||||
response_time: string
|
||||
response_value: number
|
||||
response_time?: string | null
|
||||
response_value?: number | null
|
||||
response_value_is_negative?: boolean | null
|
||||
response_value_is_within_norm?: boolean | null
|
||||
status?: string | null
|
||||
unit?: string | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
@@ -724,8 +730,11 @@ export type Database = {
|
||||
norm_upper?: number | null
|
||||
norm_upper_included?: boolean | null
|
||||
original_response_element?: Json
|
||||
response_time?: string
|
||||
response_value?: number
|
||||
response_time?: string | null
|
||||
response_value?: number | null
|
||||
response_value_is_negative?: boolean | null
|
||||
response_value_is_within_norm?: boolean | null
|
||||
status?: string | null
|
||||
unit?: string | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
@@ -1373,6 +1382,8 @@ export type Database = {
|
||||
has_analysis_results: boolean
|
||||
has_error: boolean
|
||||
id: string
|
||||
medipost_external_order_id: string | null
|
||||
medipost_private_message_id: string | null
|
||||
medusa_order_id: string | null
|
||||
response_xml: string | null
|
||||
xml: string | null
|
||||
@@ -1383,6 +1394,8 @@ export type Database = {
|
||||
has_analysis_results?: boolean
|
||||
has_error?: boolean
|
||||
id?: string
|
||||
medipost_external_order_id?: string | null
|
||||
medipost_private_message_id?: string | null
|
||||
medusa_order_id?: string | null
|
||||
response_xml?: string | null
|
||||
xml?: string | null
|
||||
@@ -1393,6 +1406,8 @@ export type Database = {
|
||||
has_analysis_results?: boolean
|
||||
has_error?: boolean
|
||||
id?: string
|
||||
medipost_external_order_id?: string | null
|
||||
medipost_private_message_id?: string | null
|
||||
medusa_order_id?: string | null
|
||||
response_xml?: string | null
|
||||
xml?: string | null
|
||||
|
||||
Reference in New Issue
Block a user