Split glass report into three tables

This commit is contained in:
Dwi Swandhana
2026-04-20 05:15:17 +07:00
parent f301de8cb5
commit 5c5bf0ce19
4 changed files with 454 additions and 79 deletions
+322 -34
View File
@@ -549,6 +549,298 @@ class ReportController extends Controller
->pluck('rekapantibiotik.antibiotic')
->toArray();
}
private function getGlassReportHeaders() {
$tableC = [
'ID Rumah Sakit',
'Nama Pasien',
'No Rekam Medis',
'Jenis Kelamin',
'Tanggal Lahir',
'Usia',
'Ruang Rawat',
'Tanggal Pasien Masuk',
'Tanggal Pengambilan Spesimen',
'Specimen Origin',
'Jenis Spesimen',
];
$tableAAntibiotics = [
'Cefoxitin',
'Oxacillin',
'Penicillin',
'Ampicillin',
'Amoxicillin',
'Cefazolin',
'Cefuroxime',
'Cefixime',
'Cefotaxime',
'Ceftriaxone',
'Ceftazidime',
'Cefepime',
'Ceftaroline',
'Ertapenem',
'Imipenem',
'Meropenem',
'Aztreonam',
'Amoxicillin-clavulanate',
'Ampicillin-sulbactam',
'Piperacillin-tazobactam',
'Ceftazidime-avibactam',
'Gentamicin',
'Amikacin',
'Ciprofloxacin',
'Levofloxacin',
'Moxifloxacin',
'Trimethoprim-sulfamethoxazole',
'Azithromycin',
'Clarithromycin',
'Erythromycin',
'Clindamycin',
'Tetracycline',
'Minocycline',
'Doxycycline',
'Colistin',
'Vancomycin',
'Linezolid',
'Tigecycline',
'Daptomycin',
'Nitrofurantoin',
'Fosfomycin',
];
$tableBAntibiotics = [
'Fluconazole',
'Voriconazole',
'Amphotericin B',
'Micafungin',
'Caspofungin',
'Flucytosine',
];
return [
'A' => array_merge($tableC, ['Nama Spesies Bakteri', 'ESBL', 'MRSA'], $tableAAntibiotics),
'B' => array_merge($tableC, ['Nama Spesies Jamur'], $tableBAntibiotics),
'C' => $tableC,
'A_antibiotics' => $tableAAntibiotics,
'B_antibiotics' => $tableBAntibiotics,
];
}
private function getGlassReportAntibioticAliases() {
return [
'Cefoxitin' => ['cefoxitin', 'cefoxitinscreen'],
'Oxacillin' => ['oxacillin'],
'Penicillin' => ['penicillin', 'benzylpenicillin'],
'Ampicillin' => ['ampicillin'],
'Amoxicillin' => ['amoxicillin'],
'Cefazolin' => ['cefazolin', 'cefazolinurine', 'cefazolinother'],
'Cefuroxime' => ['cefuroxime', 'cefuroximeiv', 'cefuroximeoral'],
'Cefixime' => ['cefixime'],
'Cefotaxime' => ['cefotaxime'],
'Ceftriaxone' => ['ceftriaxone'],
'Ceftazidime' => ['ceftazidime'],
'Cefepime' => ['cefepime'],
'Ceftaroline' => ['ceftaroline'],
'Ertapenem' => ['ertapenem'],
'Imipenem' => ['imipenem'],
'Meropenem' => ['meropenem'],
'Aztreonam' => ['aztreonam'],
'Amoxicillin-clavulanate' => ['amoxicillinclavulanate', 'amoxicillinclavulanicacid'],
'Ampicillin-sulbactam' => ['ampicillinsulbactam'],
'Piperacillin-tazobactam' => ['piperacillintazobactam'],
'Ceftazidime-avibactam' => ['ceftazidimeavibactam'],
'Gentamicin' => ['gentamicin'],
'Amikacin' => ['amikacin'],
'Ciprofloxacin' => ['ciprofloxacin'],
'Levofloxacin' => ['levofloxacin'],
'Moxifloxacin' => ['moxifloxacin'],
'Trimethoprim-sulfamethoxazole' => ['trimethoprimsulfamethoxazole', 'trimetrophimesulfamethoxazole'],
'Azithromycin' => ['azithromycin'],
'Clarithromycin' => ['clarithromycin'],
'Erythromycin' => ['erythromycin'],
'Clindamycin' => ['clindamycin'],
'Tetracycline' => ['tetracycline', 'tetracyclinescreenonly', 'tetracyclineu'],
'Minocycline' => ['minocycline'],
'Doxycycline' => ['doxycycline'],
'Colistin' => ['colistin'],
'Vancomycin' => ['vancomycin'],
'Linezolid' => ['linezolid'],
'Tigecycline' => ['tigecycline'],
'Daptomycin' => ['daptomycin'],
'Nitrofurantoin' => ['nitrofurantoin'],
'Fosfomycin' => ['fosfomycin'],
'Fluconazole' => ['fluconazole'],
'Voriconazole' => ['voriconazole'],
'Amphotericin B' => ['amphotericinb'],
'Micafungin' => ['micafungin'],
'Caspofungin' => ['caspofungin'],
'Flucytosine' => ['flucytosine'],
];
}
private function normalizeGlassReportValue($value) {
return preg_replace('/[^a-z0-9]+/', '', strtolower(strip_tags((string) $value)));
}
private function pickGlassReportResistanceValue($row, $header) {
$rawResistance = $row->resistence ?? $row->resistance ?? null;
$rawInterpretation = $row->interpretation ?? null;
$resistance = trim((string) $rawResistance);
$interpretation = trim((string) $rawInterpretation);
if ($resistance === '') {
return $interpretation;
}
$normalizedResistance = $this->normalizeGlassReportValue($resistance);
$normalizedAntibiotic = $this->normalizeGlassReportValue($row->antibiotic ?? '');
$normalizedHeader = $this->normalizeGlassReportValue($header);
if ($normalizedResistance === $normalizedAntibiotic || $normalizedResistance === $normalizedHeader) {
return $interpretation;
}
return $resistance;
}
private function getGlassReportOrigin($row) {
if (empty($row->daftar) || empty($row->mulai)) {
return '';
}
try {
$selisihHari = Carbon::parse($row->daftar)->diffInDays(Carbon::parse($row->mulai));
return $selisihHari >= 3 ? 'Hospital Origin' : 'Community Origin';
} catch (\Throwable $e) {
return '';
}
}
private function buildGlassReportBaseRow($row) {
$jenisSpesimen = trim(implode(' ', array_filter([
$row->kd_spesimen ?? '',
$row->nm_spesimen ?? '',
])));
return [
$row->nmrs ?? '',
$row->nmpasien ?? '',
$row->noregister ?? '',
$row->jkpasien ?? '',
$row->tgllahirpasien ?? '',
$row->usia ?? '',
$row->asalpasien ?? '',
$row->daftar ?? '',
$row->mulai ?? '',
$this->getGlassReportOrigin($row),
$jenisSpesimen,
];
}
private function getGlassReportLookups($rows) {
$orderIds = $rows->pluck('id')->filter()->values()->all();
$accnumbers = $rows->pluck('nofoto')->filter()->values()->all();
$komponen = DB::table('db_komponenjawaban')
->select('accnumber', 'komponen', 'isidata')
->whereIn('accnumber', $accnumbers)
->whereIn('komponen', ['bakteri', 'id_bakteri01', 'id_bakteri02', 'id_mikroorganisme'])
->whereNotNull('isidata')
->where('isidata', '!=', '')
->get()
->groupBy('accnumber');
$antibiotik = RekapAntibiotik::query()
->whereIn('orderid', $orderIds)
->get(['orderid', 'antibiotic', 'resistance', 'interpretation'])
->groupBy('orderid');
return [
'komponen' => $komponen,
'antibiotik' => $antibiotik,
];
}
private function splitGlassReportRows($rows, $lookups) {
$headers = $this->getGlassReportHeaders();
$aliases = $this->getGlassReportAntibioticAliases();
$tables = ['A' => [], 'B' => [], 'C' => []];
foreach ($rows as $row) {
$baseRow = $this->buildGlassReportBaseRow($row);
$komponenRows = $lookups['komponen'][$row->nofoto] ?? collect([]);
$antibioticRows = $lookups['antibiotik'][$row->id] ?? collect([]);
$bacterialName = '';
$fungalName = '';
$foundMorphology = false;
foreach ($komponenRows as $komponenRow) {
$value = trim(strip_tags((string) $komponenRow->isidata));
if ($value === '') {
continue;
}
if (stripos($value, 'Ditemukan morfologi') !== false) {
$foundMorphology = true;
}
if (in_array($komponenRow->komponen, ['bakteri', 'id_bakteri01', 'id_bakteri02'], true) && $bacterialName === '' && stripos($value, 'Ditemukan morfologi') === false) {
$bacterialName = $value;
}
if ($komponenRow->komponen === 'id_mikroorganisme' && $fungalName === '') {
$fungalName = $value;
}
}
$antibioticValues = [];
foreach ($antibioticRows as $antibioticRow) {
$matchedHeader = null;
$normalizedAntibiotic = $this->normalizeGlassReportValue($antibioticRow->antibiotic ?? '');
$normalizedResistance = $this->normalizeGlassReportValue($antibioticRow->resistance ?? '');
foreach ($aliases as $header => $variants) {
if (in_array($normalizedAntibiotic, $variants, true) || in_array($normalizedResistance, $variants, true)) {
$matchedHeader = $header;
break;
}
}
if ($matchedHeader === null || isset($antibioticValues[$matchedHeader])) {
continue;
}
$antibioticValues[$matchedHeader] = $this->pickGlassReportResistanceValue($antibioticRow, $matchedHeader);
}
$hasJenisSpesimen = trim((string) ($baseRow[10] ?? '')) !== '';
if ($bacterialName !== '' && $hasJenisSpesimen) {
$tableRow = array_merge($baseRow, [
$bacterialName,
$row->id_esbl ?? '',
$row->id_mrsa ?? '',
]);
foreach ($headers['A_antibiotics'] as $antibioticHeader) {
$tableRow[] = $antibioticValues[$antibioticHeader] ?? '';
}
$tables['A'][] = $tableRow;
continue;
}
if ($foundMorphology || $fungalName !== '') {
$tableRow = array_merge($baseRow, [
$fungalName !== '' ? $fungalName : 'Ditemukan morfologi',
]);
foreach ($headers['B_antibiotics'] as $antibioticHeader) {
$tableRow[] = $antibioticValues[$antibioticHeader] ?? '';
}
$tables['B'][] = $tableRow;
continue;
}
$tables['C'][] = $baseRow;
}
return $tables;
}
public function genGlassReport(Request $request) {
$bulan = $request->input('bulan');
$tahun = $request->input('tahun');
@@ -574,11 +866,16 @@ class ReportController extends Controller
// 2. Ambil Data Antibiotik HANYA untuk 50 pasien tersebut
$pageIds = $orderbydate->pluck('id')->toArray();
$antibiotikLookup = $this->mapAntibiotikData($pageIds);
$glassLookups = $this->getGlassReportLookups($orderbydate->getCollection());
$glassTables = $this->splitGlassReportRows($orderbydate->getCollection(), $glassLookups);
$glassHeaders = $this->getGlassReportHeaders();
return view('admin.glassreport', [
'orderbydate' => $orderbydate,
'antibiotikLookup' => $antibiotikLookup, // Kirim array hasil mapping
'jsonantibiotik' => $this->listAntibiotik,
'glassTables' => $glassTables,
'glassHeaders' => $glassHeaders,
'bulan' => $bulan,
'tahun' => $tahun
]);
@@ -600,10 +897,12 @@ class ReportController extends Controller
$response = new StreamedResponse(function() use ($bulan, $tahun) {
$handle = fopen('php://output', 'w');
// Header CSV
$staticHeader = ['ID RS', 'Nama Pasien', 'No RM', 'JK', 'Tgl Lahir', 'Usia', 'Ruang', 'Tgl Masuk', 'Tgl Sample', 'Origin', 'Jenis Spesimen', 'Spesies Bakteri', 'ESBL', 'MRSA'];
fputcsv($handle, array_merge($staticHeader, $this->listAntibiotik));
$headers = $this->getGlassReportHeaders();
$tables = [
'A' => [],
'B' => [],
'C' => [],
];
// Query Tanpa Get() tapi Chunk()
$query = Periksa::query()->whereYear('daftar', $tahun);
@@ -612,39 +911,28 @@ class ReportController extends Controller
}
// Proses per 500 data agar RAM stabil
$query->chunk(500, function($rows) use ($handle) {
// Mapping Antibiotik untuk batch 500 ini saja
$chunkIds = $rows->pluck('id')->toArray();
$antibiotikLookup = $this->mapAntibiotikData($chunkIds);
$query->orderBy('id')->chunk(500, function($rows) use (&$tables) {
$lookups = $this->getGlassReportLookups($rows);
$chunkTables = $this->splitGlassReportRows($rows, $lookups);
foreach ($rows as $row) {
$csvRow = [
$row->id_rs ?? '', // Sesuaikan nama kolom
$row->nmpasien,
$row->noregister,
$row->jkpasien,
$row->tgllahirpasien,
$row->usia,
$row->asalpasien,
$row->mulai,
$row->daftar,
'', // Origin
$row->kd_spesimen,
$row->nm_spesimen, // Asumsi nama bakteri disini atau kolom lain
$row->id_esbl,
$row->id_mrsa
];
foreach ($this->listAntibiotik as $headerAntibiotik) {
$nilai = $antibiotikLookup[$row->id][$headerAntibiotik] ?? '';
$csvRow[] = $nilai;
}
fputcsv($handle, $csvRow);
}
foreach (['A', 'B', 'C'] as $tableKey) {
foreach ($chunkTables[$tableKey] as $tableRow) {
$tables[$tableKey][] = $tableRow;
}
}
});
foreach (['A', 'B', 'C'] as $tableKey) {
fputcsv($handle, ['TABEL '.$tableKey]);
fputcsv($handle, $headers[$tableKey]);
foreach ($tables[$tableKey] as $tableRow) {
fputcsv($handle, $tableRow);
}
fputcsv($handle, []);
}
fclose($handle);
});
@@ -9,52 +9,41 @@
<div class="ribbon ribbon-danger">Rekapitulasi Data Bulan {{$bulan}} Tahun {{$tahun}}</div>
<p></p>
<div class="mt-3 mb-3">
<a href="{{ route('exportGlassReport', ['bulan' => $bulan, 'tahun' => $tahun]) }}" class="btn btn-success">
<a href="{{ route('exportGlassReport', ['bulan' => $bulan, 'tahun' => $tahun]) }}" class="btn btn-success">
<i class="fa fa-file-excel-o"></i> Download Full Excel (CSV)
</a>
</div>
<div class="table-responsive">
<table class="table table-bordered" id="datatable">
<thead>
<tr>
<th>Nama Pasien</th>
<th>No RM</th>
<th>Bakteri</th>
@foreach($jsonantibiotik as $antibiotic)
<th title="{{ $antibiotic }}">{{ $antibiotic }}</th>
@endforeach
</tr>
</thead>
<tbody>
@forelse($orderbydate as $data)
@foreach (['A', 'B', 'C'] as $tableKey)
<div class="table-responsive" style="margin-bottom: 24px;">
<h4>TABEL {{ $tableKey }}</h4>
<table class="table table-bordered table-sm">
<thead>
<tr>
<td>{{ $data->nmpasien }}</td>
<td>{{ $data->noregister }}</td>
<td>{{ $data->nm_spesimen }}</td> @foreach($jsonantibiotik as $headerAntibiotik)
@php
// LOGIC UTAMA:
// Cek di array lookup: [id_pasien][nama_antibiotik]
// Jika ada, ambil nilainya. Jika tidak, kosong.
$interpretasi = $antibiotikLookup[$data->id][$headerAntibiotik] ?? '';
@endphp
<td class="text-center" style="{{ $interpretasi == 'R' ? 'background-color:#ffcccc' : '' }}">
{{ $interpretasi }}
</td>
@foreach (($glassHeaders[$tableKey] ?? []) as $header)
<th>{{ $header }}</th>
@endforeach
</tr>
@empty
<tr>
<td colspan="{{ count($jsonantibiotik) + 3 }}">Data tidak ditemukan</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</thead>
<tbody>
@forelse (($glassTables[$tableKey] ?? []) as $row)
<tr>
@foreach ($row as $cell)
<td>{{ $cell }}</td>
@endforeach
</tr>
@empty
<tr>
<td colspan="{{ count($glassHeaders[$tableKey] ?? []) }}">Data tidak ditemukan</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
@endsection
@endsection
@@ -629,7 +629,7 @@ $(document).ready(function () {
type : 'error',
});
} else {
var url = '{{url('/')}}/glassreport?bulan='+bulan+'?tahun='+tahun;
var url = '{{url('/')}}/glassreport?bulan='+bulan+'&tahun='+tahun;
window.location.href = url;
}
});
+105 -7
View File
@@ -402,6 +402,27 @@ def create_genexpert_ack_r01_response(incoming_hl7):
msa = f"MSA|CA|{incoming_control_id}"
return f"{msh}\r{msa}\r"
def format_hl7_date(value):
if not value:
return ""
if isinstance(value, datetime.datetime):
return value.strftime("%Y%m%d")
if isinstance(value, datetime.date):
return value.strftime("%Y%m%d")
text = str(value).strip()
digits = re.sub(r"[^0-9]", "", text)
return digits[:8] if len(digits) >= 8 else ""
def map_hl7_sex(value):
text = str(value or "").strip().upper()
if not text:
return ""
if text.startswith("L") or "LAKI" in text or text == "M":
return "M"
if text.startswith("P") or "PEREM" in text or text == "F":
return "F"
return ""
def create_genexpert_rsp_z02_response(orders, incoming_hl7, ip_addr=None):
qpd_segment = extract_segment(incoming_hl7, "QPD")
qpd = parse_genexpert_qpd(qpd_segment)
@@ -417,25 +438,45 @@ def create_genexpert_rsp_z02_response(orders, incoming_hl7, ip_addr=None):
segments.append(qpd_segment)
for patient_idx, order in enumerate(orders, start=1):
patient_id = str(order.norm or order.rnoreg or "").strip()
sample_id = str(order.rnoreg or "").strip()
order_ts = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
patient_id = sanitize_astm_field(order.norm or order.rnoreg, max_len=50)
sample_id = sanitize_astm_field(order.rnoreg, max_len=50)
order_ts = (
getattr(order, "rtglast", None).strftime('%Y%m%d%H%M%S')
if getattr(order, "rtglast", None)
else datetime.datetime.now().strftime('%Y%m%d%H%M%S')
)
assay_code, assay_source, capability_match = resolve_genexpert_assay(order, ip_addr)
if not assay_code:
print(f"[GENEXPERT] Payload order dilewati rnoreg={sample_id} karena assay kosong.")
continue
first_name, last_name = split_patient_name(order.nama)
patient_name = sanitize_astm_field(f"{last_name}^{first_name}", uppercase=True, max_len=80, allow_component_sep=True)
dob = format_hl7_date(getattr(order, "tgllahir", None))
sex = map_hl7_sex(getattr(order, "rjenis", ""))
address = sanitize_astm_field(getattr(order, "alamat", ""), max_len=120)
room = sanitize_astm_field(getattr(order, "ruangan", ""), uppercase=True, max_len=80)
doctor = sanitize_astm_field(getattr(order, "namadok", ""), max_len=80)
requested_test_name = sanitize_astm_field(getattr(order, "tes", ""), max_len=120)
specimen_type = sanitize_astm_field(
getattr(order, "nm_spesimen", "") or getattr(order, "kd_spesimen", "") or "ORH",
uppercase=True,
max_len=40
)
print(
f"[GENEXPERT-DEBUG] Build RSP rnoreg={sample_id}, ip={ip_addr}, "
f"patient_id={patient_id}, assay_code={assay_code}, assay_source={assay_source}, "
f"capability_match={capability_match}, query_name='{query_name}', query_tag='{query_tag}'"
f"capability_match={capability_match}, patient_name='{patient_name}', dob='{dob}', "
f"sex='{sex}', room='{room}', doctor='{doctor}', specimen_type='{specimen_type}', "
f"requested_test_name='{requested_test_name}', query_name='{query_name}', query_tag='{query_tag}'"
)
segments.append(f"PID|{patient_idx}||{patient_id}")
segments.append(f"PID|{patient_idx}||{patient_id}||{patient_name}||{dob}|{sex}|||{address}")
segments.append(f"ORC|NW|1|||||||{order_ts}")
segments.append(f"OBR|1|||{assay_code}|||||||A")
segments.append(f"OBR|1|||{assay_code}^{requested_test_name}^L|||||||A")
segments.append("TQ1|||||||||R")
segments.append(f"SPM|1|{sample_id}^||ORH|||||||P")
segments.append(f"SPM|1|{sample_id}^{sample_id}||{specimen_type}|||||||P")
return "\r".join(segments) + "\r"
@@ -456,6 +497,7 @@ def send_all_orders(conn, ip_addr, hl7_msg, msg_id, response_framing="mllp"):
print(f"[GENEXPERT] Mengirim {len(orders)} order ke {ip_addr}")
rsp = create_genexpert_rsp_z02_response(orders, hl7_msg, ip_addr=ip_addr)
first_accnumber = str(orders[0].rnoreg or "").strip() if orders else ""
debug_genexpert_order_message(rsp, ip_addr=ip_addr)
send_genexpert_response(conn, ip_addr, rsp, response_framing, label=f"qbp-order:{first_accnumber}")
for order in orders:
@@ -553,6 +595,62 @@ def send_genexpert_response(conn, ip_addr, hl7_message, framing, label=""):
)
conn.sendall(payload)
def debug_genexpert_order_message(hl7_message, ip_addr=None):
segments = parse_hl7_segments(hl7_message)
current_pid = ""
current_patient_name = ""
order_index = 0
for segment in segments:
fields = segment.split("|")
seg_type = fields[0] if fields else ""
if seg_type == "PID":
current_pid = fields[3] if len(fields) > 3 else ""
current_patient_name = fields[5] if len(fields) > 5 else ""
dob = fields[7] if len(fields) > 7 else ""
sex = fields[8] if len(fields) > 8 else ""
address = fields[11] if len(fields) > 11 else ""
print(
f"[GENEXPERT-ORDER-DEBUG] ip={ip_addr}, seg=PID, patient_id='{current_pid}', "
f"patient_name='{current_patient_name}', dob='{dob}', sex='{sex}', "
f"address='{address}', raw='{segment}'"
)
elif seg_type == "ORC":
order_index = fields[2] if len(fields) > 2 else ""
order_time = fields[9] if len(fields) > 9 else ""
print(
f"[GENEXPERT-ORDER-DEBUG] ip={ip_addr}, seg=ORC, placer_order='{order_index}', "
f"order_time='{order_time}', patient_id='{current_pid}', "
f"patient_name='{current_patient_name}', raw='{segment}'"
)
elif seg_type == "OBR":
assay_code = fields[4] if len(fields) > 4 else ""
result_status = fields[11] if len(fields) > 11 else ""
assay_parts = assay_code.split("^")
assay_id = assay_parts[0] if len(assay_parts) > 0 else ""
assay_name = assay_parts[1] if len(assay_parts) > 1 else ""
print(
f"[GENEXPERT-ORDER-DEBUG] ip={ip_addr}, seg=OBR, placer_order='{order_index}', "
f"assay_code='{assay_id}', assay_name='{assay_name}', result_status='{result_status}', "
f"patient_id='{current_pid}', patient_name='{current_patient_name}', raw='{segment}'"
)
elif seg_type == "TQ1":
priority = fields[9] if len(fields) > 9 else ""
print(
f"[GENEXPERT-ORDER-DEBUG] ip={ip_addr}, seg=TQ1, placer_order='{order_index}', "
f"priority='{priority}', patient_id='{current_pid}', "
f"patient_name='{current_patient_name}', raw='{segment}'"
)
elif seg_type == "SPM":
specimen_id = fields[2] if len(fields) > 2 else ""
specimen_type = fields[4] if len(fields) > 4 else ""
print(
f"[GENEXPERT-ORDER-DEBUG] ip={ip_addr}, seg=SPM, placer_order='{order_index}', "
f"specimen_id='{specimen_id}', specimen_type='{specimen_type}', patient_id='{current_pid}', "
f"patient_name='{current_patient_name}', raw='{segment}'"
)
def build_genexpert_result_query(accnumber, msg_control_id):
ts = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
# Query hasil berbasis accession number di QRD-8.