496 lines
16 KiB
TypeScript
496 lines
16 KiB
TypeScript
import { SupabaseClient } from '@supabase/supabase-js';
|
|
|
|
import { getLogger } from '@kit/shared/logger';
|
|
import type { UuringuVastus } from '@kit/shared/types/medipost-analysis';
|
|
import { toArray } from '@kit/shared/utils';
|
|
import { Database } from '@kit/supabase/database';
|
|
|
|
import type {
|
|
AnalysisOrder,
|
|
AnalysisOrderStatus,
|
|
} from '../types/analysis-orders';
|
|
import type {
|
|
AnalysisResultDetailsElement,
|
|
AnalysisResultDetailsMapped,
|
|
AnalysisResultLevel,
|
|
AnalysisResultsDetailsElementNested,
|
|
AnalysisResultsQuery,
|
|
AnalysisStatus,
|
|
UserAnalysis,
|
|
} from '../types/analysis-results';
|
|
|
|
/**
|
|
* 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 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 orderedAnalysisElements = await this.getOrderedAnalysisElements({
|
|
analysisOrderId,
|
|
orderedAnalysisElementIds,
|
|
});
|
|
|
|
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 responseWithElements = await this.getAnalysisResponseWithElements({
|
|
analysisOrderId,
|
|
});
|
|
if (!responseWithElements) {
|
|
return null;
|
|
}
|
|
|
|
const mappedOrderedAnalysisElements =
|
|
await this.getMappedOrderedAnalysisElements({
|
|
analysisResponseElements: responseWithElements.elements,
|
|
orderedAnalysisElements,
|
|
});
|
|
|
|
const feedback =
|
|
responseWithElements.summary?.doctor_analysis_feedback?.[0];
|
|
return {
|
|
id: analysisOrderId,
|
|
order: {
|
|
status: analysisOrder.status,
|
|
medusaOrderId: analysisOrder.medusa_order_id,
|
|
createdAt: analysisOrder.created_at,
|
|
},
|
|
orderedAnalysisElements: mappedOrderedAnalysisElements,
|
|
summary:
|
|
feedback?.status === 'COMPLETED'
|
|
? (responseWithElements.summary?.doctor_analysis_feedback?.[0] ??
|
|
null)
|
|
: null,
|
|
};
|
|
}
|
|
|
|
async getAnalysisResponseWithElements({
|
|
analysisOrderId,
|
|
}: {
|
|
analysisOrderId: number;
|
|
}) {
|
|
const { data, error: userError } = await this.client.auth.getUser();
|
|
if (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,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();
|
|
|
|
return analysisResponse?.[0] as AnalysisResultsQuery | null;
|
|
}
|
|
|
|
async getOrderedAnalysisElements({
|
|
analysisOrderId,
|
|
orderedAnalysisElementIds,
|
|
}: {
|
|
analysisOrderId: number;
|
|
orderedAnalysisElementIds: number[];
|
|
}) {
|
|
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;
|
|
}
|
|
return orderedAnalysisElements;
|
|
}
|
|
|
|
async getMappedOrderedAnalysisElements({
|
|
analysisResponseElements,
|
|
orderedAnalysisElements,
|
|
}: {
|
|
analysisResponseElements: AnalysisResultsQuery['elements'];
|
|
orderedAnalysisElements: {
|
|
analysis_id_original: string;
|
|
analysis_name_lab: string;
|
|
}[];
|
|
}): Promise<AnalysisResultDetailsElement[]> {
|
|
const mappedOrderedAnalysisElements = orderedAnalysisElements
|
|
.map(({ analysis_id_original, analysis_name_lab }) => {
|
|
return this.getOrderedAnalysisElement({
|
|
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('analysis_id_original', 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(
|
|
({ analysis_id_original }) =>
|
|
analysis_id_original === analysisElementOriginalId,
|
|
);
|
|
if (!nestedAnalysisElement) {
|
|
continue;
|
|
}
|
|
|
|
nestedElement.analysisElementOriginalId = analysisElementOriginalId;
|
|
nestedElement.analysisName =
|
|
nestedAnalysisElement.analysis_name_lab as string | undefined;
|
|
}
|
|
results.nestedElements = results.nestedElements.sort(
|
|
(a, b) => a.analysisName?.localeCompare(b.analysisName ?? '') ?? 0,
|
|
);
|
|
}
|
|
} else {
|
|
console.error(
|
|
'Failed to get nested analysis elements by ids=',
|
|
nestedAnalysisElementIds,
|
|
nestedAnalysisElementsError,
|
|
);
|
|
}
|
|
}
|
|
|
|
return mappedOrderedAnalysisElements;
|
|
}
|
|
|
|
getOrderedAnalysisElement({
|
|
analysisIdOriginal,
|
|
analysisNameLab,
|
|
analysisResponseElements,
|
|
}: {
|
|
analysisIdOriginal: string;
|
|
analysisNameLab: string;
|
|
analysisResponseElements: AnalysisResultsQuery['elements'];
|
|
}): AnalysisResultDetailsElement {
|
|
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: ((): AnalysisResultsDetailsElementNested[] => {
|
|
const nestedElements = toArray(
|
|
elementResponse.original_response_element?.UuringuElement,
|
|
);
|
|
return nestedElements.map<AnalysisResultsDetailsElementNested>(
|
|
(element) => {
|
|
const mappedResponse = this.mapUuringVastus({
|
|
uuringVastus: element.UuringuVastus as
|
|
| UuringuVastus
|
|
| undefined,
|
|
});
|
|
return {
|
|
unit: element.Mootyhik ?? null,
|
|
normLower: mappedResponse.normLower,
|
|
normUpper: mappedResponse.normUpper,
|
|
normStatus: mappedResponse.normStatus,
|
|
responseTime: mappedResponse.responseTime,
|
|
responseValue: mappedResponse.responseValue,
|
|
responseValueIsNegative: mappedResponse.responseValueIsNegative,
|
|
responseValueIsWithinNorm:
|
|
mappedResponse.responseValueIsWithinNorm,
|
|
normLowerIncluded: mappedResponse.normLowerIncluded,
|
|
normUpperIncluded: mappedResponse.normUpperIncluded,
|
|
analysisElementOriginalId: element.UuringId,
|
|
status: Number(elementResponse.status) as AnalysisStatus,
|
|
analysisName: undefined,
|
|
};
|
|
},
|
|
);
|
|
})(),
|
|
labComment,
|
|
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 === null
|
|
? null
|
|
: elementResponse.response_value_is_negative === true,
|
|
responseValueIsWithinNorm:
|
|
elementResponse.response_value_is_within_norm === null
|
|
? null
|
|
: elementResponse.response_value_is_within_norm === true,
|
|
normLowerIncluded: elementResponse.norm_lower_included,
|
|
normUpperIncluded: elementResponse.norm_upper_included,
|
|
status: Number(elementResponse.status) as AnalysisStatus,
|
|
analysisElementOriginalId: elementResponse.analysis_element_original_id,
|
|
},
|
|
};
|
|
}
|
|
|
|
mapUuringVastus({ uuringVastus }: { uuringVastus?: UuringuVastus }) {
|
|
const vastuseVaartus = uuringVastus?.VastuseVaartus;
|
|
const responseValue = (() => {
|
|
const valueAsNumber = Number(vastuseVaartus);
|
|
if (isNaN(valueAsNumber)) {
|
|
return null;
|
|
}
|
|
return valueAsNumber;
|
|
})();
|
|
const responseValueNumber = Number(responseValue);
|
|
const responseValueIsNumeric = !isNaN(responseValueNumber);
|
|
const responseValueIsNegative = vastuseVaartus === 'Negatiivne';
|
|
const responseValueIsWithinNorm = vastuseVaartus === 'Normi piires';
|
|
return {
|
|
normLower: uuringVastus?.NormAlum?.['#text'] ?? null,
|
|
normUpper: uuringVastus?.NormYlem?.['#text'] ?? null,
|
|
normStatus: (uuringVastus?.NormiStaatus ??
|
|
null) as AnalysisResultLevel | null,
|
|
responseTime: uuringVastus?.VastuseAeg ?? null,
|
|
responseValue:
|
|
responseValueIsNegative || !responseValueIsNumeric
|
|
? null
|
|
: (responseValueNumber ?? null),
|
|
responseValueIsNegative: responseValueIsNumeric
|
|
? null
|
|
: responseValueIsNegative,
|
|
responseValueIsWithinNorm: responseValueIsNumeric
|
|
? null
|
|
: responseValueIsWithinNorm,
|
|
normLowerIncluded:
|
|
uuringVastus?.NormAlum?.['@_kaasaarvatud'].toLowerCase() === 'jah',
|
|
normUpperIncluded:
|
|
uuringVastus?.NormYlem?.['@_kaasaarvatud'].toLowerCase() === 'jah',
|
|
};
|
|
}
|
|
|
|
// @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;
|
|
}
|
|
|
|
async getAllUserAnalysisResponses(): Promise<
|
|
Database['medreport']['Functions']['get_latest_analysis_response_elements_for_current_user']['Returns']
|
|
> {
|
|
const {
|
|
data: { user },
|
|
} = await this.client.auth.getUser();
|
|
|
|
if (!user) {
|
|
return [];
|
|
}
|
|
|
|
const { data, error } = await this.client
|
|
.schema('medreport')
|
|
.rpc('get_latest_analysis_response_elements_for_current_user', {
|
|
p_user_id: user.id,
|
|
});
|
|
|
|
if (error) {
|
|
console.error('Error fetching user analysis responses: ', error);
|
|
throw error;
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
async updateAnalysisOrderStatus({
|
|
orderId,
|
|
medusaOrderId,
|
|
orderStatus,
|
|
}: {
|
|
orderId?: number;
|
|
medusaOrderId?: string;
|
|
orderStatus: AnalysisOrderStatus;
|
|
}) {
|
|
const logger = await getLogger();
|
|
const orderIdParam = orderId;
|
|
const medusaOrderIdParam = medusaOrderId;
|
|
const ctx = {
|
|
action: 'update-analysis-order-status',
|
|
orderId,
|
|
medusaOrderId,
|
|
orderStatus,
|
|
};
|
|
|
|
logger.info(ctx, 'Updating order');
|
|
if (!orderIdParam && !medusaOrderIdParam) {
|
|
logger.error(ctx, 'Missing orderId or medusaOrderId');
|
|
throw new Error('Either orderId or medusaOrderId must be provided');
|
|
}
|
|
await this.client
|
|
.schema('medreport')
|
|
.rpc('update_analysis_order_status', {
|
|
order_id: orderIdParam ?? -1,
|
|
status_param: orderStatus,
|
|
medusa_order_id_param: medusaOrderIdParam ?? '',
|
|
})
|
|
.throwOnError();
|
|
}
|
|
}
|
|
|
|
export function createUserAnalysesApi(client: SupabaseClient<Database>) {
|
|
return new UserAnalysesApi(client);
|
|
}
|