diff --git a/src/controllers/diagnosis.ts b/src/controllers/diagnosis.ts index 7b93984..1f74242 100644 --- a/src/controllers/diagnosis.ts +++ b/src/controllers/diagnosis.ts @@ -17,6 +17,88 @@ const log: Logger = new Logger({ type: "pretty", }); +const DIAGNOSIS_NAME_COLUMNS = [ + "Diagnosis.diagnosis", + "Diagnosis.diagnosis_ind", + "Diagnosis.diagnosis_pt", + "Diagnosis.diagnosis_tet", +] as const; + +const MIN_DIAGNOSIS_SEARCH_TOKEN_LENGTH = 2; + +function stripLikeWildcards(value: string): string { + return value.replace(/^%+|%+$/g, "").trim(); +} + +function parseDiagnosisSearchTokens(raw: unknown): string[] { + const text = stripLikeWildcards(String(raw ?? "")); + if (!text) { + return []; + } + return text + .split(/\s+/) + .map((t) => t.trim()) + .filter((t) => t.length >= MIN_DIAGNOSIS_SEARCH_TOKEN_LENGTH); +} + +function applyLocalizedDiagnosisFields( + diagnosis: Diagnosis, + param: { diagnosis: string; diagnosis_ind?: string; diagnosis_pt?: string; diagnosis_tet?: string }, +) { + diagnosis.diagnosis = param.diagnosis; + diagnosis.diagnosis_ind = param.diagnosis_ind?.trim() || param.diagnosis; + diagnosis.diagnosis_pt = param.diagnosis_pt?.trim() || param.diagnosis; + diagnosis.diagnosis_tet = param.diagnosis_tet?.trim() || param.diagnosis; +} + +type DiagnosisQueryBuilder = { + where: (sql: string, params?: Record) => unknown; + orWhere: (sql: string, params?: Record) => unknown; + andWhere: (sql: string, params?: Record) => unknown; +}; + +function addDiagnosisNameTokenSearch( + qb: DiagnosisQueryBuilder, + raw: unknown, + op: "where" | "orWhere", + paramPrefix: string, +): void { + const tokens = parseDiagnosisSearchTokens(raw); + if (tokens.length === 0) { + const phrase = `%${stripLikeWildcards(String(raw ?? ""))}%`; + (qb as any)[op]( + new Brackets((inner) => { + DIAGNOSIS_NAME_COLUMNS.forEach((col, idx) => { + const key = `${paramPrefix}_phrase_${idx}`; + const method = idx === 0 ? "where" : "orWhere"; + (inner as any)[method](`${col} ILIKE :${key}`, { [key]: phrase }); + }); + }), + ); + return; + } + + (qb as any)[op]( + new Brackets((tokenGroup) => { + tokens.forEach((token, tokenIdx) => { + const tokenKey = `${paramPrefix}_tok_${tokenIdx}`; + const tokenMethod = tokenIdx === 0 ? "andWhere" : "andWhere"; + (tokenGroup as any)[tokenMethod]( + new Brackets((colGroup) => { + DIAGNOSIS_NAME_COLUMNS.forEach((col, colIdx) => { + const colKey = `${tokenKey}_col_${colIdx}`; + const colMethod = colIdx === 0 ? "where" : "orWhere"; + (colGroup as any)[colMethod](`${col} ILIKE :${colKey}`, { + [colKey]: `%${token}%`, + }); + }); + }), + ); + }); + }), + ); +} + export class DiagnosisController { static async list(req: Request, res: Response, next: NextFunction): Promise { /* @@ -25,7 +107,7 @@ export class DiagnosisController { "bearerAuth": [] }] #swagger.parameters['filter'] = { - description: 'Filter: JSON object; Diagnosis.code, Diagnosis.diagnosis, and Diagnosis.type are combined with OR; other fields use AND.', + description: 'Filter JSON. Diagnosis.code, Diagnosis.diagnosis (multi-word token AND on diagnosis, diagnosis_ind, diagnosis_pt, diagnosis_tet), and Diagnosis.type are OR-combined; other fields use AND.', in: 'query', type: 'string', } @@ -113,7 +195,11 @@ export class DiagnosisController { (qb as any)[op](`${colExpr} = :${paramKey}`, { [paramKey]: f }); }; if (orCode !== undefined) addOr("Diagnosis.code", "diag_or_code", orCode); - if (orDiagnosis !== undefined) addOr("Diagnosis.diagnosis", "diag_or_diagnosis", orDiagnosis); + if (orDiagnosis !== undefined) { + const op = isFirst ? "where" : "orWhere"; + isFirst = false; + addDiagnosisNameTokenSearch(qb as DiagnosisQueryBuilder, orDiagnosis, op, "diag_or_diagnosis"); + } if (orType !== undefined) addOr("Diagnosis.type", "diag_or_type", orType, true); }), ); @@ -169,6 +255,9 @@ export class DiagnosisController { try { const schema = Joi.object().keys({ diagnosis: Joi.string().max(256).required().label("Diagnosis"), + diagnosis_ind: Joi.string().max(256).allow("", null).optional().label("Diagnosis (Indonesian)"), + diagnosis_pt: Joi.string().max(256).allow("", null).optional().label("Diagnosis (Portuguese)"), + diagnosis_tet: Joi.string().max(256).allow("", null).optional().label("Diagnosis (Tetum)"), code: Joi.string().max(128).required().label("Code"), status: Joi.string().required().label("Status"), }); @@ -182,7 +271,7 @@ export class DiagnosisController { if (exist) throw { message: "Diagnosis " + Language.lang.failed_duplicate }; let diagnosis = new Diagnosis(); - diagnosis.diagnosis = param.diagnosis; + applyLocalizedDiagnosisFields(diagnosis, param); diagnosis.code = param.code; diagnosis.status = param.status; diagnosis.created_by = req.auth?.data.name; @@ -269,6 +358,9 @@ export class DiagnosisController { const schema = Joi.object().keys({ id: Joi.string().uuid().required().label("Diagnosis ID"), diagnosis: Joi.string().max(256).required().label("Diagnosis"), + diagnosis_ind: Joi.string().max(256).allow("", null).optional().label("Diagnosis (Indonesian)"), + diagnosis_pt: Joi.string().max(256).allow("", null).optional().label("Diagnosis (Portuguese)"), + diagnosis_tet: Joi.string().max(256).allow("", null).optional().label("Diagnosis (Tetum)"), code: Joi.string().max(128).required().label("Code"), status: Joi.string().required().label("Status"), }); @@ -287,7 +379,7 @@ export class DiagnosisController { if (!diagnosis) throw { message: "Diagnosis " + Language.lang.failed_not_found }; - diagnosis.diagnosis = param.diagnosis; + applyLocalizedDiagnosisFields(diagnosis, param); diagnosis.code = param.code; diagnosis.status = param.status; diagnosis.updated_by = req.auth?.data.name; diff --git a/src/swagger/builder.js b/src/swagger/builder.js index 8d5eae9..bc21c2d 100644 --- a/src/swagger/builder.js +++ b/src/swagger/builder.js @@ -114,7 +114,10 @@ const doc = { $status: "Y", }, diagnosis: { - $diagnosis: "Diagnosis name (ICD-11)", + $diagnosis: "Diagnosis name English (ICD-11)", + $diagnosis_ind: "Diagnosis name Indonesian", + $diagnosis_pt: "Diagnosis name Portuguese", + $diagnosis_tet: "Diagnosis name Tetum", $code: "ABC123", $status: "Y", },