update
This commit is contained in:
@@ -255,7 +255,7 @@ class ListController extends Controller
|
||||
'kd_spesimen', 'nm_spesimen', 'dlp', 'daftar', 'foto', 'tgldraft', 'nmdrafter', 'baca', 'nmpembaca', 'verifikasi',
|
||||
'export', 'tgladendum', 'nmadendum', 'filefoto', 'pendaftar', 'status', 'created_at', 'updated_at',
|
||||
'bhp_media', 'bhp_potsputum', 'bhp_poturine', 'bhp_oshe', 'bhp_obyekglass', 'bhp_botolbd', 'bhp_parafilm',
|
||||
'bhp_tips', 'bhp_cottonswab', 'bhp_antibiotiktambahan', 'id_esbl', 'id_mrsa', 'rating', 'nmpendaftar',
|
||||
'bhp_tips', 'bhp_cottonswab', 'bhp_antibiotiktambahan', 'id_esbl', 'id_mrsa', 'rating', 'nmpendaftar', 'tanggalregis', 'tanggalsampel', 'pengambilan',
|
||||
|
||||
DB::raw("DATE(daftar) as daftartgl"),
|
||||
DB::raw("TO_CHAR(daftar, 'HH24:MI:SS') as daftarjam"),
|
||||
|
||||
@@ -605,13 +605,14 @@ class AstmMessageService
|
||||
{
|
||||
$normalizedData = str_replace(["\r\n", "\r"], "\n", $response);
|
||||
$segments = explode("\n", $normalizedData);
|
||||
|
||||
$patient_id = '';
|
||||
$patient_name = '';
|
||||
$accession_number = '';
|
||||
$waktu_periksa = date('d-m-Y H:i:s'); // Default jika tidak ada di ASTM
|
||||
|
||||
// Array untuk menyimpan data terstruktur
|
||||
$parsedResults = [];
|
||||
$hasil_list = []; // Tetap dipertahankan untuk kebutuhan legacy/summary
|
||||
$hasil_list = [];
|
||||
|
||||
foreach ($segments as $rsegmen) {
|
||||
$cekdata = explode('|', $rsegmen);
|
||||
@@ -619,32 +620,37 @@ class AstmMessageService
|
||||
|
||||
$recordType = preg_replace("/[^a-zA-Z]/", "", $cekdata[0]);
|
||||
|
||||
// 1. Parsing Pasien
|
||||
if ($recordType === 'P') {
|
||||
$patient_id = $cekdata[3] ?? ($cekdata[4] ?? '');
|
||||
if (isset($cekdata[5])) {
|
||||
$patient_name = trim(str_replace('^', ' ', $cekdata[5]));
|
||||
$patient_id = !empty($cekdata[3]) ? $cekdata[3] : (!empty($cekdata[4]) ? $cekdata[4] : '');
|
||||
|
||||
$raw_name = isset($cekdata[5]) ? trim(str_replace('^', ' ', $cekdata[5])) : '';
|
||||
$patient_name = !empty(trim($raw_name)) ? $raw_name : $patient_id;
|
||||
}
|
||||
|
||||
// 2. Parsing Accession Number
|
||||
if ($recordType === 'O') {
|
||||
$accession_number = !empty($cekdata[2]) ? $cekdata[2] : $accession_number;
|
||||
// Jika ada waktu di record O (index 6), kita bisa format (opsional)
|
||||
if (!empty($cekdata[6])) {
|
||||
$rawTime = preg_replace('/[^0-9]/', '', $cekdata[6]);
|
||||
if (strlen($rawTime) >= 14) {
|
||||
$waktu_periksa = date('d-m-Y H:i:s', strtotime(substr($rawTime, 0, 14)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($recordType === 'O') {
|
||||
$accession_number = $cekdata[2] ?? '';
|
||||
}
|
||||
|
||||
// 3. Parsing Data Result, Ct, dan EndPt
|
||||
if ($recordType === 'R') {
|
||||
if (isset($cekdata[2]) && isset($cekdata[3])) {
|
||||
$test_info_raw = $cekdata[2];
|
||||
$result_val = trim(str_replace('^', '', $cekdata[3]));
|
||||
|
||||
if ($result_val !== '') {
|
||||
// Pecah test info berdasarkan '^'
|
||||
$test_info = explode('^', $test_info_raw);
|
||||
|
||||
// Pada format GeneXpert: index ke-6 biasanya nama target (FII, FII 20210G, dll)
|
||||
// index ke-7 biasanya penanda tipe data (Ct, EndPt)
|
||||
$target_name = $test_info[6] ?? 'Unknown Target';
|
||||
$metric_type = $test_info[7] ?? 'Result'; // Jika tidak ada, berarti ini Result (POS/NEG)
|
||||
$target_name = !empty($test_info[6]) ? $test_info[6] : 'Unknown Target';
|
||||
$metric_type = (!empty($test_info[7])) ? $test_info[7] : 'Result';
|
||||
|
||||
// Inisialisasi array untuk target ini jika belum ada
|
||||
if (!isset($parsedResults[$target_name])) {
|
||||
$parsedResults[$target_name] = [
|
||||
'Result' => '-',
|
||||
@@ -653,12 +659,11 @@ class AstmMessageService
|
||||
];
|
||||
}
|
||||
|
||||
// Masukkan nilai ke dalam struktur data
|
||||
$parsedResults[$target_name][$metric_type] = $result_val;
|
||||
|
||||
// Untuk summary legacy (kolom organism/additional_result)
|
||||
if ($metric_type === 'Result') {
|
||||
$hasil_list[] = $target_name . ": " . $result_val;
|
||||
if ($metric_type === 'Result' && $result_val !== '') {
|
||||
// Simpan semua result ke dalam list untuk kesimpulan
|
||||
$hasil_list[$target_name] = "<strong>{$target_name}:</strong> {$result_val}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -670,19 +675,50 @@ class AstmMessageService
|
||||
return false;
|
||||
}
|
||||
|
||||
// Buat string kesimpulan DULUAN sebelum membuat HTML
|
||||
$kesimpulan_html = implode(" | ", array_values($hasil_list));
|
||||
$kesimpulan_db = strip_tags($kesimpulan_html); // Versi tanpa tag HTML untuk kolom DB
|
||||
$kesimpulan_safe = substr($kesimpulan_db, 0, 100);
|
||||
|
||||
// ====================================================================
|
||||
// MEMBUAT HTML REPORT & GRAFIK (CHART.JS)
|
||||
// ====================================================================
|
||||
|
||||
// 1. Buat Tabel HTML
|
||||
$reporthtml = '<div class="genexpert-report" style="font-family: Arial, sans-serif;">';
|
||||
$reporthtml .= '<h4 style="margin-bottom:10px;">GeneXpert Assay Result</h4>';
|
||||
$reporthtml .= '<table border="1" cellpadding="5" style="width: 100%; border-collapse: collapse; text-align: left; margin-bottom: 20px;">';
|
||||
$reporthtml .= '<tr style="background-color: #f4f4f4;">
|
||||
// Wrapper Utama
|
||||
$reporthtml = '<div class="genexpert-report" style="font-family: Arial, sans-serif; max-width: 900px; margin: auto; border: 1px solid #ddd; padding: 20px; border-radius: 8px;">';
|
||||
$reporthtml .= '<h3 style="text-align: center; margin-top: 0; color: #2c3e50;">Hasil Pemeriksaan GeneXpert</h3>';
|
||||
$reporthtml .= '<hr style="border: 0; border-top: 2px solid #3498db; margin-bottom: 20px;">';
|
||||
|
||||
// Bagian 1: Identitas Pasien (Grid Table)
|
||||
$reporthtml .= '<table style="width: 100%; margin-bottom: 20px; font-size: 14px; border: none;">';
|
||||
$reporthtml .= '<tr>';
|
||||
$reporthtml .= '<td style="width: 15%; padding: 4px 0;"><strong>No. Reg LIS</strong></td>';
|
||||
$reporthtml .= '<td style="width: 35%; padding: 4px 0;">: ' . htmlspecialchars($accession_number) . '</td>';
|
||||
$reporthtml .= '<td style="width: 15%; padding: 4px 0;"><strong>ID Pasien</strong></td>';
|
||||
$reporthtml .= '<td style="width: 35%; padding: 4px 0;">: ' . htmlspecialchars($patient_id) . '</td>';
|
||||
$reporthtml .= '</tr>';
|
||||
$reporthtml .= '<tr>';
|
||||
$reporthtml .= '<td style="padding: 4px 0;"><strong>Nama Pasien</strong></td>';
|
||||
$reporthtml .= '<td style="padding: 4px 0;">: ' . htmlspecialchars($patient_name) . '</td>';
|
||||
$reporthtml .= '<td style="padding: 4px 0;"><strong>Waktu Tes</strong></td>';
|
||||
$reporthtml .= '<td style="padding: 4px 0;">: ' . htmlspecialchars($waktu_periksa) . '</td>';
|
||||
$reporthtml .= '</tr>';
|
||||
$reporthtml .= '</table>';
|
||||
|
||||
// Bagian 2: Kesimpulan / Hasil Utama
|
||||
$reporthtml .= '<div style="background-color: #f8f9fa; padding: 15px; border-left: 5px solid #2ecc71; margin-bottom: 25px; border-radius: 4px;">';
|
||||
$reporthtml .= '<h4 style="margin: 0 0 10px 0; color: #2c3e50;">Kesimpulan Hasil:</h4>';
|
||||
$reporthtml .= '<p style="margin: 0; font-size: 14px; line-height: 1.6;">' . $kesimpulan_html . '</p>';
|
||||
$reporthtml .= '</div>';
|
||||
|
||||
// Bagian 3: Tabel Detail Analit
|
||||
$reporthtml .= '<h4 style="margin-bottom:10px; color: #2c3e50;">Detail Target / Analit</h4>';
|
||||
$reporthtml .= '<table border="1" cellpadding="8" style="width: 100%; border-collapse: collapse; text-align: left; margin-bottom: 25px; font-size: 13px;">';
|
||||
$reporthtml .= '<tr style="background-color: #ecf0f1; color: #2c3e50;">
|
||||
<th>Target/Analyte</th>
|
||||
<th>Result</th>
|
||||
<th>Ct Value</th>
|
||||
<th>EndPt</th>
|
||||
<th>End Point</th>
|
||||
</tr>';
|
||||
|
||||
$chartLabels = [];
|
||||
@@ -697,7 +733,6 @@ class AstmMessageService
|
||||
<td>{$data['EndPt']}</td>
|
||||
</tr>";
|
||||
|
||||
// Siapkan data untuk grafik (hanya ambil target yang memiliki nilai numerik Ct/EndPt)
|
||||
if (is_numeric($data['Ct']) || is_numeric($data['EndPt'])) {
|
||||
$chartLabels[] = $target;
|
||||
$chartCtData[] = is_numeric($data['Ct']) ? (float)$data['Ct'] : 0;
|
||||
@@ -706,27 +741,25 @@ class AstmMessageService
|
||||
}
|
||||
$reporthtml .= '</table>';
|
||||
|
||||
// 2. Buat Grafik menggunakan Chart.js (Inject Script)
|
||||
// Bagian 4: Grafik Chart.js
|
||||
if (!empty($chartLabels)) {
|
||||
$chartId = 'gxChart_' . preg_replace('/[^a-zA-Z0-9]/', '', $accession_number);
|
||||
$labelsJson = json_encode($chartLabels);
|
||||
$ctJson = json_encode($chartCtData);
|
||||
$endPtJson = json_encode($chartEndPtData);
|
||||
|
||||
// Tambahkan Canvas untuk grafik
|
||||
$reporthtml .= '<h4 style="margin-bottom:10px; color: #2c3e50; text-align: center;">Grafik Amplifikasi</h4>';
|
||||
$reporthtml .= '<div style="width: 100%; max-width: 800px; margin: auto;">';
|
||||
$reporthtml .= '<canvas id="'.$chartId.'"></canvas>';
|
||||
$reporthtml .= '</div>';
|
||||
|
||||
// Tambahkan script Chart.js (asumsi CDN) dan inisialisasi
|
||||
$reporthtml .= "
|
||||
<script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>
|
||||
<script>
|
||||
// Gunakan DOMContentLoaded agar script jalan setelah HTML di-render di frontend
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var ctx = document.getElementById('{$chartId}').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'bar', // Menggunakan kombinasi Bar & Line chart
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: {$labelsJson},
|
||||
datasets: [
|
||||
@@ -745,6 +778,7 @@ class AstmMessageService
|
||||
borderColor: 'rgba(255, 99, 132, 1)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
borderWidth: 2,
|
||||
tension: 0.1,
|
||||
yAxisID: 'y1'
|
||||
}
|
||||
]
|
||||
@@ -762,8 +796,8 @@ class AstmMessageService
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'right',
|
||||
title: { display: true, text: 'EndPt' },
|
||||
grid: { drawOnChartArea: false } // Agar garis grid tidak bertumpuk
|
||||
title: { display: true, text: 'End Point' },
|
||||
grid: { drawOnChartArea: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -772,15 +806,12 @@ class AstmMessageService
|
||||
</script>
|
||||
";
|
||||
}
|
||||
$reporthtml .= '</div>';
|
||||
$reporthtml .= '</div>'; // End of Wrapper Utama
|
||||
|
||||
// ====================================================================
|
||||
// SIMPAN KE DATABASE
|
||||
// ====================================================================
|
||||
|
||||
$kesimpulan = implode(" | ", $hasil_list);
|
||||
$kesimpulan_safe = substr($kesimpulan, 0, 100);
|
||||
|
||||
try {
|
||||
Periksa::where('nofoto', $accession_number)
|
||||
->whereNotIn('status', ['Selesai', 'Arsip', 'Dibatalkan (Arsip)', 'Batal'])
|
||||
@@ -788,7 +819,6 @@ class AstmMessageService
|
||||
'status' => 'Data GeneXpert di Terima'
|
||||
]);
|
||||
|
||||
|
||||
Riwayat::create([
|
||||
'nofoto' => $accession_number,
|
||||
'jawaban' => $reporthtml,
|
||||
@@ -797,15 +827,14 @@ class AstmMessageService
|
||||
'verifikasi' => '',
|
||||
]);
|
||||
|
||||
$resultSample = ResultSample::firstOrNew(['accession_number' => $accession_number]);
|
||||
$resultSample->sender_name = $alat;
|
||||
$resultSample->patient_id = substr($patient_id, 0, 50);
|
||||
$resultSample->patient_name_first = substr($patient_name, 0, 100);
|
||||
$resultSample->organism = $kesimpulan_safe; // Tetap menyimpan text summary
|
||||
$resultSample->additional_result = json_encode($parsedResults); // Menyimpan JSON utuh agar lebih informatif
|
||||
$resultSample->message_datetime = date('Y-m-d H:i:s');
|
||||
$resultSample = ResultSample::firstOrNew(['accession_number' => $accession_number]);
|
||||
$resultSample->sender_name = $alat;
|
||||
$resultSample->patient_id = substr($patient_id, 0, 50);
|
||||
$resultSample->patient_name_first = substr($patient_name, 0, 100);
|
||||
$resultSample->organism = $kesimpulan_safe; // Disimpan versi plain text
|
||||
$resultSample->additional_result = json_encode($parsedResults);
|
||||
$resultSample->message_datetime = date('Y-m-d H:i:s');
|
||||
$resultSample->save();
|
||||
Log::info("GeneXpert Result Disimpan", ["NoReg" => $accession_number, "Hasil" => $kesimpulan_safe]);
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
Log::error("GeneXpert DB Save Error: " . $e->getMessage());
|
||||
|
||||
@@ -119,6 +119,18 @@
|
||||
<td>Address</td>
|
||||
<td id="alamat"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tanggal Registrasi</td>
|
||||
<td id="tanggalregis"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tanggal Pengambilan</td>
|
||||
<td id="tanggalsampel"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cara Pengambilan</td>
|
||||
<td id="pengambilan"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td id="nostatus"></td>
|
||||
@@ -5002,6 +5014,9 @@
|
||||
{ name: 'otherppds', type: 'text'},
|
||||
{ name: 'nmpendaftar', type: 'text'},
|
||||
{ name: 'alamatpasien', type: 'text'},
|
||||
{ name: 'tanggalregis', type: 'text'},
|
||||
{ name: 'tanggalsampel', type: 'text'},
|
||||
{ name: 'pengambilan', type: 'text'},
|
||||
],
|
||||
type : 'GET',
|
||||
data : {jenis:jenis, mulai:mulai, akhir:akhir, valcari:valcari, poli:poli, master:master, _token: '{{csrf_token()}}'},
|
||||
@@ -5180,6 +5195,9 @@
|
||||
$("#tlppasien").html(dataRecord.tlppasien);
|
||||
$("#periksa_id").val(dataRecord.id);
|
||||
$("#ruangan").val(dataRecord.idruangan);
|
||||
$("#tanggalregis").val(dataRecord.tanggalregis);
|
||||
$("#tanggalsampel").val(dataRecord.tanggalsampel);
|
||||
$("#pengambilan").val(dataRecord.pengambilan);
|
||||
if (dataRecord.dokter_id == '0' || dataRecord.dokter_id == null || dataRecord.dokter_id == ''){
|
||||
$("#dokter").val(masteru).select2().trigger('change');
|
||||
} else {
|
||||
|
||||
@@ -175,6 +175,18 @@
|
||||
<td>Address</td>
|
||||
<td id="alamat"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tanggal Registrasi</td>
|
||||
<td id="tanggalregis"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tanggal Pengambilan</td>
|
||||
<td id="tanggalsampel"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cara Pengambilan</td>
|
||||
<td id="pengambilan"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td id="nostatus"></td>
|
||||
@@ -5053,6 +5065,9 @@
|
||||
{ name: 'otherppds', type: 'text'},
|
||||
{ name: 'nmpendaftar', type: 'text'},
|
||||
{ name: 'alamatpasien', type: 'text'},
|
||||
{ name: 'tanggalregis', type: 'text'},
|
||||
{ name: 'tanggalsampel', type: 'text'},
|
||||
{ name: 'pengambilan', type: 'text'},
|
||||
],
|
||||
type : 'GET',
|
||||
data : {jenis:jenis, mulai:mulai, akhir:akhir, valcari:valcari, poli:poli, master:master, _token: '{{csrf_token()}}'},
|
||||
@@ -5229,6 +5244,9 @@
|
||||
$("#tlppasien").html(dataRecord.tlppasien);
|
||||
$("#periksa_id").val(dataRecord.id);
|
||||
$("#ruangan").val(dataRecord.idruangan);
|
||||
$("#tanggalregis").val(dataRecord.tanggalregis);
|
||||
$("#tanggalsampel").val(dataRecord.tanggalsampel);
|
||||
$("#pengambilan").val(dataRecord.pengambilan);
|
||||
if (dataRecord.dokter_id == '0' || dataRecord.dokter_id == null || dataRecord.dokter_id == ''){
|
||||
$("#dokter").val(masteru).select2().trigger('change');
|
||||
} else {
|
||||
|
||||
@@ -175,6 +175,18 @@
|
||||
<td>Address</td>
|
||||
<td id="alamat"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tanggal Registrasi</td>
|
||||
<td id="tanggalregis"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Tanggal Pengambilan</td>
|
||||
<td id="tanggalsampel"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cara Pengambilan</td>
|
||||
<td id="pengambilan"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td id="nostatus"></td>
|
||||
@@ -5053,6 +5065,9 @@
|
||||
{ name: 'otherppds', type: 'text'},
|
||||
{ name: 'nmpendaftar', type: 'text'},
|
||||
{ name: 'alamatpasien', type: 'text'},
|
||||
{ name: 'tanggalregis', type: 'text'},
|
||||
{ name: 'tanggalsampel', type: 'text'},
|
||||
{ name: 'pengambilan', type: 'text'},
|
||||
],
|
||||
type : 'GET',
|
||||
data : {jenis:jenis, mulai:mulai, akhir:akhir, valcari:valcari, poli:poli, master:master, _token: '{{csrf_token()}}'},
|
||||
@@ -5229,6 +5244,9 @@
|
||||
$("#tlppasien").html(dataRecord.tlppasien);
|
||||
$("#periksa_id").val(dataRecord.id);
|
||||
$("#ruangan").val(dataRecord.idruangan);
|
||||
$("#tanggalregis").val(dataRecord.tanggalregis);
|
||||
$("#tanggalsampel").val(dataRecord.tanggalsampel);
|
||||
$("#pengambilan").val(dataRecord.pengambilan);
|
||||
if (dataRecord.dokter_id == '0' || dataRecord.dokter_id == null || dataRecord.dokter_id == ''){
|
||||
$("#dokter").val(masteru).select2().trigger('change');
|
||||
} else {
|
||||
|
||||
+203
-33
@@ -59,6 +59,35 @@ def print(*args, **kwargs):
|
||||
_write_thread_log(message)
|
||||
return builtins.print(*args, **kwargs)
|
||||
|
||||
def _visible_bytes(data: bytes) -> str:
|
||||
mapping = {
|
||||
0x02: "<STX>",
|
||||
0x03: "<ETX>",
|
||||
0x04: "<EOT>",
|
||||
0x05: "<ENQ>",
|
||||
0x06: "<ACK>",
|
||||
0x0D: "<CR>",
|
||||
0x0A: "<LF>",
|
||||
0x17: "<ETB>",
|
||||
0x15: "<NAK>",
|
||||
0x1C: "<FS>",
|
||||
0x0B: "<VT>",
|
||||
}
|
||||
parts = []
|
||||
for b in data:
|
||||
if b in mapping:
|
||||
parts.append(mapping[b])
|
||||
elif 32 <= b <= 126:
|
||||
parts.append(chr(b))
|
||||
else:
|
||||
parts.append(f"<0x{b:02X}>")
|
||||
return "".join(parts)
|
||||
|
||||
def _hex_bytes(data: bytes, limit: int = 160) -> str:
|
||||
clipped = data[:limit]
|
||||
text = clipped.hex().upper()
|
||||
return text + ("..." if len(data) > limit else "")
|
||||
|
||||
# ==========================================
|
||||
# 1. KONFIGURASI SISTEM
|
||||
# ==========================================
|
||||
@@ -481,7 +510,7 @@ def send_all_orders_astm(conn, ip_addr, astm_msg, response_framing="astm"):
|
||||
|
||||
base_orders = session.query(PaslabOrder).filter(
|
||||
(flag_attr == False) | (flag_attr == None)
|
||||
).order_by(PaslabOrder.urut.asc()).all()
|
||||
).order_by(PaslabOrder.rtglast.desc().nullslast(), PaslabOrder.urut.desc()).all()
|
||||
|
||||
selected_orders = []
|
||||
if requested_sample_id and requested_sample_id.upper() != "ALL":
|
||||
@@ -802,20 +831,21 @@ def handle_genexpert_client(conn, addr):
|
||||
# Decode ke string
|
||||
temp_str = full_message_bytes.decode('latin-1', errors='ignore')
|
||||
|
||||
# --- SANITIZING (PEMBERSIHAN) ---
|
||||
# Cari MSH pertama
|
||||
if "MSH|" in temp_str:
|
||||
msh_index = temp_str.find("MSH|")
|
||||
clean_hl7 = temp_str[msh_index:]
|
||||
elif response_framing == "astm":
|
||||
clean_hl7 = extract_astm_frame_text(full_message_bytes)
|
||||
else:
|
||||
clean_hl7 = temp_str.strip("\x0b\x1c\r\n")
|
||||
|
||||
if clean_hl7:
|
||||
if response_framing == "astm":
|
||||
pending_astm_hl7 = clean_hl7
|
||||
pending_astm_framing = response_framing
|
||||
log_genexpert_handshake(addr[0], "ASTM-MSG-STORED", detail=f"len={len(clean_hl7)}")
|
||||
continue
|
||||
process_genexpert_hl7_message(conn, addr[0], clean_hl7, response_framing)
|
||||
else:
|
||||
# Jika pesan lengkap tapi tidak ada MSH (misal cuma EOT doang)
|
||||
pass
|
||||
else:
|
||||
if buffer:
|
||||
head_hex = buffer[:12].hex()
|
||||
@@ -957,10 +987,16 @@ def parse_genexpert_astm_records(astm_string, device_name):
|
||||
tgl_data=datetime.datetime.now().date(),
|
||||
rawdt=astm_string, # Simpan data mentahnya (utuh) ke TEXT untuk jaga-jaga/bisa dibaca ulang
|
||||
organisme=kesimpulan_safe, # Hasil yang sudah bersih dan muat
|
||||
alat=device_name,
|
||||
processed='N' # Atau menyesuaikan default sistem Anda
|
||||
alat=device_name
|
||||
)
|
||||
session.add(new_result)
|
||||
source_ip = str(device_name or "").replace("GeneXpert-", "").strip()
|
||||
flag_name = get_flag_by_device(source_ip)
|
||||
if flag_name:
|
||||
order = session.query(PaslabOrder).filter(PaslabOrder.rnoreg == seq_no_safe).first()
|
||||
if order:
|
||||
setattr(order, flag_name, True)
|
||||
print(f"[GENEXPERT] Hasil ASTM diterima, set {flag_name}=TRUE untuk {seq_no_safe}")
|
||||
session.commit()
|
||||
print(f"[GENEXPERT-DB-SUCCESS] Hasil lab untuk Order {seq_no_safe} berhasil disimpan ke LisPhoenix!")
|
||||
|
||||
@@ -1026,7 +1062,7 @@ def get_genexpert_query_orders(ip_addr, hl7_msg):
|
||||
|
||||
base_orders = session.query(PaslabOrder).filter(
|
||||
(flag_attr == False) | (flag_attr == None)
|
||||
).order_by(PaslabOrder.urut.asc()).all()
|
||||
).order_by(PaslabOrder.rtglast.desc().nullslast(), PaslabOrder.urut.desc()).all()
|
||||
|
||||
requested_sample_id = (param_2 or param_1).strip() if (param_2 or param_1) else ""
|
||||
if requested_sample_id and requested_sample_id.upper() != "ALL":
|
||||
@@ -1049,24 +1085,25 @@ def get_genexpert_query_orders(ip_addr, hl7_msg):
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def build_genexpert_response_msh(message_code, incoming_hl7, resp_control_id):
|
||||
def build_genexpert_response_msh(message_code, incoming_hl7, resp_control_id, ip_addr=None):
|
||||
timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
|
||||
msh_fields = extract_segment(incoming_hl7, "MSH").split('|')
|
||||
sender_app = msh_fields[2] if len(msh_fields) > 2 else "GeneXpert"
|
||||
sender_fac = msh_fields[3] if len(msh_fields) > 3 else ""
|
||||
return f"MSH|^~\\&|LIS||{sender_app}|{sender_fac}|{timestamp}||{message_code}|{resp_control_id}|P|2.5|||NE|NE"
|
||||
host_app = get_genexpert_host_application(ip_addr)
|
||||
return f"MSH|^~\\&|{host_app}||{sender_app}|{sender_fac}|{timestamp}||{message_code}|{resp_control_id}|P|2.5|||NE|NE"
|
||||
|
||||
def create_genexpert_ack_j01_response(incoming_hl7):
|
||||
def create_genexpert_ack_j01_response(incoming_hl7, ip_addr=None):
|
||||
incoming_control_id = extract_msg_control_id(incoming_hl7) or "UNKNOWN"
|
||||
resp_control_id = f"ACK{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||
msh = build_genexpert_response_msh("ACK^J01", incoming_hl7, resp_control_id)
|
||||
msh = build_genexpert_response_msh("ACK^J01", incoming_hl7, resp_control_id, ip_addr=ip_addr)
|
||||
msa = f"MSA|CA|{incoming_control_id}"
|
||||
return f"{msh}\r{msa}\r"
|
||||
|
||||
def create_genexpert_ack_r01_response(incoming_hl7):
|
||||
def create_genexpert_ack_r01_response(incoming_hl7, ip_addr=None):
|
||||
incoming_control_id = extract_msg_control_id(incoming_hl7) or "UNKNOWN"
|
||||
resp_control_id = f"ACK{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||
msh = build_genexpert_response_msh("ACK^R01", incoming_hl7, resp_control_id)
|
||||
msh = build_genexpert_response_msh("ACK^R01", incoming_hl7, resp_control_id, ip_addr=ip_addr)
|
||||
msa = f"MSA|CA|{incoming_control_id}"
|
||||
return f"{msh}\r{msa}\r"
|
||||
|
||||
@@ -1081,7 +1118,7 @@ def create_genexpert_rsp_z02_response(orders, incoming_hl7, ip_addr=None):
|
||||
if str(incoming_message_type or "").startswith("QBP^Z03"):
|
||||
query_name = "Z03^HOST QUERY"
|
||||
|
||||
msh = build_genexpert_response_msh("RSP^Z02", incoming_hl7, resp_control_id)
|
||||
msh = build_genexpert_response_msh("RSP^Z02", incoming_hl7, resp_control_id, ip_addr=ip_addr)
|
||||
msa = f"MSA|AA|{query_tag}"
|
||||
qak = f"QAK|{query_tag}|OK|{query_name}"
|
||||
segments = [msh, msa, qak]
|
||||
@@ -1186,10 +1223,27 @@ def detect_genexpert_message_framing(message_bytes):
|
||||
raw = message_bytes or b""
|
||||
if b"\x1c" in raw or raw.startswith(b"\x0b"):
|
||||
return "mllp"
|
||||
if b"\x03" in raw:
|
||||
if b"\x03" in raw or b"\x17" in raw:
|
||||
return "astm"
|
||||
return "plain"
|
||||
|
||||
def extract_astm_frame_text(frame_bytes):
|
||||
raw = frame_bytes or b""
|
||||
if not raw.startswith(b"\x02"):
|
||||
return ""
|
||||
|
||||
etx_pos = raw.find(b"\x03")
|
||||
etb_pos = raw.find(b"\x17")
|
||||
if etx_pos != -1 and etb_pos != -1:
|
||||
end_pos = min(etx_pos, etb_pos)
|
||||
else:
|
||||
end_pos = max(etx_pos, etb_pos)
|
||||
|
||||
if end_pos == -1 or end_pos < 2:
|
||||
return ""
|
||||
|
||||
return raw[2:end_pos].decode("latin-1", errors="ignore")
|
||||
|
||||
def frame_genexpert_response(hl7_message, framing):
|
||||
message = str(hl7_message or "")
|
||||
if framing == "astm":
|
||||
@@ -1200,6 +1254,30 @@ def frame_genexpert_response(hl7_message, framing):
|
||||
return f"\x0b{message}\x1c\r".encode("utf-8")
|
||||
return message.encode("utf-8")
|
||||
|
||||
def build_genexpert_astm_frames(hl7_message, max_text_bytes=240):
|
||||
message = str(hl7_message or "")
|
||||
text_bytes = message.encode("latin-1")
|
||||
chunks = [text_bytes[i:i + max_text_bytes] for i in range(0, len(text_bytes), max_text_bytes)] or [b""]
|
||||
frames = []
|
||||
frame_number = 1
|
||||
|
||||
for index, chunk in enumerate(chunks):
|
||||
is_last = index == len(chunks) - 1
|
||||
terminator = b"\x03" if is_last else b"\x17"
|
||||
frame_no_byte = str(frame_number).encode("ascii")
|
||||
frame_core_bytes = frame_no_byte + chunk + terminator
|
||||
checksum = calculate_astm_checksum(frame_core_bytes.decode("latin-1", errors="ignore")).encode("ascii")
|
||||
full_frame = b"\x02" + frame_core_bytes + checksum + b"\x0D\x0A"
|
||||
frames.append({
|
||||
"frame_number": frame_number,
|
||||
"is_last": is_last,
|
||||
"chunk_len": len(chunk),
|
||||
"payload": full_frame,
|
||||
"checksum": checksum.decode("ascii", errors="ignore"),
|
||||
})
|
||||
frame_number = (frame_number + 1) % 8
|
||||
return frames
|
||||
|
||||
def recv_genexpert_control_char(conn, timeout_seconds=5):
|
||||
previous_timeout = conn.gettimeout()
|
||||
try:
|
||||
@@ -1208,8 +1286,10 @@ def recv_genexpert_control_char(conn, timeout_seconds=5):
|
||||
finally:
|
||||
conn.settimeout(previous_timeout)
|
||||
|
||||
def send_genexpert_astm_frame(conn, ip_addr, payload, label=""):
|
||||
def send_genexpert_astm_frame(conn, ip_addr, hl7_message, label=""):
|
||||
try:
|
||||
frames = build_genexpert_astm_frames(hl7_message)
|
||||
print(f"[GENEXPERT-ASTM-TX] ip={ip_addr}, label={label}, total_frames={len(frames)}, mode=astm_active")
|
||||
conn.sendall(b"\x05")
|
||||
log_genexpert_handshake(ip_addr, "ENQ-TX", detail=f"label={label}")
|
||||
|
||||
@@ -1223,25 +1303,46 @@ def send_genexpert_astm_frame(conn, ip_addr, payload, label=""):
|
||||
log_genexpert_handshake(ip_addr, "CTRL-RX", detail=f"phase=pre-frame,label={label},hex={ctrl.hex() if ctrl else 'timeout'}")
|
||||
return False
|
||||
|
||||
conn.sendall(payload)
|
||||
log_genexpert_handshake(ip_addr, "FRAME-TX", detail=f"label={label},bytes={len(payload)}")
|
||||
for frame in frames:
|
||||
payload = frame["payload"]
|
||||
debug_genexpert_astm_frame(ip_addr, payload, direction="TX", label=f"{label}:frame{frame['frame_number']}")
|
||||
conn.sendall(payload)
|
||||
log_genexpert_handshake(
|
||||
ip_addr,
|
||||
"FRAME-TX",
|
||||
detail=(
|
||||
f"label={label},frame_no={frame['frame_number']},bytes={len(payload)},"
|
||||
f"chunk_len={frame['chunk_len']},last={frame['is_last']}"
|
||||
),
|
||||
)
|
||||
|
||||
ctrl = recv_genexpert_control_char(conn, timeout_seconds=5)
|
||||
if ctrl == b"\x06":
|
||||
log_genexpert_handshake(ip_addr, "ACK-RX", detail=f"phase=post-frame,label={label}")
|
||||
ctrl = recv_genexpert_control_char(conn, timeout_seconds=15)
|
||||
if ctrl == b"\x06":
|
||||
log_genexpert_handshake(ip_addr, "ACK-RX", detail=f"phase=post-frame,label={label},frame_no={frame['frame_number']}")
|
||||
continue
|
||||
if ctrl == b"\x15":
|
||||
log_genexpert_handshake(ip_addr, "NAK-RX", detail=f"phase=post-frame,label={label},frame_no={frame['frame_number']}")
|
||||
conn.sendall(b"\x04")
|
||||
log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label},after=nak,frame_no={frame['frame_number']}")
|
||||
return False
|
||||
if ctrl == b"\x04":
|
||||
log_genexpert_handshake(ip_addr, "EOT-RX", detail=f"phase=post-frame,label={label},frame_no={frame['frame_number']}")
|
||||
conn.sendall(b"\x04")
|
||||
log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label},after-peer-eot,frame_no={frame['frame_number']}")
|
||||
return False
|
||||
|
||||
log_genexpert_handshake(
|
||||
ip_addr,
|
||||
"CTRL-RX",
|
||||
detail=f"phase=post-frame,label={label},frame_no={frame['frame_number']},hex={ctrl.hex() if ctrl else 'timeout'}",
|
||||
)
|
||||
conn.sendall(b"\x04")
|
||||
log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label}")
|
||||
return True
|
||||
if ctrl == b"\x15":
|
||||
log_genexpert_handshake(ip_addr, "NAK-RX", detail=f"phase=post-frame,label={label}")
|
||||
conn.sendall(b"\x04")
|
||||
log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label},after=nak")
|
||||
log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label},after=unexpected,frame_no={frame['frame_number']}")
|
||||
return False
|
||||
|
||||
log_genexpert_handshake(ip_addr, "CTRL-RX", detail=f"phase=post-frame,label={label},hex={ctrl.hex() if ctrl else 'timeout'}")
|
||||
conn.sendall(b"\x04")
|
||||
log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label},after=unexpected")
|
||||
return False
|
||||
log_genexpert_handshake(ip_addr, "EOT-TX", detail=f"label={label}")
|
||||
return True
|
||||
except Exception as exc:
|
||||
log_genexpert_handshake(ip_addr, "ASTM-SEND-ERROR", detail=f"label={label},error={exc}")
|
||||
return False
|
||||
@@ -1263,8 +1364,15 @@ def send_genexpert_response(conn, ip_addr, hl7_message, framing, label=""):
|
||||
f"response_mode={response_mode}, label={label}, bytes={len(payload)}"
|
||||
)
|
||||
if framing == "astm" and response_mode == "astm_active":
|
||||
send_genexpert_astm_frame(conn, ip_addr, payload, label=label)
|
||||
send_genexpert_astm_frame(conn, ip_addr, hl7_message, label=label)
|
||||
return
|
||||
if framing == "astm":
|
||||
debug_genexpert_astm_frame(ip_addr, payload, direction="TX", label=label)
|
||||
else:
|
||||
print(
|
||||
f"[GENEXPERT-FRAME-TX] ip={ip_addr}, label={label}, framing={framing}, "
|
||||
f"hex={_hex_bytes(payload)}, visible={_visible_bytes(payload)}"
|
||||
)
|
||||
conn.sendall(payload)
|
||||
|
||||
def send_genexpert_transport_ack(conn, ip_addr, framing, reason="frame-received"):
|
||||
@@ -1280,6 +1388,46 @@ def log_genexpert_handshake(ip_addr, event, detail=""):
|
||||
suffix = f", detail={detail}" if detail else ""
|
||||
print(f"[GENEXPERT-HANDSHAKE] ip={ip_addr}, event={event}{suffix}")
|
||||
|
||||
def debug_genexpert_astm_frame(ip_addr, frame_bytes, direction="RX", label=""):
|
||||
raw = frame_bytes or b""
|
||||
suffix = f", label={label}" if label else ""
|
||||
if not raw:
|
||||
print(f"[GENEXPERT-ASTM-{direction}] ip={ip_addr}{suffix}, empty-frame")
|
||||
return
|
||||
|
||||
detail_parts = [
|
||||
f"len={len(raw)}",
|
||||
f"hex={_hex_bytes(raw)}",
|
||||
f"visible={_visible_bytes(raw)}",
|
||||
]
|
||||
|
||||
if raw.startswith(b"\x02") and len(raw) >= 6:
|
||||
frame_no = raw[1:2]
|
||||
etx_pos = raw.find(b"\x03")
|
||||
etb_pos = raw.find(b"\x17")
|
||||
end_pos = etx_pos if etx_pos != -1 else etb_pos
|
||||
end_name = "ETX" if etx_pos != -1 else ("ETB" if etb_pos != -1 else "NONE")
|
||||
if end_pos != -1 and len(raw) >= end_pos + 5:
|
||||
checksum_rx = raw[end_pos + 1:end_pos + 3]
|
||||
trailer = raw[end_pos + 3:end_pos + 5]
|
||||
checksum_basis = raw[1:end_pos + 1].decode("latin-1", errors="ignore")
|
||||
checksum_calc = calculate_astm_checksum(checksum_basis).encode("ascii")
|
||||
checksum_ok = checksum_rx.upper() == checksum_calc.upper()
|
||||
trailer_ok = trailer == b"\r\n"
|
||||
detail_parts.extend([
|
||||
f"frame_no={frame_no.decode('ascii', errors='ignore')}",
|
||||
f"terminator=<{end_name}>",
|
||||
f"checksum_rx={checksum_rx.decode('ascii', errors='ignore')}",
|
||||
f"checksum_calc={checksum_calc.decode('ascii', errors='ignore')}",
|
||||
f"checksum_ok={checksum_ok}",
|
||||
f"trailer={_visible_bytes(trailer)}",
|
||||
f"trailer_ok={trailer_ok}",
|
||||
])
|
||||
else:
|
||||
detail_parts.append(f"terminator=<{end_name}>")
|
||||
|
||||
print(f"[GENEXPERT-ASTM-{direction}] ip={ip_addr}{suffix}, " + ", ".join(detail_parts))
|
||||
|
||||
def debug_genexpert_order_message(hl7_message, ip_addr=None):
|
||||
segments = parse_hl7_segments(hl7_message)
|
||||
current_pid = ""
|
||||
@@ -1367,6 +1515,28 @@ def clear_genexpert_inflight_for_ip(ip_addr, reason="cleared"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def stop_all_scheduled_result_queries(reason="no-active-genexpert"):
|
||||
with scheduled_result_query_lock:
|
||||
scheduled_result_queries.clear()
|
||||
with genexpert_query_inflight_lock:
|
||||
genexpert_query_inflight_by_ip.clear()
|
||||
print(f"[GENEXPERT-SCHEDULER] Nonaktif. Clear jadwal, reason={reason}")
|
||||
return 0
|
||||
|
||||
def stop_scheduled_result_query(accnumber, reason="completed"):
|
||||
accnumber = str(accnumber or "").strip()
|
||||
with scheduled_result_query_lock:
|
||||
scheduled_result_queries.pop(accnumber, None)
|
||||
print(f"[GENEXPERT-SCHEDULER] Nonaktif. Stop accnumber={accnumber}, reason={reason}")
|
||||
return False
|
||||
|
||||
def schedule_result_query_for_order(accnumber, register_no, target_ip=None, **kwargs):
|
||||
print(
|
||||
f"[GENEXPERT-SCHEDULER] Nonaktif. Jadwal query hasil dilewati "
|
||||
f"untuk accnumber={accnumber}, target_ip={target_ip}"
|
||||
)
|
||||
return False
|
||||
|
||||
@app.route("/api/genexpert/query-result", methods=["POST"])
|
||||
def api_query_genexpert_result():
|
||||
return jsonify({
|
||||
|
||||
@@ -455,7 +455,7 @@ def get_genexpert_query_orders(ip_addr, hl7_msg):
|
||||
|
||||
base_orders = session.query(PaslabOrder).filter(
|
||||
(flag_attr == False) | (flag_attr == None)
|
||||
).order_by(PaslabOrder.urut.asc()).all()
|
||||
).order_by(PaslabOrder.rtglast.desc().nullslast(), PaslabOrder.urut.desc()).all()
|
||||
|
||||
requested_sample_id = (param_2 or param_1).strip() if (param_2 or param_1) else ""
|
||||
if requested_sample_id and requested_sample_id.upper() != "ALL":
|
||||
@@ -672,7 +672,7 @@ def send_all_orders_astm(conn, ip_addr, astm_msg, response_framing="astm"):
|
||||
|
||||
base_orders = session.query(PaslabOrder).filter(
|
||||
(flag_attr == False) | (flag_attr == None)
|
||||
).order_by(PaslabOrder.urut.asc()).all()
|
||||
).order_by(PaslabOrder.rtglast.desc().nullslast(), PaslabOrder.urut.desc()).all()
|
||||
|
||||
selected_orders = []
|
||||
if requested_sample_id and requested_sample_id.upper() != "ALL":
|
||||
@@ -2458,9 +2458,15 @@ def parse_genexpert_astm_records(astm_string, device_name):
|
||||
rawdt=astm_string, # Simpan data mentahnya (utuh) ke TEXT untuk jaga-jaga/bisa dibaca ulang
|
||||
organisme=kesimpulan_safe, # Hasil yang sudah bersih dan muat
|
||||
alat=device_name,
|
||||
processed='N' # Atau menyesuaikan default sistem Anda
|
||||
)
|
||||
session.add(new_result)
|
||||
source_ip = str(device_name or "").replace("GeneXpert-", "").strip()
|
||||
flag_name = get_flag_by_device(source_ip)
|
||||
if flag_name:
|
||||
order = session.query(PaslabOrder).filter(PaslabOrder.rnoreg == seq_no_safe).first()
|
||||
if order:
|
||||
setattr(order, flag_name, True)
|
||||
print(f"[GENEXPERT] Hasil ASTM diterima, set {flag_name}=TRUE untuk {seq_no_safe}")
|
||||
session.commit()
|
||||
print(f"[GENEXPERT-DB-SUCCESS] Hasil lab untuk Order {seq_no_safe} berhasil disimpan ke LisPhoenix!")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user