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, ¬e, &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} }