Files
cpone_midleware/internal/repository/lab_result.go

436 lines
15 KiB
Go

package repository
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
)
type LabResultOutcome struct {
HTTPStatus int
Success bool
Status string
Message string
Data *LabResultData
}
type LabResultData struct {
Lab LabResultHeader `json:"lab"`
Patient LabResultPatient `json:"patient"`
Doctor LabResultDoctor `json:"doctor"`
Groups []LabResultGroup `json:"groups"`
}
type LabResultHeader struct {
LabNumber string `json:"lab_number"`
TrxDepartemenID int64 `json:"trx_departemen_id"`
RegistrationID *string `json:"registration_id"`
Tanggal *string `json:"tanggal"`
Status string `json:"status"`
VerifiedAt *string `json:"verified_at"`
AuthorizedAt *string `json:"authorized_at"`
Catatan *string `json:"catatan"`
}
type LabResultPatient struct {
MedrecID *string `json:"medrec_id"`
Nama *string `json:"nama"`
TanggalLahir *string `json:"tanggal_lahir"`
JenisKelamin *string `json:"jenis_kelamin"`
}
type LabResultDoctor struct {
PenanggungJawab *string `json:"penanggung_jawab"`
Pengirim *string `json:"pengirim"`
}
type LabResultGroup struct {
GroupID *string `json:"group_id"`
GroupName *string `json:"group_name"`
Items []LabResultItem `json:"items"`
}
type LabResultItem struct {
TrxLayananID int64 `json:"trx_layanan_id"`
ServiceID string `json:"service_id"`
ServiceCode *string `json:"service_code"`
Name *string `json:"name"`
Result *string `json:"result"`
ReferenceValue *string `json:"reference_value"`
Unit *string `json:"unit"`
CriticalFlag string `json:"critical_flag"`
ValueType *string `json:"value_type"`
Note *string `json:"note"`
}
type labResultRecord struct {
TrxDepartemenID int64
TrxID string
RegID sql.NullString
Tanggal sql.NullString
Authorized sql.NullString
AuthorizedAt sql.NullString
Catatan sql.NullString
MedrecID sql.NullString
Referrer sql.NullString
PatientName sql.NullString
BirthDate sql.NullString
Gender sql.NullString
ResponsibleDoctor sql.NullString
SenderDoctor sql.NullString
}
type rawLabResultItem struct {
LabResultItem
GroupID sql.NullString
GroupName sql.NullString
FlagHide sql.NullString
DisplayResult sql.NullFloat64
UnitSnapshot sql.NullString
UnitName sql.NullString
VerifiedAt sql.NullString
AnalyzerID sql.NullString
MethodID sql.NullString
}
func (r MySQLLayananRepository) GetLabResult(ctx context.Context, labNumber string) (LabResultOutcome, error) {
lab, found, err := r.findLabResult(ctx, labNumber)
if err != nil {
return LabResultOutcome{}, err
}
if !found {
return labResultOutcome(404, false, "lab_result_not_found", "Hasil laboratorium tidak ditemukan", nil), nil
}
if lab.Authorized.String != "Y" {
return labResultOutcome(409, false, "lab_result_not_released", "Hasil laboratorium belum diotorisasi", nil), nil
}
items, err := r.findVerifiedLabResultItems(ctx, lab)
if err != nil {
return LabResultOutcome{}, err
}
groups := make([]LabResultGroup, 0)
groupIndexes := make(map[string]int)
var verifiedAt *string
for _, item := range items {
if item.VerifiedAt.Valid && (verifiedAt == nil || item.VerifiedAt.String > *verifiedAt) {
value := item.VerifiedAt.String
verifiedAt = &value
}
key := "__ungrouped"
if item.GroupID.Valid {
key = item.GroupID.String
}
index, exists := groupIndexes[key]
if !exists {
index = len(groups)
groupIndexes[key] = index
groups = append(groups, LabResultGroup{
GroupID: labNullStringPtr(item.GroupID), GroupName: labNullStringPtr(item.GroupName), Items: make([]LabResultItem, 0),
})
}
groups[index].Items = append(groups[index].Items, item.LabResultItem)
}
sender := labNullStringPtr(lab.SenderDoctor)
if sender == nil {
sender = labNullStringPtr(lab.Referrer)
}
data := &LabResultData{
Lab: LabResultHeader{
LabNumber: lab.TrxID, TrxDepartemenID: lab.TrxDepartemenID,
RegistrationID: labNullStringPtr(lab.RegID), Tanggal: labNullStringPtr(lab.Tanggal),
Status: "authorized", VerifiedAt: verifiedAt,
AuthorizedAt: labNullStringPtr(lab.AuthorizedAt), Catatan: labNullStringPtr(lab.Catatan),
},
Patient: LabResultPatient{
MedrecID: labNullStringPtr(lab.MedrecID), Nama: labNullStringPtr(lab.PatientName),
TanggalLahir: labNullStringPtr(lab.BirthDate), JenisKelamin: labNullStringPtr(lab.Gender),
},
Doctor: LabResultDoctor{PenanggungJawab: labNullStringPtr(lab.ResponsibleDoctor), Pengirim: sender},
Groups: groups,
}
return labResultOutcome(200, true, "lab_result_found", "Hasil laboratorium ditemukan", data), nil
}
func (r MySQLLayananRepository) findLabResult(ctx context.Context, labNumber string) (labResultRecord, bool, error) {
row := r.db.QueryRowContext(ctx, `
SELECT td.TrxDepartemenID, td.TrxID, td.RegID,
DATE_FORMAT(td.Tanggal, '%Y-%m-%d %H:%i:%s'), td.DiOtorisasi,
DATE_FORMAT(td.DiOtorisasiTanggal, '%Y-%m-%d %H:%i:%s'), td.Catatan,
rp.MEDRECID, rp.Perujuk, mr.Nama, DATE_FORMAT(mr.TanggalLahir, '%Y-%m-%d'), mr.Kelamin,
dpj.Nama, dpk.Nama
FROM trxdepartemen AS td
INNER JOIN departemen AS dep ON dep.DepartemenID = td.DepartemenID
LEFT JOIN regpas AS rp ON rp.RegID = td.RegID
LEFT JOIN medrec AS mr ON mr.MEDRECID = rp.MEDRECID
LEFT JOIN dokter AS dpj ON dpj.DokterID = td.DokterID
LEFT JOIN dokter AS dpk ON dpk.DokterID = td.Dokter2
WHERE td.TrxID = ? AND td.NA = 'N' AND dep.NamaModulExternal = 'LABORATORIUM'
LIMIT 1`, labNumber)
var lab labResultRecord
err := row.Scan(
&lab.TrxDepartemenID, &lab.TrxID, &lab.RegID, &lab.Tanggal, &lab.Authorized,
&lab.AuthorizedAt, &lab.Catatan, &lab.MedrecID, &lab.Referrer, &lab.PatientName,
&lab.BirthDate, &lab.Gender, &lab.ResponsibleDoctor, &lab.SenderDoctor,
)
if err == sql.ErrNoRows {
return labResultRecord{}, false, nil
}
return lab, err == nil, err
}
func (r MySQLLayananRepository) findVerifiedLabResultItems(ctx context.Context, lab labResultRecord) ([]rawLabResultItem, error) {
processColumns, err := r.tableColumns(ctx, "trxlayanan_proses")
if err != nil {
return nil, err
}
analyzerColumn := findColumn(processColumns, "AnalyzerID", "Lab_AnalyzerID")
methodColumn := findColumn(processColumns, "Lab_MethodID", "MethodID", "MetodeID")
processColumn := findColumn(processColumns, "Proses")
analyzerSelect := "NULL"
if analyzerColumn != "" {
analyzerSelect = "tlp.`" + analyzerColumn + "`"
}
methodSelect := "NULL"
if methodColumn != "" {
methodSelect = "tlp.`" + methodColumn + "`"
}
processJoin := ""
if processColumn != "" {
processJoin = " AND tlp.`" + processColumn + "` = 'Y'"
}
query := fmt.Sprintf(`
SELECT tl.TrxLayananID, tl.LayananID, ml.LayananKode, ml.Nama,
ml.GroupLayananID, gl.Nama, lml.FlagHideCetak, lts.Display_Hasil,
tll.Nilai, tll.NilaiNormalKet, tll.Satuan, sl.Nama,
tll.CriticalFlag, tll.CatatanHasil, tll.TipeNilai,
DATE_FORMAT(tll.HasilDiverifikasiTanggal, '%%Y-%%m-%%d %%H:%%i:%%s'),
%s, %s
FROM trxlayanan AS tl
INNER JOIN trxlayananlaboratorium AS tll ON tll.TrxLayananID = tl.TrxLayananID
INNER JOIN masterlayanan AS ml ON ml.LayananID = tl.LayananID
LEFT JOIN grouplayanan AS gl ON gl.GroupLayananID = ml.GroupLayananID
LEFT JOIN lab_masterlayanan AS lml ON lml.LayananID = tl.LayananID
LEFT JOIN lab_trxlayanan_setup AS lts ON lts.TrxLayananID = tl.TrxLayananID
LEFT JOIN trxlayanan_penunjang AS tlp_header ON tlp_header.TrxLayananID = tl.TrxLayananID
LEFT JOIN trxlayanan_proses AS tlp ON tlp.TrxLayananID = tl.TrxLayananID%s
LEFT JOIN satuanlab AS sl ON sl.SatuanID = tll.Satuan
WHERE tl.TrxDepartemenID = ? AND tl.NA = 'N' AND tl.HeaderDetail > 0
AND tll.HasilDiverifikasi = 'Y'
ORDER BY tl.PaketID, tlp_header.LayananIDHeader, tl.Urutan, tl.LayananID, tl.TrxLayananID`,
analyzerSelect, methodSelect, processJoin,
)
rows, err := r.db.QueryContext(ctx, query, lab.TrxDepartemenID)
if err != nil {
return nil, err
}
result := make([]rawLabResultItem, 0)
seen := make(map[int64]struct{})
for rows.Next() {
var item rawLabResultItem
var serviceCode, name, resultValue, referenceValue, criticalFlag, note, valueType sql.NullString
err := rows.Scan(
&item.TrxLayananID, &item.ServiceID, &serviceCode, &name,
&item.GroupID, &item.GroupName, &item.FlagHide, &item.DisplayResult,
&resultValue, &referenceValue, &item.UnitSnapshot, &item.UnitName,
&criticalFlag, &note, &valueType, &item.VerifiedAt, &item.AnalyzerID, &item.MethodID,
)
if err != nil {
return nil, err
}
if _, duplicate := seen[item.TrxLayananID]; duplicate {
continue
}
seen[item.TrxLayananID] = struct{}{}
if strings.EqualFold(strings.TrimSpace(item.FlagHide.String), "Y") && item.DisplayResult.Valid && item.DisplayResult.Float64 > 0 {
continue
}
item.ServiceCode = labNullStringPtr(serviceCode)
item.Name = labNullStringPtr(name)
item.Result = labNullStringPtr(resultValue)
item.ReferenceValue = labNullStringPtr(referenceValue)
item.Unit = labNullStringPtr(item.UnitName)
if item.Unit == nil {
item.Unit = labNullStringPtr(item.UnitSnapshot)
}
item.CriticalFlag = strings.TrimSpace(criticalFlag.String)
item.ValueType = labNullStringPtr(valueType)
item.Note = labNullStringPtr(note)
result = append(result, item)
}
if err := rows.Err(); err != nil {
_ = rows.Close()
return nil, err
}
if err := rows.Close(); err != nil {
return nil, err
}
for index := range result {
if result[index].ReferenceValue != nil && result[index].Unit != nil {
continue
}
fallbackReference, fallbackUnit, err := r.findLabReferenceFallback(ctx, lab, result[index])
if err != nil {
return nil, err
}
if result[index].ReferenceValue == nil {
result[index].ReferenceValue = fallbackReference
}
if result[index].Unit == nil {
result[index].Unit = fallbackUnit
}
}
return result, nil
}
func (r MySQLLayananRepository) findLabReferenceFallback(ctx context.Context, lab labResultRecord, item rawLabResultItem) (*string, *string, error) {
if !lab.BirthDate.Valid || !lab.Tanggal.Valid {
return nil, nil, nil
}
birthDate, err := time.Parse("2006-01-02", lab.BirthDate.String)
if err != nil {
return nil, nil, nil
}
transactionDate, err := time.Parse("2006-01-02 15:04:05", lab.Tanggal.String)
if err != nil || birthDate.After(transactionDate) {
return nil, nil, nil
}
ageCode := labAgeCode(birthDate, transactionDate)
columns, err := r.tableColumns(ctx, "lab_reference")
if err != nil {
return nil, nil, err
}
analyzerColumn := findColumn(columns, "Lab_AnalyzerID", "AnalyzerID")
methodColumn := findColumn(columns, "Lab_MethodID", "MethodID", "MetodeID")
naColumn := findColumn(columns, "NA")
unitColumn := findColumn(columns, "Satuan", "SatuanID")
unitSelect := "NULL"
if unitColumn != "" {
unitSelect = "`" + unitColumn + "`"
}
var builder strings.Builder
builder.WriteString("SELECT NilaiNormalKet, NilaiNormal1, NilaiNormal2, " + unitSelect + " FROM lab_reference WHERE LayananID = ? AND KelaminID = ? AND TipeNilai = 'Normal' AND UsiaDari <= ? AND UsiaSampai >= ?")
args := []any{item.ServiceID, lab.Gender.String, ageCode, ageCode}
if naColumn != "" {
builder.WriteString(" AND `" + naColumn + "` = 'N'")
}
if analyzerColumn != "" && item.AnalyzerID.Valid && strings.TrimSpace(item.AnalyzerID.String) != "" {
builder.WriteString(" AND `" + analyzerColumn + "` = ?")
args = append(args, item.AnalyzerID.String)
}
if methodColumn != "" && item.MethodID.Valid && strings.TrimSpace(item.MethodID.String) != "" {
builder.WriteString(" AND `" + methodColumn + "` = ?")
args = append(args, item.MethodID.String)
}
builder.WriteString(" ORDER BY UsiaDari DESC LIMIT 1")
var description, minimum, maximum, unitID sql.NullString
err = r.db.QueryRowContext(ctx, builder.String(), args...).Scan(&description, &minimum, &maximum, &unitID)
if err == sql.ErrNoRows {
return nil, nil, nil
}
if err != nil {
return nil, nil, err
}
referenceValue := formatLabReferenceValue(description, minimum, maximum)
if !unitID.Valid || strings.TrimSpace(unitID.String) == "" {
return referenceValue, nil, nil
}
unit := strings.TrimSpace(unitID.String)
var unitName sql.NullString
err = r.db.QueryRowContext(ctx, "SELECT Nama FROM satuanlab WHERE SatuanID = ? LIMIT 1", unit).Scan(&unitName)
if err != nil && err != sql.ErrNoRows {
return nil, nil, err
}
if unitName.Valid && strings.TrimSpace(unitName.String) != "" {
return referenceValue, trimmedStringPtr(unitName.String), nil
}
return referenceValue, &unit, nil
}
func (r MySQLLayananRepository) tableColumns(ctx context.Context, tableName string) ([]string, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT COLUMN_NAME FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = ?
ORDER BY ORDINAL_POSITION`, tableName)
if err != nil {
return nil, err
}
defer rows.Close()
columns := make([]string, 0)
for rows.Next() {
var column string
if err := rows.Scan(&column); err != nil {
return nil, err
}
columns = append(columns, column)
}
return columns, rows.Err()
}
func findColumn(columns []string, candidates ...string) string {
for _, candidate := range candidates {
for _, column := range columns {
if strings.EqualFold(column, candidate) {
return column
}
}
}
return ""
}
func labAgeCode(birthDate, transactionDate time.Time) string {
years := transactionDate.Year() - birthDate.Year()
if birthDate.AddDate(years, 0, 0).After(transactionDate) {
years--
}
afterYears := birthDate.AddDate(years, 0, 0)
months := 0
for months < 11 && !afterYears.AddDate(0, months+1, 0).After(transactionDate) {
months++
}
afterMonths := afterYears.AddDate(0, months, 0)
days := int(transactionDate.Sub(afterMonths).Hours() / 24)
return fmt.Sprintf("%03d%02d%02d", years, months, days)
}
func formatLabReferenceValue(description, minimum, maximum sql.NullString) *string {
if value := labNullStringPtr(description); value != nil {
return value
}
minimumValue := labNullStringPtr(minimum)
maximumValue := labNullStringPtr(maximum)
if minimumValue != nil && maximumValue != nil {
return trimmedStringPtr(*minimumValue + " - " + *maximumValue)
}
if minimumValue != nil {
return minimumValue
}
return maximumValue
}
func trimmedStringPtr(value string) *string {
value = strings.TrimSpace(value)
if value == "" {
return nil
}
return &value
}
func labNullStringPtr(value sql.NullString) *string {
if !value.Valid {
return nil
}
return trimmedStringPtr(value.String)
}
func labResultOutcome(httpStatus int, success bool, status, message string, data *LabResultData) LabResultOutcome {
return LabResultOutcome{HTTPStatus: httpStatus, Success: success, Status: status, Message: message, Data: data}
}