3466 lines
192 KiB
PHP
3466 lines
192 KiB
PHP
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>ABDM Work Task Board</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<style>
|
|
body { background: #f5f7fb; }
|
|
.container-wrap { width: 100%; margin: 0; padding: 20px 24px; box-sizing: border-box; }
|
|
.table td, .table th { vertical-align: middle; }
|
|
.status-pill { text-transform: uppercase; font-size: 11px; letter-spacing: .4px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container-wrap">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<div>
|
|
<h4 class="mb-0">ABDM Work Task Board</h4>
|
|
<div class="small text-muted">ABDM operational dashboard and work queues</div>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-outline-primary" id="btnRefresh">Refresh</button>
|
|
</div>
|
|
|
|
<div class="d-flex flex-wrap gap-2 mb-3" id="taskFilters">
|
|
<button type="button" class="btn btn-sm btn-primary filter-btn" data-filter="dashboard">Dashboard</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="opd_book">OPD Book</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="opd_consult_publish">OPD Consult / Prescription Records</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="lab_report_publish">Lab Reports</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="radiology_report_publish">Radiology Reports</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="immunization_record_publish">Immunization Record</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="invoice">Invoice</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="health_document_publish">Health Document</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="wellness_record_publish">Wellness Record</button>
|
|
<button type="button" class="btn btn-sm btn-outline-primary filter-btn" data-filter="ipd_discharge_publish">IPD Discharge</button>
|
|
</div>
|
|
|
|
<?php $dashboard = $dashboard_metrics ?? []; ?>
|
|
<div id="dashboardCard">
|
|
<form method="get" action="<?= current_url() ?>" class="d-flex flex-wrap align-items-end gap-2 mb-3" id="dashboardRangeForm">
|
|
<div>
|
|
<label class="form-label small mb-1" for="dashboardDateFrom">From</label>
|
|
<input type="date" class="form-control form-control-sm" id="dashboardDateFrom" name="date_from" value="<?= esc((string) ($dashboard_date_from ?? date('Y-m-d'))) ?>">
|
|
</div>
|
|
<div>
|
|
<label class="form-label small mb-1" for="dashboardDateTo">To</label>
|
|
<input type="date" class="form-control form-control-sm" id="dashboardDateTo" name="date_to" value="<?= esc((string) ($dashboard_date_to ?? date('Y-m-d'))) ?>">
|
|
</div>
|
|
<button type="submit" class="btn btn-sm btn-outline-primary">Apply Date Range</button>
|
|
</form>
|
|
<div class="row g-3 mb-3">
|
|
<?php foreach ([
|
|
['Total Patients', 'total_patients', 'primary'],
|
|
['Patients Without ABHA', 'without_abha', 'danger'],
|
|
['ABHA Verified', 'abha_verified', 'success'],
|
|
['Verified In Range', 'verified_in_range', 'info'],
|
|
['Records Pushed', 'records_pushed', 'warning'],
|
|
['Pushed In Range', 'records_pushed_in_range', 'primary'],
|
|
['OPD Tokens', 'opd_tokens', 'secondary'],
|
|
['Tokens In Range', 'opd_tokens_in_range', 'success'],
|
|
] as [$label, $key, $tone]): ?>
|
|
<div class="col-6 col-lg-3">
|
|
<div class="card h-100 border-start border-4 border-<?= esc($tone) ?>">
|
|
<div class="card-body py-3">
|
|
<div class="small text-muted"><?= esc($label) ?></div>
|
|
<div class="fs-3 fw-semibold"><?= number_format((int) ($dashboard[$key] ?? 0)) ?></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php if (($dashboard['verification_date_source'] ?? '') === 'abdm_linked_at'): ?>
|
|
<div class="small text-muted mb-3">Date-range verification uses the stored ABHA link/verification timestamp.</div>
|
|
<?php elseif (($dashboard['verification_date_source'] ?? '') === 'last_update'): ?>
|
|
<div class="small text-muted mb-3">Date-range verification uses the patient last-updated date because an ABHA timestamp is not available.</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div class="card shadow-sm mb-3 d-none" id="fhirDebugCard">
|
|
<div class="card-header py-2 d-flex justify-content-between align-items-center">
|
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
<strong id="fhirDebugTitle">FHIR JSON Debug</strong>
|
|
<span class="badge bg-secondary" id="fhirDataBadge">UNKNOWN</span>
|
|
<span class="badge bg-secondary" id="fhirHttpBadge">HTTP -</span>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<button type="button" class="btn btn-sm btn-outline-primary" id="btnCopyFhirDebug">Copy JSON</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnHideFhirDebug">Hide</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="small text-muted mb-2" id="fhirDebugMeta">Ready</div>
|
|
<pre id="fhirDebugJson" class="mb-0" style="max-height:420px; overflow:auto; background:#0b1020; color:#d9e2ff; padding:12px; border-radius:8px;">{}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm" id="taskTableCard">
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-striped mb-0" id="taskTable">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Task</th>
|
|
<th>Patient</th>
|
|
<th>ABHA</th>
|
|
<th>Entity</th>
|
|
<th>Status</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach (($tasks ?? []) as $t): ?>
|
|
<?php
|
|
$type = strtolower((string) ($t['task_type'] ?? ''));
|
|
$abhaId = trim((string) ($t['abha_id'] ?? ''));
|
|
$hasValidAbha = preg_match('/^\d{14}$/', $abhaId) === 1;
|
|
|
|
$payload = json_decode((string) ($t['payload_json'] ?? ''), true);
|
|
if (! is_array($payload)) {
|
|
$payload = [];
|
|
}
|
|
$meta = (isset($payload['meta']) && is_array($payload['meta'])) ? $payload['meta'] : [];
|
|
$opdId = (int) ($meta['opd_id'] ?? 0);
|
|
$opdSessionId = (int) ($meta['opd_session_id'] ?? 0);
|
|
$showSandbox = ($type === 'opd_prescription_publish' && $opdId > 0);
|
|
$showPreview = ($type === 'opd_prescription_publish' && $opdId > 0)
|
|
|| in_array($type, ['lab_report_publish', 'radiology_report_publish', 'ipd_discharge_publish', 'immunization_record_publish', 'health_document_publish', 'wellness_record_publish'], true);
|
|
$bridgeSubmitted = (int) ($t['bridge_submitted'] ?? 0) === 1;
|
|
$bridgeCareContext = trim((string) ($t['bridge_care_context_reference'] ?? ''));
|
|
?>
|
|
<tr
|
|
data-task-id="<?= (int) ($t['id'] ?? 0) ?>"
|
|
data-task-type="<?= esc((string) ($t['task_type'] ?? '')) ?>"
|
|
data-entity-id="<?= esc((string) ($t['entity_id'] ?? '')) ?>"
|
|
data-patient-id="<?= (int) ($t['patient_id'] ?? 0) ?>"
|
|
data-abha-id="<?= esc($abhaId) ?>"
|
|
data-opd-id="<?= $opdId ?>"
|
|
data-opd-session-id="<?= $opdSessionId ?>"
|
|
data-sandbox-eligible="<?= $showSandbox ? '1' : '0' ?>"
|
|
data-bridge-submitted="<?= $bridgeSubmitted ? '1' : '0' ?>"
|
|
>
|
|
<td><?= (int) ($t['id'] ?? 0) ?></td>
|
|
<td>
|
|
<div><strong><?= esc((string) ($t['task_code'] ?? '')) ?></strong></div>
|
|
<div class="text-muted small"><?= esc((string) ($t['task_type'] ?? '')) ?></div>
|
|
</td>
|
|
<td>
|
|
<div><?= esc((string) ($t['patient_name'] ?? '')) ?></div>
|
|
<div class="text-muted small">#<?= (int) ($t['patient_id'] ?? 0) ?></div>
|
|
</td>
|
|
<td><input type="text" class="form-control form-control-sm abha-input" value="<?= esc((string) ($t['abha_id'] ?? '')) ?>" placeholder="14-digit ABHA"></td>
|
|
<td>
|
|
<div><?= esc((string) ($t['entity_type'] ?? '')) ?></div>
|
|
<div class="text-muted small"><?= esc((string) ($t['entity_id'] ?? '')) ?></div>
|
|
</td>
|
|
<td>
|
|
<?php if ($bridgeSubmitted && strtolower((string) ($t['bridge_push_status'] ?? '')) === 'linked'): ?>
|
|
<span class="badge bg-success status-pill">LINKED</span>
|
|
<?php elseif ($bridgeSubmitted): ?>
|
|
<span class="badge bg-info status-pill">BRIDGE SUBMITTED</span>
|
|
<?php if ($bridgeCareContext !== ''): ?><div class="small"><code><?= esc($bridgeCareContext) ?></code></div><?php endif; ?>
|
|
<?php else: ?>
|
|
<span class="badge bg-secondary status-pill"><?= esc((string) ($t['status'] ?? 'pending')) ?></span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex gap-1 flex-wrap">
|
|
<?php if ($type === 'patient_abha_create' || $type === 'patient_abha_link'): ?>
|
|
<button type="button" class="btn btn-sm btn-outline-success action-btn" data-action="create_abha">Create ABHA</button>
|
|
<?php elseif ($type === 'patient_abha_update'): ?>
|
|
<button type="button" class="btn btn-sm btn-outline-primary action-btn" data-action="update_abha">Update ABHA</button>
|
|
<?php elseif ($type === 'immunization_record_publish'): ?>
|
|
<?php if ($bridgeSubmitted): ?>
|
|
<button type="button" class="btn btn-sm btn-outline-success" disabled title="Submitted to ABDM Bridge<?= $bridgeCareContext !== '' ? ': ' . esc($bridgeCareContext) : '' ?>">Submitted to Bridge</button>
|
|
<?php else: ?>
|
|
<button type="button" class="btn btn-sm btn-outline-success immunization-bridge-btn" title="Push this ImmunizationRecord FHIR bundle to ABDM Bridge.">Push to Bridge</button>
|
|
<?php endif; ?>
|
|
<?php else: ?>
|
|
<button type="button" class="btn btn-sm btn-outline-warning action-btn" data-action="submit">Submit</button>
|
|
<?php endif; ?>
|
|
<?php if ($showSandbox): ?>
|
|
<button type="button" class="btn btn-sm btn-outline-info sandbox-btn">Sandbox</button>
|
|
<?php endif; ?>
|
|
<?php if ($showPreview): ?>
|
|
<button type="button" class="btn btn-sm btn-outline-primary preview-fhir-btn">Preview FHIR</button>
|
|
<?php endif; ?>
|
|
<button type="button" class="btn btn-sm btn-outline-dark close-btn">Close</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card shadow-sm mt-3 d-none" id="abdmSandboxCard">
|
|
<div class="card-header py-2 d-flex justify-content-between align-items-center">
|
|
<strong>ABDM Sandbox Actions</strong>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnSandboxHide">Hide</button>
|
|
</div>
|
|
<div class="card-body p-3">
|
|
<div class="small text-muted mb-2" id="abdmSandboxContext">Select eligible OPD Publish task to use sandbox actions.</div>
|
|
|
|
<div class="row g-2 mb-2">
|
|
<div class="col-md-6">
|
|
<label class="form-label mb-1 small">ABHA Number</label>
|
|
<input type="text" class="form-control form-control-sm" id="tb_abha_id" maxlength="14" placeholder="14-digit ABHA">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label mb-1 small">QR Payload (Scan & Share)</label>
|
|
<input type="text" class="form-control form-control-sm" id="tb_abdm_qr_payload" placeholder="Paste scanned QR payload">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex flex-wrap gap-2 mb-2">
|
|
<button type="button" class="btn btn-outline-primary btn-sm" id="tb_btn_abdm_scan_lookup">Scan Lookup</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" id="tb_btn_abdm_validate_abha">Validate ABHA</button>
|
|
</div>
|
|
|
|
<div class="row g-2 mb-2">
|
|
<div class="col-md-6">
|
|
<input type="text" class="form-control form-control-sm" id="tb_abdm_purpose_code" placeholder="Purpose (ex: CAREMGT)">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<input type="datetime-local" class="form-control form-control-sm" id="tb_abdm_consent_expires_at">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex flex-wrap gap-2 mb-2">
|
|
<button type="button" class="btn btn-outline-success btn-sm" id="tb_btn_abdm_consent_request">Request Consent</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" id="tb_btn_abdm_preview_fhir">Preview FHIR JSON</button>
|
|
<button type="button" class="btn btn-outline-warning btn-sm" id="tb_btn_abdm_share_fhir">Share FHIR</button>
|
|
</div>
|
|
|
|
<div class="mb-2">
|
|
<label class="form-label mb-1 small">Consent Handle</label>
|
|
<input type="text" class="form-control form-control-sm" id="tb_abdm_consent_handle" placeholder="Auto-filled after consent request">
|
|
</div>
|
|
|
|
<div class="mb-2">
|
|
<label class="form-label mb-1 small">Gateway Consent ID</label>
|
|
<input type="text" class="form-control form-control-sm" id="tb_abdm_gateway_consent_id" placeholder="Auto-filled if returned by gateway" readonly>
|
|
</div>
|
|
|
|
<div class="row g-2 mb-2">
|
|
<div class="col-md-7">
|
|
<input type="number" min="1" class="form-control form-control-sm" id="tb_abdm_claim_document_id" placeholder="Claim Document ID">
|
|
</div>
|
|
<div class="col-md-5">
|
|
<button type="button" class="btn btn-outline-dark btn-sm w-100" id="tb_btn_abdm_claim_status">Claim Status</button>
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="my-2">
|
|
<div class="small fw-semibold mb-1">Share Additional HI Types</div>
|
|
<div class="row g-2 mb-2">
|
|
<div class="col-md-4">
|
|
<input type="number" min="1" class="form-control form-control-sm" id="tb_hi_patient_id" placeholder="Patient ID">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<input type="number" min="1" class="form-control form-control-sm" id="tb_hi_opd_id" placeholder="OPD ID (wellness)">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<input type="number" min="1" class="form-control form-control-sm" id="tb_hi_invoice_id" placeholder="Invoice ID">
|
|
</div>
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2 mb-2">
|
|
<button type="button" class="btn btn-outline-info btn-sm" data-hi-share="immunization">Immunization</button>
|
|
<button type="button" class="btn btn-outline-info btn-sm" data-hi-share="wellness">Wellness</button>
|
|
<button type="button" class="btn btn-outline-info btn-sm" data-hi-share="health_document">Health Document</button>
|
|
<button type="button" class="btn btn-outline-info btn-sm" data-hi-share="invoice">Invoice</button>
|
|
</div>
|
|
|
|
<div class="small text-muted" id="abdmSandboxStatus">Ready</div>
|
|
<div class="mt-2 d-none" id="abdmScanResolvePane"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- OPD Book Card -->
|
|
<div class="card shadow-sm mt-3 d-none" id="opdBookCard">
|
|
<div class="card-header py-2 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
|
<div>
|
|
<strong>OPD Book - ABDM Tokens</strong>
|
|
<span class="badge bg-primary ms-2" id="hmsOpdBadge"><?= count($opd_book_rows ?? []) ?></span>
|
|
</div>
|
|
<div class="d-flex gap-2 align-items-center">
|
|
<input type="date" class="form-control form-control-sm" id="opdBookDate" value="<?= date('Y-m-d') ?>" style="width:145px">
|
|
<button type="button" class="btn btn-sm btn-outline-info" id="btnFetchAbdmQueue">Fetch ABDM Queue</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="px-3 pt-2 pb-1 bg-light border-bottom d-flex align-items-center gap-2">
|
|
<strong class="small">ABDM Gateway Queue</strong>
|
|
<span class="badge bg-secondary" id="abdmQueueBadge">-</span>
|
|
<span class="text-muted small" id="abdmQueueStatus">Click "Fetch ABDM Queue" to load</span>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm mb-0">
|
|
<thead class="table-light">
|
|
<tr><th>Token</th><th>ABHA</th><th>Name</th><th>Status</th><th>Time</th><th>HMS Patient</th></tr>
|
|
</thead>
|
|
<tbody id="abdmQueueTbody"><tr><td colspan="6" class="text-muted px-2 py-2">Not loaded yet.</td></tr></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="px-3 pt-2 pb-1 bg-light border-bottom border-top d-flex align-items-center gap-2 mt-1">
|
|
<strong class="small">HMS ABDM OPD Token List</strong>
|
|
<span class="badge bg-success"><?= count($opd_book_rows ?? []) ?></span>
|
|
<span class="text-muted small">Last 7 days synced and manual ABDM tokens only</span>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm mb-0">
|
|
<thead class="table-light">
|
|
<tr><th>Token</th><th>Patient</th><th>ABHA</th><th>Date</th><th>Source</th><th>Status</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (! empty($opd_book_rows ?? [])): ?>
|
|
<?php foreach (($opd_book_rows ?? []) as $r): ?>
|
|
<tr>
|
|
<td><?= esc((string) (($r['token_number'] ?? '') ?: ('#' . ($r['gateway_token_id'] ?? '')))) ?></td>
|
|
<td><?= esc((string) ($r['patient_name'] ?? '')) ?></td>
|
|
<td><span class="text-primary small"><?= esc((string) (($r['abha_number'] ?? '') ?: ($r['abha_address'] ?? ''))) ?></span></td>
|
|
<td><?= esc((string) ($r['queue_date'] ?? '')) ?></td>
|
|
<td><?= esc(ucwords(str_replace('_', ' ', (string) ($r['source'] ?? '')))) ?></td>
|
|
<?php $tokenStatus = strtoupper((string) ($r['status'] ?? 'PENDING')); ?>
|
|
<td><span class="badge bg-<?= $tokenStatus === 'COMPLETED' ? 'success' : ($tokenStatus === 'CANCELLED' ? 'danger' : 'warning text-dark') ?>"><?= esc($tokenStatus) ?></span></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php else: ?>
|
|
<tr><td colspan="6" class="text-muted px-2 py-2">No ABDM OPD tokens found in the last 7 days.</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- OPD Consult / Prescription Records Card -->
|
|
<div class="card shadow-sm mt-3 d-none" id="opdConsultCard">
|
|
<div class="card-header py-2 d-flex align-items-center gap-2">
|
|
<strong>OPD Consult / Prescription Records - Done Appointments with ABHA</strong>
|
|
<span class="badge bg-success"><?= count($opd_consult_rows ?? []) ?></span>
|
|
<small class="text-muted ms-1">Last 30 days opd_status=Done ABHA linked</small>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm mb-0">
|
|
<thead class="table-light">
|
|
<tr><th>OPD ID</th><th>Patient</th><th>ABHA</th><th>Consult Date</th><th>Doctor</th><th>Record Status</th><th>Details</th><th>Action</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (! empty($opd_consult_rows ?? [])): ?>
|
|
<?php foreach (($opd_consult_rows ?? []) as $r): ?>
|
|
<?php
|
|
$statusTone = trim((string) ($r['record_status_tone'] ?? 'secondary'));
|
|
$allowedTones = ['secondary', 'primary', 'info', 'success', 'warning', 'danger', 'dark'];
|
|
if (! in_array($statusTone, $allowedTones, true)) {
|
|
$statusTone = 'secondary';
|
|
}
|
|
$statusLabel = trim((string) ($r['record_status_label'] ?? 'Not Registered'));
|
|
$statusNote = trim((string) ($r['record_status_note'] ?? ''));
|
|
$queueId = trim((string) ($r['queue_id'] ?? ''));
|
|
$careContext = trim((string) ($r['care_context_reference'] ?? ''));
|
|
$linkStatus = trim((string) ($r['link_status'] ?? ''));
|
|
$bridgeRecordId = trim((string) ($r['bridge_record_id'] ?? ''));
|
|
$hasFhir = (int) ($r['has_fhir'] ?? 0) === 1;
|
|
$pushStatusRaw = strtolower(trim((string) ($r['push_status'] ?? '')));
|
|
$canSubmitGateway = $hasFhir;
|
|
$submitButtonLabel = in_array($pushStatusRaw, ['queued', 'linked', 'failed'], true)
|
|
? 'Submit Again'
|
|
: 'Submit to ABDM';
|
|
$submitButtonClass = in_array($pushStatusRaw, ['queued', 'linked'], true)
|
|
? 'btn-outline-secondary'
|
|
: 'btn-outline-warning';
|
|
$statusBadgeClass = 'badge bg-' . $statusTone;
|
|
if ($statusTone === 'warning') {
|
|
$statusBadgeClass .= ' text-dark';
|
|
}
|
|
?>
|
|
<tr data-push-status="<?= esc($pushStatusRaw) ?>">
|
|
<td>#<?= (int) ($r['opd_id'] ?? 0) ?></td>
|
|
<td><?= esc((string) ($r['P_name'] ?? '')) ?></td>
|
|
<td><span class="text-primary small"><?= esc((string) ($r['abha_id'] ?? '')) ?></span></td>
|
|
<td><?= esc(substr((string) ($r['apointment_date'] ?? ''), 0, 16)) ?></td>
|
|
<td><?= esc((string) ($r['doc_name'] ?? '')) ?></td>
|
|
<td><span class="<?= esc($statusBadgeClass) ?> opd-consult-status-badge"><?= esc($statusLabel) ?></span></td>
|
|
<td class="small text-muted opd-consult-details" style="min-width:260px;max-width:340px;">
|
|
<?php if ($statusNote !== ''): ?>
|
|
<div><?= esc($statusNote) ?></div>
|
|
<?php endif; ?>
|
|
<?php if ($queueId !== ''): ?>
|
|
<div><strong>Queue:</strong> <?= esc($queueId) ?></div>
|
|
<?php endif; ?>
|
|
<?php if ($bridgeRecordId !== ''): ?>
|
|
<div><strong>Bridge:</strong> #<?= esc($bridgeRecordId) ?></div>
|
|
<?php endif; ?>
|
|
<?php if ($linkStatus !== ''): ?>
|
|
<div><strong>Link:</strong> <?= esc(strtoupper($linkStatus)) ?></div>
|
|
<?php endif; ?>
|
|
<?php if ($careContext !== ''): ?>
|
|
<div class="text-truncate" title="<?= esc($careContext) ?>"><strong>CC:</strong> <?= esc($careContext) ?></div>
|
|
<?php endif; ?>
|
|
<?php if (! $hasFhir): ?>
|
|
<div class="text-danger">FHIR bundle not generated yet.</div>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<button type="button" class="btn btn-sm btn-outline-primary btn-opd-consult-fhir" data-opd-id="<?= (int) ($r['opd_id'] ?? 0) ?>">Preview FHIR</button>
|
|
<?php if ($canSubmitGateway): ?>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm <?= esc($submitButtonClass) ?> btn-opd-consult-submit-gateway ms-1"
|
|
data-opd-id="<?= (int) ($r['opd_id'] ?? 0) ?>"
|
|
data-abha-id="<?= esc((string) ($r['abha_id'] ?? '')) ?>"
|
|
><?= esc($submitButtonLabel) ?></button>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php else: ?>
|
|
<tr><td colspan="8" class="text-muted px-2 py-2">No done OPD with ABHA found in last 30 days.</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Invoice Status Card -->
|
|
<div class="card shadow-sm mt-3 d-none" id="invoiceCard">
|
|
<div class="card-header py-2 d-flex align-items-center gap-2 flex-wrap">
|
|
<strong>Invoice - ABDM Billing Records</strong>
|
|
<span class="badge bg-primary"><?= count($invoice_rows ?? []) ?></span>
|
|
<small class="text-muted">OPD, Charges and IPD Billing</small>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm mb-0">
|
|
<thead class="table-light">
|
|
<tr><th>Source</th><th>Bill No.</th><th>Patient</th><th>Date</th><th class="text-end">Amount</th><th>Push Status</th><th>Link Status</th><th>Gateway Reference</th><th>Action</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (! empty($invoice_rows ?? [])): ?>
|
|
<?php foreach (($invoice_rows ?? []) as $invoice): ?>
|
|
<?php
|
|
$tone = (string) ($invoice['record_status_tone'] ?? 'secondary');
|
|
if (! in_array($tone, ['secondary', 'primary', 'info', 'success', 'warning', 'danger'], true)) {
|
|
$tone = 'secondary';
|
|
}
|
|
$badgeClass = 'badge bg-' . $tone . ($tone === 'warning' ? ' text-dark' : '');
|
|
$queueId = trim((string) ($invoice['queue_id'] ?? ''));
|
|
$bridgeRecordId = (int) ($invoice['bridge_record_id'] ?? 0);
|
|
$careContext = trim((string) ($invoice['care_context_reference'] ?? ''));
|
|
$pushStatus = strtolower(trim((string) ($invoice['push_status'] ?? '')));
|
|
$linkStatus = strtolower(trim((string) ($invoice['link_status'] ?? '')));
|
|
$alreadySubmitted = in_array($pushStatus, ['queued', 'pushed', 'linked'], true) || $linkStatus === 'linked';
|
|
?>
|
|
<tr
|
|
data-invoice-source="<?= esc((string) ($invoice['source_key'] ?? '')) ?>"
|
|
data-bill-id="<?= (int) ($invoice['bill_id'] ?? 0) ?>"
|
|
data-patient-id="<?= (int) ($invoice['patient_id'] ?? 0) ?>"
|
|
data-push-status="<?= esc($pushStatus) ?>"
|
|
>
|
|
<td><?= esc((string) ($invoice['source'] ?? '')) ?></td>
|
|
<td><strong><?= esc((string) (($invoice['bill_code'] ?? '') ?: ('#' . ($invoice['bill_id'] ?? '')))) ?></strong></td>
|
|
<td>
|
|
<div><?= esc((string) ($invoice['patient_name'] ?? '')) ?></div>
|
|
<small class="text-muted">#<?= (int) ($invoice['patient_id'] ?? 0) ?></small>
|
|
</td>
|
|
<td><?= esc(substr((string) ($invoice['bill_date'] ?? ''), 0, 16)) ?></td>
|
|
<td class="text-end"><?= number_format((float) ($invoice['amount'] ?? 0), 2) ?></td>
|
|
<td><span class="<?= esc($badgeClass) ?> invoice-status-badge"><?= esc((string) ($invoice['record_status_label'] ?? 'Not Pushed')) ?></span></td>
|
|
<td><?= $invoice['link_status'] !== '' ? esc(strtoupper((string) $invoice['link_status'])) : '<span class="text-muted">-</span>' ?></td>
|
|
<td class="small text-muted invoice-details" style="min-width:190px;max-width:300px;">
|
|
<?php if ($queueId !== ''): ?><div><strong>Queue:</strong> <?= esc($queueId) ?></div><?php endif; ?>
|
|
<?php if ($bridgeRecordId > 0): ?><div><strong>Bridge:</strong> #<?= $bridgeRecordId ?></div><?php endif; ?>
|
|
<?php if ($careContext !== ''): ?><div class="text-truncate" title="<?= esc($careContext) ?>"><strong>CC:</strong> <?= esc($careContext) ?></div><?php endif; ?>
|
|
<?php if ($queueId === '' && $bridgeRecordId <= 0 && $careContext === ''): ?><span>-</span><?php endif; ?>
|
|
</td>
|
|
<td class="text-nowrap">
|
|
<button type="button" class="btn btn-sm btn-outline-primary btn-invoice-preview">Preview</button>
|
|
<button type="button" class="btn btn-sm btn-outline-warning btn-invoice-push" <?= $alreadySubmitted ? 'disabled' : '' ?>>Push</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php else: ?>
|
|
<tr><td colspan="9" class="text-muted px-2 py-2">No billing records found.</td></tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="small text-muted mt-2" id="statusBox">Ready</div>
|
|
</div>
|
|
|
|
<!-- FHIR Preview Modal -->
|
|
<div class="modal fade" id="fhirPreviewModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header py-2">
|
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
|
<h6 class="modal-title mb-0" id="fhirModalTitle">FHIR Preview</h6>
|
|
<span class="badge bg-secondary" id="fhirModalDataBadge">CHECKING</span>
|
|
<span class="badge bg-secondary" id="fhirModalHttpBadge">HTTP -</span>
|
|
</div>
|
|
<div class="d-flex gap-2 ms-auto flex-wrap">
|
|
<div class="dropdown">
|
|
<button type="button" class="btn btn-sm btn-outline-success dropdown-toggle" id="btnFhirUpdateCodes" data-bs-toggle="dropdown" aria-expanded="false">
|
|
<i class="bi bi-pencil-square"></i> Update Codes
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li><h6 class="dropdown-header">Assign SNOMED / LOINC</h6></li>
|
|
<li>
|
|
<a class="dropdown-item" href="#" id="btnGoInvestMaster">
|
|
<i class="bi bi-clipboard2-pulse me-1"></i> Investigation Master <small class="text-muted">(LOINC codes)</small>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="dropdown-item" href="#" id="btnGoClinicalMaster">
|
|
<i class="bi bi-heart-pulse me-1"></i> Clinical Master <small class="text-muted">(Complaints & Diagnosis SNOMED)</small>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-outline-warning" id="btnRegenerateFhirModal" title="Rebuild FHIR bundle from current clinical data"><i class="bi bi-arrow-clockwise"></i> Regenerate</button>
|
|
<button type="button" class="btn btn-sm btn-primary" id="btnUpdateFhirJson" title="Validate and apply edited JSON changes to preview and database"><i class="bi bi-check2-circle"></i> Update JSON</button>
|
|
<div>
|
|
<label class="me-3 small text-muted"><input type="checkbox" id="chkForceNewRecord" checked> New Record ID (for PHR testing)</label>
|
|
<button type="button" class="btn btn-sm btn-outline-success" id="btnSubmitFhirToAbdm" title="Push FHIR bundle to ABDM bridge"><i class="bi bi-cloud-upload"></i> Submit to ABDM</button>
|
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="btnCopyFhirModal">Copy JSON</button>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<div class="d-flex align-items-center gap-3 px-3 pt-2 pb-0 border-bottom">
|
|
<div class="flex-grow-1 small" id="fhirModalMeta">Loading...</div>
|
|
<ul class="nav nav-tabs border-0" id="fhirViewTabs" style="margin-bottom:-1px;">
|
|
<li class="nav-item">
|
|
<button class="nav-link active py-1 px-3" id="fhirTabForm" type="button">Form View</button>
|
|
</li>
|
|
<li class="nav-item">
|
|
<button class="nav-link py-1 px-3" id="fhirTabJson" type="button">JSON</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="px-3 pb-2 pt-1" id="fhirModalQuality" style="display:none;"></div>
|
|
<div id="fhirFormView" style="overflow-y:auto;max-height:62vh;padding:16px;"></div>
|
|
<textarea id="fhirModalJson" class="form-control" style="display:none;margin:0;max-height:62vh;height:500px;font-family:monospace;background:#0b1020;color:#d9e2ff;padding:16px;border-radius:0;" spellcheck="false">{}</textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="taskActionModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">ABDM Guided Action</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-2 small text-muted" id="modalTaskSummary"></div>
|
|
<label class="form-label">ABHA Number</label>
|
|
<input type="text" class="form-control" id="modalAbhaInput" maxlength="14" placeholder="Enter 14-digit ABHA">
|
|
<div class="form-text">ABHA validation is required before queueing the ABDM API action.</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-light" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary" id="modalConfirmBtn">Queue Action</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script>
|
|
(function () {
|
|
var csrfName = '<?= csrf_token() ?>';
|
|
var csrfHash = '<?= csrf_hash() ?>';
|
|
var refreshUrl = '<?= base_url('AbdmTaskBoard') ?>';
|
|
var statusBox = document.getElementById('statusBox');
|
|
var modalEl = document.getElementById('taskActionModal');
|
|
var actionModal = new bootstrap.Modal(modalEl);
|
|
var modalAbhaInput = document.getElementById('modalAbhaInput');
|
|
var modalTaskSummary = document.getElementById('modalTaskSummary');
|
|
var modalConfirmBtn = document.getElementById('modalConfirmBtn');
|
|
var sandboxCard = document.getElementById('abdmSandboxCard');
|
|
var sandboxContext = document.getElementById('abdmSandboxContext');
|
|
var sandboxStatus = document.getElementById('abdmSandboxStatus');
|
|
var scanResolvePane = document.getElementById('abdmScanResolvePane');
|
|
var sandboxAbhaInput = document.getElementById('tb_abha_id');
|
|
var sandboxQrPayloadInput = document.getElementById('tb_abdm_qr_payload');
|
|
var sandboxPurposeCodeInput = document.getElementById('tb_abdm_purpose_code');
|
|
var sandboxConsentExpiresInput = document.getElementById('tb_abdm_consent_expires_at');
|
|
var sandboxConsentHandleInput = document.getElementById('tb_abdm_consent_handle');
|
|
var sandboxGatewayConsentIdInput = document.getElementById('tb_abdm_gateway_consent_id');
|
|
var sandboxClaimDocumentInput = document.getElementById('tb_abdm_claim_document_id');
|
|
var quickOpdInput = null;
|
|
var quickSessionInput = null;
|
|
var fhirDebugCard = document.getElementById('fhirDebugCard');
|
|
var fhirDebugTitle = document.getElementById('fhirDebugTitle');
|
|
var fhirDebugMeta = document.getElementById('fhirDebugMeta');
|
|
var fhirDebugJson = document.getElementById('fhirDebugJson');
|
|
var fhirDataBadge = document.getElementById('fhirDataBadge');
|
|
var fhirHttpBadge = document.getElementById('fhirHttpBadge');
|
|
var selectedAction = '';
|
|
var selectedRow = null;
|
|
var sandboxRow = null;
|
|
|
|
function setBadge(el, text, tone) {
|
|
if (!el) {
|
|
return;
|
|
}
|
|
el.classList.remove('bg-secondary', 'bg-success', 'bg-danger', 'bg-warning', 'text-dark');
|
|
if (tone === 'success') {
|
|
el.classList.add('bg-success');
|
|
} else if (tone === 'danger') {
|
|
el.classList.add('bg-danger');
|
|
} else if (tone === 'warning') {
|
|
el.classList.add('bg-warning', 'text-dark');
|
|
} else {
|
|
el.classList.add('bg-secondary');
|
|
}
|
|
el.textContent = text;
|
|
}
|
|
|
|
var opdBookCard = document.getElementById('opdBookCard');
|
|
var opdConsultCard = document.getElementById('opdConsultCard');
|
|
var invoiceCard = document.getElementById('invoiceCard');
|
|
var taskTableCard = document.getElementById('taskTableCard');
|
|
var dashboardCard = document.getElementById('dashboardCard');
|
|
|
|
function applyFilter(filter) {
|
|
// Hide dedicated section cards
|
|
if (opdBookCard) opdBookCard.classList.add('d-none');
|
|
if (opdConsultCard) opdConsultCard.classList.add('d-none');
|
|
if (invoiceCard) invoiceCard.classList.add('d-none');
|
|
if (dashboardCard) dashboardCard.classList.add('d-none');
|
|
|
|
if (filter === 'dashboard') {
|
|
if (taskTableCard) taskTableCard.classList.add('d-none');
|
|
if (dashboardCard) dashboardCard.classList.remove('d-none');
|
|
} else if (filter === 'opd_book') {
|
|
if (taskTableCard) taskTableCard.classList.add('d-none');
|
|
if (opdBookCard) opdBookCard.classList.remove('d-none');
|
|
} else if (filter === 'opd_consult_publish') {
|
|
if (taskTableCard) taskTableCard.classList.add('d-none');
|
|
if (opdConsultCard) opdConsultCard.classList.remove('d-none');
|
|
} else if (filter === 'invoice') {
|
|
if (taskTableCard) taskTableCard.classList.add('d-none');
|
|
if (invoiceCard) invoiceCard.classList.remove('d-none');
|
|
} else {
|
|
if (taskTableCard) taskTableCard.classList.remove('d-none');
|
|
var rows = document.querySelectorAll('#taskTable tbody tr');
|
|
rows.forEach(function (row) {
|
|
var type = (row.getAttribute('data-task-type') || '').toLowerCase();
|
|
row.style.display = (type === filter.toLowerCase()) ? '' : 'none';
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('.filter-btn').forEach(function (btn) {
|
|
btn.classList.remove('btn-primary');
|
|
btn.classList.add('btn-outline-primary');
|
|
});
|
|
var active = document.querySelector('.filter-btn[data-filter="' + filter + '"]');
|
|
if (active) {
|
|
active.classList.remove('btn-outline-primary');
|
|
active.classList.add('btn-primary');
|
|
}
|
|
}
|
|
|
|
function showInlineFhirPreview(previewUrl, titleText) {
|
|
if (!previewUrl) {
|
|
setStatus('FHIR preview URL is missing.', true);
|
|
return;
|
|
}
|
|
|
|
if (fhirDebugTitle) {
|
|
fhirDebugTitle.textContent = titleText || 'FHIR JSON Debug';
|
|
}
|
|
if (fhirDebugMeta) {
|
|
fhirDebugMeta.textContent = 'Loading...';
|
|
fhirDebugMeta.className = 'small text-muted mb-2';
|
|
}
|
|
setBadge(fhirDataBadge, 'CHECKING', 'warning');
|
|
setBadge(fhirHttpBadge, 'HTTP ...', 'warning');
|
|
if (fhirDebugJson) {
|
|
fhirDebugJson.textContent = '{\n "loading": true\n}';
|
|
}
|
|
if (fhirDebugCard) {
|
|
fhirDebugCard.classList.remove('d-none');
|
|
fhirDebugCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}
|
|
|
|
fetch(previewUrl, {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json'
|
|
}
|
|
}).then(function (r) {
|
|
return r.text().then(function (body) {
|
|
return {
|
|
ok: r.ok,
|
|
status: r.status,
|
|
body: body
|
|
};
|
|
});
|
|
}).then(function (res) {
|
|
var parsed = null;
|
|
try {
|
|
parsed = JSON.parse(res.body || '{}');
|
|
} catch (e) {
|
|
parsed = { raw: res.body || '' };
|
|
}
|
|
|
|
var hasFhir = false;
|
|
if (parsed && typeof parsed === 'object') {
|
|
if (parsed.status === 'ok' && parsed.bundle && typeof parsed.bundle === 'object') {
|
|
hasFhir = true;
|
|
}
|
|
if (parsed.resourceType === 'Bundle') {
|
|
hasFhir = true;
|
|
}
|
|
}
|
|
|
|
if (fhirDebugMeta) {
|
|
if (res.ok) {
|
|
fhirDebugMeta.textContent = 'Loaded successfully (' + res.status + ')';
|
|
fhirDebugMeta.className = 'small text-success mb-2';
|
|
} else {
|
|
fhirDebugMeta.textContent = 'Loaded with error status (' + res.status + ')';
|
|
fhirDebugMeta.className = 'small text-danger mb-2';
|
|
}
|
|
}
|
|
setBadge(fhirHttpBadge, 'HTTP ' + res.status, res.ok ? 'success' : 'danger');
|
|
setBadge(fhirDataBadge, hasFhir ? 'HAS FHIR' : 'NOT GENERATED', hasFhir ? 'success' : 'danger');
|
|
if (fhirDebugJson) {
|
|
fhirDebugJson.textContent = JSON.stringify(parsed, null, 2);
|
|
}
|
|
}).catch(function (e) {
|
|
if (fhirDebugMeta) {
|
|
fhirDebugMeta.textContent = 'Request failed: ' + e.message;
|
|
fhirDebugMeta.className = 'small text-danger mb-2';
|
|
}
|
|
setBadge(fhirHttpBadge, 'HTTP ERR', 'danger');
|
|
setBadge(fhirDataBadge, 'NOT GENERATED', 'danger');
|
|
if (fhirDebugJson) {
|
|
fhirDebugJson.textContent = JSON.stringify({
|
|
error: e.message
|
|
}, null, 2);
|
|
}
|
|
});
|
|
}
|
|
|
|
function setStatus(msg, isError) {
|
|
statusBox.textContent = msg;
|
|
statusBox.style.color = isError ? '#dc3545' : '#6c757d';
|
|
}
|
|
|
|
function setSubmissionStatus(msg, queueId) {
|
|
setStatus(msg, false);
|
|
if (!queueId) {
|
|
return;
|
|
}
|
|
statusBox.appendChild(document.createTextNode(' '));
|
|
var logLink = document.createElement('a');
|
|
logLink.href = '<?= base_url('AbdmBridgeLog') ?>' + '?search=' + encodeURIComponent(queueId);
|
|
logLink.textContent = 'View Bridge Log';
|
|
logLink.className = 'ms-1';
|
|
statusBox.appendChild(logLink);
|
|
}
|
|
|
|
function post(url, payload, cb) {
|
|
payload[csrfName] = csrfHash;
|
|
var body = Object.keys(payload).map(function (key) {
|
|
return encodeURIComponent(key) + '=' + encodeURIComponent(payload[key]);
|
|
}).join('&');
|
|
fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
},
|
|
body: body
|
|
}).then(function (r) {
|
|
return r.json();
|
|
}).then(function (json) {
|
|
if (json && json.csrfHash) {
|
|
csrfHash = json.csrfHash;
|
|
}
|
|
cb(json || {});
|
|
}).catch(function (e) {
|
|
setStatus('Request failed: ' + e.message, true);
|
|
});
|
|
}
|
|
|
|
function setSandboxStatus(msg, level) {
|
|
if (!sandboxStatus) {
|
|
return;
|
|
}
|
|
sandboxStatus.classList.remove('text-muted', 'text-success', 'text-danger', 'text-warning');
|
|
if (level === 'success') {
|
|
sandboxStatus.classList.add('text-success');
|
|
} else if (level === 'danger') {
|
|
sandboxStatus.classList.add('text-danger');
|
|
} else if (level === 'warning') {
|
|
sandboxStatus.classList.add('text-warning');
|
|
} else {
|
|
sandboxStatus.classList.add('text-muted');
|
|
}
|
|
sandboxStatus.textContent = msg || 'Ready';
|
|
}
|
|
|
|
function clearScanResolvePane() {
|
|
if (!scanResolvePane) {
|
|
return;
|
|
}
|
|
scanResolvePane.classList.add('d-none');
|
|
scanResolvePane.innerHTML = '';
|
|
}
|
|
|
|
function renderScanResolvePane(queueId, identity, matches) {
|
|
if (!scanResolvePane) {
|
|
return;
|
|
}
|
|
|
|
identity = identity || {};
|
|
matches = Array.isArray(matches) ? matches : [];
|
|
|
|
var cardHtml = '<div class="border rounded p-2 bg-light">';
|
|
cardHtml += '<div class="small fw-semibold mb-1">Scan User ABHA QR Result</div>';
|
|
cardHtml += '<div class="small text-muted mb-2">Queue ID: ' + (queueId || '-') + '</div>';
|
|
cardHtml += '<div class="small mb-1"><strong>Name:</strong> ' + ((identity.patient_name || '-') + '') + '</div>';
|
|
cardHtml += '<div class="small mb-1"><strong>ABHA:</strong> ' + ((identity.abha_number || identity.abha_address || '-') + '') + '</div>';
|
|
cardHtml += '<div class="small mb-2"><strong>Phone:</strong> ' + ((identity.phone || '-') + '') + '</div>';
|
|
|
|
if (matches.length > 0) {
|
|
cardHtml += '<div class="small fw-semibold mb-1">Possible Matching HMS Patients</div>';
|
|
matches.forEach(function (m) {
|
|
var conf = m.match_confidence || 'low';
|
|
var confLabels = { definitive: 'Definitive', high: 'High', medium: 'Medium', low: 'Low' };
|
|
var confBadgeClass = { definitive: 'bg-success', high: 'bg-primary', medium: 'bg-warning text-dark', low: 'bg-secondary' };
|
|
var confBadge = '<span class="badge ' + (confBadgeClass[conf] || 'bg-secondary') + ' ms-1 small">' + (confLabels[conf] || 'Low') + '</span>';
|
|
var reasons = Array.isArray(m.match_reasons) ? m.match_reasons.join(', ') : '';
|
|
var genderLabel = m.gender == 2 ? 'F' : m.gender == 1 ? 'M' : '-';
|
|
var borderClass = conf === 'definitive' ? 'border-success' : conf === 'high' ? 'border-primary' : conf === 'medium' ? 'border-warning' : 'border-secondary';
|
|
var mLabel = ((m.p_code || 'UHID') + ' - ' + (m.p_fname || 'Unnamed'));
|
|
cardHtml += '<div class="border rounded p-1 mb-1 bg-white ' + borderClass + '">';
|
|
cardHtml += '<div class="d-flex justify-content-between align-items-start">';
|
|
cardHtml += '<div class="small"><span class="fw-semibold">' + mLabel + '</span>' + confBadge + '</div>';
|
|
cardHtml += '<button type="button" class="btn btn-sm btn-outline-primary ms-2" data-scan-use-existing="1" data-patient-id="' + (m.id || 0) + '">Link</button>';
|
|
cardHtml += '</div>';
|
|
cardHtml += '<div class="small text-muted">DOB: ' + (m.dob || '-') + ' | Gender: ' + genderLabel + ' | Phone: ' + (m.mphone1 || '-') + '</div>';
|
|
cardHtml += '<div class="small text-muted fst-italic">Matched by: ' + reasons + '</div>';
|
|
cardHtml += '</div>';
|
|
});
|
|
// Add "Create New Patient" as a selectable card option at the end of the list
|
|
cardHtml += '<div class="border rounded border-2 border-secondary p-1 mb-1 bg-white">';
|
|
cardHtml += '<div class="d-flex justify-content-between align-items-start">';
|
|
cardHtml += '<div class="small"><span class="fw-semibold text-secondary">None of the above - Register as New Patient</span></div>';
|
|
cardHtml += '<button type="button" class="btn btn-sm btn-success ms-2" data-scan-create-new="1">Create New</button>';
|
|
cardHtml += '</div>';
|
|
cardHtml += '<div class="small text-muted">This ABHA does not belong to any listed record.</div>';
|
|
cardHtml += '</div>';
|
|
} else {
|
|
cardHtml += '<div class="small text-muted mb-2">No existing patient matched from HMS demographics or ABHA.</div>';
|
|
}
|
|
|
|
cardHtml += '<div class="d-flex gap-2 mt-2">';
|
|
cardHtml += '<button type="button" class="btn btn-sm btn-success" id="btnScanCreateNewPatient">Create New Patient</button>';
|
|
cardHtml += '<button type="button" class="btn btn-sm btn-outline-secondary" id="btnScanClearPane">Clear</button>';
|
|
cardHtml += '</div>';
|
|
cardHtml += '</div>';
|
|
|
|
scanResolvePane.innerHTML = cardHtml;
|
|
scanResolvePane.classList.remove('d-none');
|
|
|
|
scanResolvePane.querySelectorAll('[data-scan-use-existing="1"]').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var patientId = parseInt(btn.getAttribute('data-patient-id') || '0', 10);
|
|
if (!patientId) {
|
|
return;
|
|
}
|
|
setSandboxStatus('Linking selected HMS patient...', 'muted');
|
|
post('<?= base_url('AbdmGateway/scan_share_link_patient') ?>/' + queueId, {
|
|
action: 'link_existing',
|
|
existing_patient_id: patientId,
|
|
abha_number: (identity.abha_number || ''),
|
|
abha_address: (identity.abha_address || ''),
|
|
patient_name: (identity.patient_name || ''),
|
|
phone: (identity.phone || ''),
|
|
gender: (identity.gender || ''),
|
|
dob: (identity.dob || '')
|
|
}, function (resp) {
|
|
if (!resp || parseInt(resp.ok || 0, 10) !== 1) {
|
|
setSandboxStatus((resp && resp.error_text) ? resp.error_text : 'Unable to link patient.', 'danger');
|
|
return;
|
|
}
|
|
clearScanResolvePane();
|
|
setSandboxStatus('Patient linked: ' + (resp.p_code || resp.patient_id || '-') + '. You can open profile now.', 'success');
|
|
});
|
|
});
|
|
});
|
|
|
|
scanResolvePane.querySelectorAll('[data-scan-create-new="1"]').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
setSandboxStatus('Creating new patient from scan result...', 'muted');
|
|
post('<?= base_url('AbdmGateway/scan_share_link_patient') ?>/' + queueId, {
|
|
action: 'create_new',
|
|
abha_number: (identity.abha_number || ''),
|
|
abha_address: (identity.abha_address || ''),
|
|
patient_name: (identity.patient_name || ''),
|
|
phone: (identity.phone || ''),
|
|
gender: (identity.gender || ''),
|
|
dob: (identity.dob || '')
|
|
}, function (resp) {
|
|
if (!resp || parseInt(resp.ok || 0, 10) !== 1) {
|
|
setSandboxStatus((resp && resp.error_text) ? resp.error_text : 'Unable to create patient.', 'danger');
|
|
return;
|
|
}
|
|
clearScanResolvePane();
|
|
setSandboxStatus('New patient created: ' + (resp.p_code || resp.patient_id || '-') + '.', 'success');
|
|
});
|
|
});
|
|
});
|
|
|
|
var createBtn = document.getElementById('btnScanCreateNewPatient');
|
|
if (createBtn) {
|
|
createBtn.addEventListener('click', function () {
|
|
setSandboxStatus('Creating new patient from scan result...', 'muted');
|
|
post('<?= base_url('AbdmGateway/scan_share_link_patient') ?>/' + queueId, {
|
|
action: 'create_new',
|
|
abha_number: (identity.abha_number || ''),
|
|
abha_address: (identity.abha_address || ''),
|
|
patient_name: (identity.patient_name || ''),
|
|
phone: (identity.phone || ''),
|
|
gender: (identity.gender || ''),
|
|
dob: (identity.dob || '')
|
|
}, function (resp) {
|
|
if (!resp || parseInt(resp.ok || 0, 10) !== 1) {
|
|
setSandboxStatus((resp && resp.error_text) ? resp.error_text : 'Unable to create patient.', 'danger');
|
|
return;
|
|
}
|
|
clearScanResolvePane();
|
|
setSandboxStatus('New patient created: ' + (resp.p_code || resp.patient_id || '-') + '.', 'success');
|
|
});
|
|
});
|
|
}
|
|
|
|
var clearBtn = document.getElementById('btnScanClearPane');
|
|
if (clearBtn) {
|
|
clearBtn.addEventListener('click', function () {
|
|
clearScanResolvePane();
|
|
setSandboxStatus('Ready', 'muted');
|
|
});
|
|
}
|
|
}
|
|
|
|
function currentSandboxContext() {
|
|
if (!sandboxRow) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
taskId: parseInt(sandboxRow.getAttribute('data-task-id') || '0', 10) || 0,
|
|
patientId: parseInt(sandboxRow.getAttribute('data-patient-id') || '0', 10) || 0,
|
|
opdId: parseInt(sandboxRow.getAttribute('data-opd-id') || '0', 10) || 0,
|
|
opdSessionId: parseInt(sandboxRow.getAttribute('data-opd-session-id') || '0', 10) || 0,
|
|
abhaId: (sandboxAbhaInput.value || '').trim()
|
|
};
|
|
}
|
|
|
|
function openSandboxForRow(row) {
|
|
if (!row || !sandboxCard) {
|
|
return;
|
|
}
|
|
if ((row.getAttribute('data-sandbox-eligible') || '0') !== '1') {
|
|
setSandboxStatus('Sandbox actions are available only for OPD Publish rows with printed consult.', 'warning');
|
|
return;
|
|
}
|
|
|
|
sandboxRow = row;
|
|
|
|
var taskCode = '';
|
|
var patientName = '';
|
|
var taskCodeEl = row.querySelector('td:nth-child(2) strong');
|
|
var patientNameEl = row.querySelector('td:nth-child(3) div');
|
|
if (taskCodeEl) {
|
|
taskCode = taskCodeEl.textContent.trim();
|
|
}
|
|
if (patientNameEl) {
|
|
patientName = patientNameEl.textContent.trim();
|
|
}
|
|
|
|
var rowAbha = (row.getAttribute('data-abha-id') || '').trim();
|
|
sandboxAbhaInput.value = rowAbha;
|
|
sandboxQrPayloadInput.value = '';
|
|
sandboxPurposeCodeInput.value = sandboxPurposeCodeInput.value || 'CAREMGT';
|
|
sandboxConsentHandleInput.value = '';
|
|
sandboxGatewayConsentIdInput.value = '';
|
|
sandboxClaimDocumentInput.value = '';
|
|
clearScanResolvePane();
|
|
|
|
var opdId = row.getAttribute('data-opd-id') || '0';
|
|
var opdSessionId = row.getAttribute('data-opd-session-id') || '0';
|
|
sandboxContext.textContent = 'Task: ' + taskCode + ' | Patient: ' + patientName + ' | OPD: ' + opdId + ' | Session: ' + opdSessionId;
|
|
sandboxCard.classList.remove('d-none');
|
|
setSandboxStatus('Ready');
|
|
sandboxCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}
|
|
|
|
function runAction(row, action, overrideAbha) {
|
|
var taskId = row.getAttribute('data-task-id');
|
|
var abha = (overrideAbha || modalAbhaInput.value || '').trim();
|
|
|
|
setStatus('Submitting action...');
|
|
post('<?= base_url('AbdmTaskBoard/perform_action') ?>', {
|
|
task_id: taskId,
|
|
action: action,
|
|
abha_id: abha,
|
|
opd_session_id: ''
|
|
}, function (res) {
|
|
if (res.ok !== 1) {
|
|
setStatus(res.error_text || 'Action failed', true);
|
|
return;
|
|
}
|
|
|
|
var badge = row.querySelector('.status-pill');
|
|
if (badge) {
|
|
badge.textContent = 'IN_PROGRESS';
|
|
badge.className = 'badge bg-info status-pill';
|
|
}
|
|
setStatus('Action queued: ' + (res.event_type || '-'));
|
|
if (modalEl && modalEl.classList.contains('show')) {
|
|
actionModal.hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
function openTaskFhirPreviewForSubmit(row) {
|
|
if (!row) {
|
|
return false;
|
|
}
|
|
|
|
var taskType = (row.getAttribute('data-task-type') || '').toLowerCase();
|
|
var abhaInput = row.querySelector('.abha-input');
|
|
var abhaId = (abhaInput ? abhaInput.value : (row.getAttribute('data-abha-id') || '')).trim();
|
|
|
|
if (!/^\d{14}$/.test(abhaId)) {
|
|
setStatus('ABHA must be a 14-digit number before FHIR preview.', true);
|
|
return true;
|
|
}
|
|
|
|
var previewUrl = '';
|
|
var title = 'FHIR Preview';
|
|
|
|
if (taskType === 'lab_report_publish' || taskType === 'radiology_report_publish') {
|
|
var labReqId = parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0;
|
|
var patientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0;
|
|
|
|
if (labReqId <= 0 || patientId <= 0) {
|
|
setStatus('Task data missing for FHIR preview.', true);
|
|
return true;
|
|
}
|
|
|
|
previewUrl = '<?= base_url('AbdmGateway/diagnosis_report_fhir_preview') ?>'
|
|
+ '?lab_req_id=' + encodeURIComponent(labReqId)
|
|
+ '&patient_id=' + encodeURIComponent(patientId)
|
|
+ '&abha_id=' + encodeURIComponent(abhaId);
|
|
title = (taskType === 'radiology_report_publish' ? 'Radiology' : 'Lab') + ' FHIR Preview - Request #' + labReqId;
|
|
} else if (taskType === 'opd_prescription_publish') {
|
|
var opdId = parseInt(row.getAttribute('data-opd-id') || row.getAttribute('data-entity-id') || '0', 10) || 0;
|
|
var opdSessionId = parseInt(row.getAttribute('data-opd-session-id') || '0', 10) || 0;
|
|
|
|
if (opdId <= 0) {
|
|
setStatus('Task data missing for OPD FHIR preview.', true);
|
|
return true;
|
|
}
|
|
|
|
previewUrl = opdSessionId > 0
|
|
? '<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + opdId + '/' + opdSessionId
|
|
: '<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + opdId;
|
|
title = 'OPD FHIR Preview - OPD #' + opdId;
|
|
} else if (taskType === 'ipd_discharge_publish') {
|
|
var ipdId = parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0;
|
|
var ipdPatientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0;
|
|
|
|
if (ipdId <= 0 || ipdPatientId <= 0) {
|
|
setStatus('Task data missing for IPD discharge FHIR preview.', true);
|
|
return true;
|
|
}
|
|
|
|
previewUrl = '<?= base_url('AbdmGateway/ipd_discharge_fhir_preview') ?>'
|
|
+ '?ipd_id=' + encodeURIComponent(ipdId)
|
|
+ '&patient_id=' + encodeURIComponent(ipdPatientId)
|
|
+ '&abha_id=' + encodeURIComponent(abhaId);
|
|
title = 'IPD Discharge FHIR Preview - IPD #' + ipdId;
|
|
} else if (taskType === 'immunization_record_publish') {
|
|
var immunizationRecordId = parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0;
|
|
var immunizationPatientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0;
|
|
|
|
if (immunizationRecordId <= 0 || immunizationPatientId <= 0) {
|
|
setStatus('Task data missing for Immunization FHIR preview.', true);
|
|
return true;
|
|
}
|
|
|
|
previewUrl = '<?= base_url('AbdmGateway/immunization_fhir_preview') ?>'
|
|
+ '?record_id=' + encodeURIComponent(immunizationRecordId)
|
|
+ '&patient_id=' + encodeURIComponent(immunizationPatientId)
|
|
+ '&abha_id=' + encodeURIComponent(abhaId);
|
|
title = 'ImmunizationRecord FHIR Preview - Record #' + immunizationRecordId;
|
|
} else if (taskType === 'health_document_publish') {
|
|
var healthDocId = parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0;
|
|
var healthPatientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0;
|
|
var healthEntityType = (row.getAttribute('data-entity-type') || '').trim();
|
|
|
|
if (healthDocId <= 0 || healthPatientId <= 0) {
|
|
setStatus('Task data missing for Health Document FHIR preview.', true);
|
|
return true;
|
|
}
|
|
|
|
previewUrl = '<?= base_url('DoctorDocument/health_document_fhir_preview') ?>/' + healthDocId
|
|
+ '?entity_type=' + encodeURIComponent(healthEntityType)
|
|
+ '&patient_id=' + encodeURIComponent(healthPatientId)
|
|
+ '&abha_id=' + encodeURIComponent(abhaId);
|
|
title = 'HealthDocumentRecord FHIR Preview - Document #' + healthDocId;
|
|
} else if (taskType === 'wellness_record_publish') {
|
|
var wellnessPatientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0;
|
|
var wellnessOpdId = parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0;
|
|
|
|
if (wellnessPatientId <= 0) {
|
|
setStatus('Task data missing for Wellness Record FHIR preview.', true);
|
|
return true;
|
|
}
|
|
|
|
previewUrl = '<?= base_url('DoctorDocument/wellness_record_fhir_preview') ?>/' + wellnessPatientId + (wellnessOpdId > 0 ? ('/' + wellnessOpdId) : '');
|
|
title = 'WellnessRecord FHIR Preview - Patient #' + wellnessPatientId;
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
_fhirSubmitMode = 'task';
|
|
_fhirSubmitTaskRow = row;
|
|
_fhirSubmitAbha = abhaId;
|
|
|
|
openFhirModal(previewUrl, title, {
|
|
submitMode: 'task',
|
|
taskRow: row,
|
|
abhaId: abhaId
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
document.querySelectorAll('.action-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
selectedRow = btn.closest('tr');
|
|
if (!selectedRow) {
|
|
return;
|
|
}
|
|
selectedAction = btn.getAttribute('data-action') || 'submit';
|
|
|
|
if (selectedAction === 'submit') {
|
|
var previewHandled = openTaskFhirPreviewForSubmit(selectedRow);
|
|
if (previewHandled) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
var abhaInput = selectedRow.querySelector('.abha-input');
|
|
var taskTypeLabel = selectedRow.getAttribute('data-task-type') || '';
|
|
modalAbhaInput.value = abhaInput ? abhaInput.value.trim() : '';
|
|
modalTaskSummary.textContent = 'Task: ' + taskTypeLabel + ' | Action: ' + selectedAction.toUpperCase();
|
|
actionModal.show();
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.immunization-bridge-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var row = btn.closest('tr');
|
|
var abhaInput = row ? row.querySelector('.abha-input') : null;
|
|
var abhaId = (abhaInput ? abhaInput.value : '').trim();
|
|
var patientId = row ? (parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0) : 0;
|
|
var recordId = row ? (parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0) : 0;
|
|
var taskId = row ? (parseInt(row.getAttribute('data-task-id') || '0', 10) || 0) : 0;
|
|
|
|
if (!/^\d{14}$/.test(abhaId) || patientId <= 0 || recordId <= 0) {
|
|
setStatus('A 14-digit ABHA and valid Immunization record are required.', true);
|
|
return;
|
|
}
|
|
|
|
var original = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Pushing';
|
|
post('<?= base_url('AbdmGateway/share_immunization_bundle') ?>', {
|
|
task_id: taskId,
|
|
patient_id: patientId,
|
|
record_id: recordId,
|
|
abha_id: abhaId,
|
|
push_to_gateway: 1
|
|
}, function (res) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = original;
|
|
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
|
setStatus((res && (res.error_text || res.error || res.message)) || 'ABDM Bridge push failed.', true);
|
|
return;
|
|
}
|
|
|
|
var badge = row.querySelector('.status-pill');
|
|
if (badge) {
|
|
badge.textContent = 'BRIDGE SUBMITTED';
|
|
badge.className = 'badge bg-info status-pill';
|
|
}
|
|
btn.disabled = true;
|
|
btn.textContent = 'Submitted to Bridge';
|
|
btn.title = 'Submitted to ABDM Bridge: ' + (res.care_context_reference || '-');
|
|
setStatus('Submitted to ABDM Bridge. Queue ID: ' + (res.queue_id || '-') + '; care context: ' + (res.care_context_reference || '-'));
|
|
});
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.filter-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
applyFilter(btn.getAttribute('data-filter') || 'all');
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.sandbox-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
openSandboxForRow(btn.closest('tr'));
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.preview-fhir-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var row = btn.closest('tr');
|
|
if (!row) {
|
|
return;
|
|
}
|
|
|
|
var handled = openTaskFhirPreviewForSubmit(row);
|
|
if (handled) {
|
|
return;
|
|
}
|
|
|
|
var opdId = parseInt(row.getAttribute('data-opd-id') || '0', 10) || 0;
|
|
var opdSessionId = parseInt(row.getAttribute('data-opd-session-id') || '0', 10) || 0;
|
|
if (opdId <= 0) {
|
|
setStatus('FHIR preview unavailable for this row.', true);
|
|
return;
|
|
}
|
|
|
|
var previewUrl = opdSessionId > 0
|
|
? '<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + opdId + '/' + opdSessionId
|
|
: '<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + opdId;
|
|
showInlineFhirPreview(previewUrl, 'Task Row OPD ' + opdId + (opdSessionId > 0 ? (' / Session ' + opdSessionId) : ''));
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.inline-json-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var previewUrl = (btn.getAttribute('data-preview-url') || '').trim();
|
|
var titleText = (btn.getAttribute('data-title') || 'FHIR JSON Debug').trim();
|
|
showInlineFhirPreview(previewUrl, titleText);
|
|
});
|
|
});
|
|
|
|
applyFilter('dashboard');
|
|
|
|
modalConfirmBtn.addEventListener('click', function () {
|
|
if (!selectedRow || !selectedAction) {
|
|
return;
|
|
}
|
|
var abha = modalAbhaInput.value.trim();
|
|
if (!/^\d{14}$/.test(abha)) {
|
|
setStatus('ABHA must be a 14-digit number.', true);
|
|
return;
|
|
}
|
|
var rowInput = selectedRow.querySelector('.abha-input');
|
|
if (rowInput) {
|
|
rowInput.value = abha;
|
|
}
|
|
runAction(selectedRow, selectedAction);
|
|
});
|
|
|
|
document.querySelectorAll('.close-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var row = btn.closest('tr');
|
|
if (!row) {
|
|
return;
|
|
}
|
|
var taskId = row.getAttribute('data-task-id');
|
|
post('<?= base_url('AbdmTaskBoard/mark_status') ?>', {
|
|
task_id: taskId,
|
|
status: 'completed',
|
|
note: 'Closed from task board'
|
|
}, function (res) {
|
|
if (res.ok !== 1) {
|
|
setStatus(res.error_text || 'Close failed', true);
|
|
return;
|
|
}
|
|
if (sandboxRow === row && sandboxCard) {
|
|
sandboxCard.classList.add('d-none');
|
|
sandboxRow = null;
|
|
}
|
|
row.remove();
|
|
setStatus('Task closed');
|
|
});
|
|
});
|
|
});
|
|
|
|
document.getElementById('btnSandboxHide').addEventListener('click', function () {
|
|
if (sandboxCard) {
|
|
sandboxCard.classList.add('d-none');
|
|
}
|
|
sandboxRow = null;
|
|
clearScanResolvePane();
|
|
});
|
|
|
|
function pollScanLookupStatus(queueId, onTick, onDone) {
|
|
if (!queueId || queueId <= 0) {
|
|
if (typeof onDone === 'function') {
|
|
onDone({ ok: 0, status: 'unknown', error_text: 'Missing queue id' });
|
|
}
|
|
return;
|
|
}
|
|
|
|
var tries = 0;
|
|
var maxTries = 10;
|
|
var timer = setInterval(function () {
|
|
tries++;
|
|
$.getJSON('<?= base_url('AbdmGateway/scan_share_lookup_status') ?>/' + queueId, function (resp) {
|
|
if (typeof onTick === 'function') {
|
|
onTick(resp, tries, maxTries);
|
|
}
|
|
if (!resp || parseInt(resp.ok || 0, 10) !== 1) {
|
|
if (tries >= maxTries) {
|
|
clearInterval(timer);
|
|
if (typeof onDone === 'function') {
|
|
onDone(resp || { ok: 0, status: 'failed', error_text: 'Status lookup failed' });
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
var status = (resp.status || '').toString().toLowerCase();
|
|
if (parseInt(resp.done || 0, 10) === 1 || status === 'sent' || status === 'failed') {
|
|
clearInterval(timer);
|
|
if (typeof onDone === 'function') {
|
|
onDone(resp);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (tries >= maxTries) {
|
|
clearInterval(timer);
|
|
if (typeof onDone === 'function') {
|
|
onDone(resp);
|
|
}
|
|
}
|
|
}).fail(function () {
|
|
if (tries >= maxTries) {
|
|
clearInterval(timer);
|
|
if (typeof onDone === 'function') {
|
|
onDone({ ok: 0, status: 'failed', error_text: 'Status polling failed' });
|
|
}
|
|
}
|
|
});
|
|
}, 3000);
|
|
}
|
|
|
|
document.getElementById('tb_btn_abdm_scan_lookup').addEventListener('click', function () {
|
|
var qrPayload = (sandboxQrPayloadInput.value || '').trim();
|
|
if (!qrPayload) {
|
|
setSandboxStatus('Paste QR payload first.', 'warning');
|
|
return;
|
|
}
|
|
setSandboxStatus('Queuing Scan & Share lookup...');
|
|
post('<?= base_url('AbdmGateway/scan_share_lookup') ?>', {
|
|
qr_payload: qrPayload
|
|
}, function (res) {
|
|
if (res.ok !== 1) {
|
|
setSandboxStatus(res.error_text || 'Scan lookup failed.', 'danger');
|
|
return;
|
|
}
|
|
if (res.abha_id_hint) {
|
|
sandboxAbhaInput.value = (res.abha_id_hint || '').toString().trim();
|
|
}
|
|
var queueId = parseInt(res.queue_id || '0', 10);
|
|
setSandboxStatus('Scan lookup queued. Queue ID: ' + (queueId || '-') + '. Checking status...', 'success');
|
|
|
|
pollScanLookupStatus(queueId, function (scanResp, currentTry, maxTry) {
|
|
if (!scanResp || parseInt(scanResp.ok || 0, 10) !== 1) {
|
|
setSandboxStatus('Scan lookup status check (' + currentTry + '/' + maxTry + ')...', 'warning');
|
|
return;
|
|
}
|
|
var st = (scanResp.status || 'pending').toString();
|
|
setSandboxStatus('Scan lookup status: ' + st.toUpperCase() + ' (' + currentTry + '/' + maxTry + ')', st === 'failed' ? 'danger' : 'muted');
|
|
}, function (finalResp) {
|
|
if (!finalResp || parseInt(finalResp.ok || 0, 10) !== 1) {
|
|
setSandboxStatus((finalResp && finalResp.error_text) ? finalResp.error_text : 'Scan lookup status unresolved. Please check Bridge Log.', 'danger');
|
|
return;
|
|
}
|
|
var fs = (finalResp.status || '').toString().toLowerCase();
|
|
if (fs === 'sent') {
|
|
setSandboxStatus('Scan lookup sent to bridge successfully. Queue ID: ' + queueId + '. Resolving patient...', 'success');
|
|
$.getJSON('<?= base_url('AbdmGateway/scan_share_resolve_patient') ?>/' + queueId, function (resolveResp) {
|
|
if (!resolveResp || parseInt(resolveResp.ok || 0, 10) !== 1) {
|
|
setSandboxStatus((resolveResp && resolveResp.error_text) ? resolveResp.error_text : 'Scan result received but patient data not available yet.', 'warning');
|
|
clearScanResolvePane();
|
|
return;
|
|
}
|
|
|
|
if (resolveResp.identity && resolveResp.identity.abha_number) {
|
|
sandboxAbhaInput.value = (resolveResp.identity.abha_number || '').toString();
|
|
}
|
|
|
|
renderScanResolvePane(queueId, resolveResp.identity || {}, resolveResp.matches || []);
|
|
setSandboxStatus('Resolve patient using scan result below.', 'success');
|
|
}).fail(function () {
|
|
setSandboxStatus('Scan lookup sent, but patient resolution fetch failed.', 'warning');
|
|
});
|
|
} else if (fs === 'failed') {
|
|
setSandboxStatus('Scan lookup failed: ' + (finalResp.last_error || 'Unknown bridge error'), 'danger');
|
|
clearScanResolvePane();
|
|
} else {
|
|
setSandboxStatus('Scan lookup is still ' + fs.toUpperCase() + '. You can recheck from Bridge Log.', 'warning');
|
|
clearScanResolvePane();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
document.getElementById('tb_btn_abdm_validate_abha').addEventListener('click', function () {
|
|
var abha = (sandboxAbhaInput.value || '').trim();
|
|
if (!/^\d{14}$/.test(abha)) {
|
|
setSandboxStatus('ABHA must be a 14-digit number.', 'warning');
|
|
return;
|
|
}
|
|
setSandboxStatus('Queuing ABHA validation...');
|
|
post('<?= base_url('AbdmGateway/abha_validate') ?>', {
|
|
abha_id: abha
|
|
}, function (res) {
|
|
if (res.ok !== 1) {
|
|
setSandboxStatus(res.error_text || 'ABHA validation queue failed.', 'danger');
|
|
return;
|
|
}
|
|
setSandboxStatus('ABHA validation queued. Queue ID: ' + (res.queue_id || '-'), 'success');
|
|
});
|
|
});
|
|
|
|
document.getElementById('tb_btn_abdm_consent_request').addEventListener('click', function () {
|
|
var ctx = currentSandboxContext();
|
|
if (!ctx || ctx.patientId <= 0) {
|
|
setSandboxStatus('Select an eligible OPD Publish row first.', 'warning');
|
|
return;
|
|
}
|
|
if (!/^\d{14}$/.test(ctx.abhaId)) {
|
|
setSandboxStatus('ABHA must be a 14-digit number.', 'warning');
|
|
return;
|
|
}
|
|
|
|
setSandboxStatus('Requesting consent...');
|
|
post('<?= base_url('AbdmGateway/consent_request') ?>', {
|
|
patient_id: ctx.patientId,
|
|
abha_id: ctx.abhaId,
|
|
purpose_code: (sandboxPurposeCodeInput.value || '').trim(),
|
|
expires_at: (sandboxConsentExpiresInput.value || '').trim().replace('T', ' ')
|
|
}, function (res) {
|
|
if (res.ok !== 1) {
|
|
setSandboxStatus(res.error_text || 'Consent request failed.', 'danger');
|
|
return;
|
|
}
|
|
if (res.consent_handle) {
|
|
sandboxConsentHandleInput.value = (res.consent_handle || '').toString();
|
|
}
|
|
if (res.gateway_consent_id) {
|
|
sandboxGatewayConsentIdInput.value = (res.gateway_consent_id || '').toString();
|
|
}
|
|
var statusText = 'Consent requested. Handle: ' + (res.consent_handle || '-');
|
|
if (res.gateway_consent_id) {
|
|
statusText += ' | Consent ID: ' + res.gateway_consent_id;
|
|
}
|
|
setSandboxStatus(statusText, 'success');
|
|
});
|
|
});
|
|
|
|
document.getElementById('tb_btn_abdm_share_fhir').addEventListener('click', function () {
|
|
var ctx = currentSandboxContext();
|
|
if (!ctx || ctx.patientId <= 0 || ctx.opdId <= 0) {
|
|
setSandboxStatus('Select an eligible OPD Publish row first.', 'warning');
|
|
return;
|
|
}
|
|
if (!/^\d{14}$/.test(ctx.abhaId)) {
|
|
setSandboxStatus('ABHA must be a 14-digit number.', 'warning');
|
|
return;
|
|
}
|
|
|
|
setSandboxStatus('Checking consent and queuing FHIR share...');
|
|
post('<?= base_url('AbdmGateway/share_prescription_bundle') ?>', {
|
|
opd_id: ctx.opdId,
|
|
opd_session_id: ctx.opdSessionId,
|
|
patient_id: ctx.patientId,
|
|
abha_id: ctx.abhaId,
|
|
consent_handle: (sandboxConsentHandleInput.value || '').trim()
|
|
}, function (res) {
|
|
if (res.ok !== 1) {
|
|
setSandboxStatus(res.error_text || 'FHIR share queue failed.', 'danger');
|
|
return;
|
|
}
|
|
if (res.consent_handle) {
|
|
sandboxConsentHandleInput.value = (res.consent_handle || '').toString();
|
|
}
|
|
setSandboxStatus('FHIR share queued. Queue ID: ' + (res.queue_id || '-'), 'success');
|
|
});
|
|
});
|
|
|
|
document.getElementById('tb_btn_abdm_preview_fhir').addEventListener('click', function () {
|
|
var ctx = currentSandboxContext();
|
|
if (!ctx || ctx.opdId <= 0) {
|
|
setSandboxStatus('Select an eligible OPD Publish row first.', 'warning');
|
|
return;
|
|
}
|
|
|
|
var previewUrl = ctx.opdSessionId > 0
|
|
? '<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + ctx.opdId + '/' + ctx.opdSessionId
|
|
: '<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + ctx.opdId;
|
|
|
|
window.open(previewUrl, '_blank');
|
|
setSandboxStatus('Opened FHIR preview for OPD ' + ctx.opdId + (ctx.opdSessionId > 0 ? (' / Session ' + ctx.opdSessionId) : ''), 'success');
|
|
});
|
|
|
|
document.getElementById('tb_btn_abdm_claim_status').addEventListener('click', function () {
|
|
var docId = parseInt((sandboxClaimDocumentInput.value || '0').toString(), 10) || 0;
|
|
if (docId <= 0) {
|
|
setSandboxStatus('Enter Claim Document ID first.', 'warning');
|
|
return;
|
|
}
|
|
|
|
setSandboxStatus('Queuing claim status request...');
|
|
post('<?= base_url('AbdmGateway/nhcx_claim_status_request') ?>', {
|
|
document_id: docId
|
|
}, function (res) {
|
|
if (res.ok !== 1) {
|
|
setSandboxStatus(res.error_text || 'Claim status queue failed.', 'danger');
|
|
return;
|
|
}
|
|
setSandboxStatus('Claim status queued. Queue ID: ' + (res.queue_id || '-'), 'success');
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('[data-hi-share]').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var kind = btn.getAttribute('data-hi-share');
|
|
var patientId = parseInt((document.getElementById('tb_hi_patient_id').value || '0'), 10) || 0;
|
|
var opdId = parseInt((document.getElementById('tb_hi_opd_id').value || '0'), 10) || 0;
|
|
var invoiceId = parseInt((document.getElementById('tb_hi_invoice_id').value || '0'), 10) || 0;
|
|
|
|
if (kind !== 'invoice' && patientId <= 0) {
|
|
setSandboxStatus('Enter Patient ID first.', 'warning');
|
|
return;
|
|
}
|
|
if (kind === 'invoice' && invoiceId <= 0) {
|
|
setSandboxStatus('Enter Invoice ID first.', 'warning');
|
|
return;
|
|
}
|
|
|
|
var urls = {
|
|
immunization: '<?= base_url('AbdmGateway/share_immunization_bundle') ?>',
|
|
wellness: '<?= base_url('AbdmGateway/share_wellness_bundle') ?>',
|
|
health_document: '<?= base_url('AbdmGateway/share_health_document_bundle') ?>',
|
|
invoice: '<?= base_url('AbdmGateway/share_invoice_bundle') ?>'
|
|
};
|
|
|
|
var payload = { patient_id: patientId, consent_handle: '' };
|
|
if (kind === 'wellness') { payload.opd_id = opdId; }
|
|
if (kind === 'invoice') { payload.invoice_id = invoiceId; }
|
|
|
|
var original = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Sharing';
|
|
setSandboxStatus('Sharing ' + kind + ' record...');
|
|
|
|
post(urls[kind], payload, function (res) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = original;
|
|
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
|
setSandboxStatus(res.error_text || res.error || ('Failed to share ' + kind + ' record.'), 'danger');
|
|
return;
|
|
}
|
|
setSandboxStatus(kind + ' shared. Care context: ' + (res.care_context_reference || '-')
|
|
+ (res.queue_id ? (' | Queue: ' + res.queue_id) : ''), 'success');
|
|
});
|
|
});
|
|
});
|
|
|
|
document.getElementById('btnHideFhirDebug').addEventListener('click', function () {
|
|
if (fhirDebugCard) {
|
|
fhirDebugCard.classList.add('d-none');
|
|
}
|
|
});
|
|
|
|
document.getElementById('btnCopyFhirDebug').addEventListener('click', function () {
|
|
var btn = this;
|
|
var originalText = btn.textContent;
|
|
var originalClass = btn.className;
|
|
var jsonText = fhirDebugJson ? (fhirDebugJson.textContent || '{}') : '{}';
|
|
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
|
setStatus('Clipboard API unavailable in this browser context.', true);
|
|
return;
|
|
}
|
|
navigator.clipboard.writeText(jsonText).then(function () {
|
|
setStatus('FHIR JSON copied to clipboard.');
|
|
btn.textContent = 'Copied v';
|
|
btn.className = 'btn btn-sm btn-success';
|
|
window.setTimeout(function () {
|
|
btn.textContent = originalText;
|
|
btn.className = originalClass;
|
|
}, 1500);
|
|
}).catch(function (e) {
|
|
setStatus('Copy failed: ' + e.message, true);
|
|
});
|
|
});
|
|
|
|
// ABDM Gateway Queue fetch for OPD Book
|
|
document.getElementById('btnFetchAbdmQueue').addEventListener('click', function () {
|
|
var date = (document.getElementById('opdBookDate').value || '').trim() || '<?= date('Y-m-d') ?>';
|
|
var statusEl = document.getElementById('abdmQueueStatus');
|
|
var badge = document.getElementById('abdmQueueBadge');
|
|
var tbody = document.getElementById('abdmQueueTbody');
|
|
if (statusEl) statusEl.textContent = 'Fetching...';
|
|
fetch('<?= base_url('AbdmOpdQueue/fetch') ?>?date=' + encodeURIComponent(date) + '&status=PENDING', {
|
|
method: 'GET',
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
|
|
}).then(function (r) { return r.json(); }).then(function (data) {
|
|
var tokens = data.data || data.tokens || [];
|
|
if (badge) badge.textContent = tokens.length;
|
|
if (statusEl) statusEl.textContent = 'Loaded ' + tokens.length + ' token(s)';
|
|
if (! tbody) return;
|
|
if (! tokens.length) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="text-muted px-2 py-2">No PENDING tokens for ' + date + '.</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = tokens.map(function (t) {
|
|
var abha = (t.abha_id || t.abha_number || t.abhaId || '').toString().trim();
|
|
var name = (t.patient_name || t.name || t.patientName || '').toString();
|
|
var stat = (t.status || t.queue_status || '').toString();
|
|
var token = (t.token_number || t.tokenNumber || t.gateway_token_id || t.id || '').toString();
|
|
var time = (t.created_at || t.token_time || t.arrival_time || '').toString();
|
|
var hms = t.patient_id ? ('#' + t.patient_id + (t.p_fname ? ' ' + t.p_fname : '')) : '<span class="text-muted">-</span>';
|
|
return '<tr>' +
|
|
'<td>' + (token || '-') + '</td>' +
|
|
'<td><span class="text-primary small">' + (abha || '<span class="text-muted">-</span>') + '</span></td>' +
|
|
'<td>' + (name || '-') + '</td>' +
|
|
'<td><span class="badge bg-' + (stat === 'PENDING' ? 'warning text-dark' : 'secondary') + '">' + (stat || '-') + '</span></td>' +
|
|
'<td class="small">' + (time ? time.substring(0, 16) : '-') + '</td>' +
|
|
'<td>' + hms + '</td>' +
|
|
'</tr>';
|
|
}).join('');
|
|
}).catch(function (e) {
|
|
if (statusEl) statusEl.textContent = 'Fetch failed: ' + e.message;
|
|
});
|
|
});
|
|
|
|
// FHIR Preview Modal
|
|
var fhirPreviewModalEl = document.getElementById('fhirPreviewModal');
|
|
var fhirPreviewModal = new bootstrap.Modal(fhirPreviewModalEl);
|
|
var fhirModalTitle = document.getElementById('fhirModalTitle');
|
|
var fhirModalMeta = document.getElementById('fhirModalMeta');
|
|
var fhirModalQuality = document.getElementById('fhirModalQuality');
|
|
var fhirModalJson = document.getElementById('fhirModalJson');
|
|
var fhirFormView = document.getElementById('fhirFormView');
|
|
var fhirTabForm = document.getElementById('fhirTabForm');
|
|
var fhirTabJson = document.getElementById('fhirTabJson');
|
|
var fhirModalDataBadge = document.getElementById('fhirModalDataBadge');
|
|
var fhirModalHttpBadge = document.getElementById('fhirModalHttpBadge');
|
|
var btnRegenerateFhirModal = document.getElementById('btnRegenerateFhirModal');
|
|
var btnUpdateFhirJson = document.getElementById('btnUpdateFhirJson');
|
|
var btnSubmitFhirToAbdm = document.getElementById('btnSubmitFhirToAbdm');
|
|
|
|
var defaultSubmitBtnHtml = btnSubmitFhirToAbdm ? btnSubmitFhirToAbdm.innerHTML : '';
|
|
var defaultSubmitBtnClass = btnSubmitFhirToAbdm ? btnSubmitFhirToAbdm.className : 'btn btn-sm btn-outline-success';
|
|
var defaultSubmitBtnTitle = btnSubmitFhirToAbdm ? btnSubmitFhirToAbdm.title : 'Push FHIR bundle to ABDM bridge';
|
|
|
|
// -- Tab switching --------------------------------------------------
|
|
fhirTabForm.addEventListener('click', function () {
|
|
fhirTabForm.classList.add('active');
|
|
fhirTabJson.classList.remove('active');
|
|
fhirFormView.style.display = '';
|
|
fhirModalJson.style.display = 'none';
|
|
|
|
// Auto-refresh Form View from JSON if valid
|
|
try {
|
|
var curText = (fhirModalJson ? fhirModalJson.value : '').trim();
|
|
if (curText && curText !== '{\n "loading": true\n}' && curText !== '{}') {
|
|
var p = JSON.parse(curText);
|
|
var b = (p && p.resourceType === 'Bundle') ? p : ((p && p.bundle) ? p.bundle : null);
|
|
if (b && fhirFormView) {
|
|
fhirFormView.innerHTML = renderFhirBundleForm(b, _fhirLiveComplaints, _fhirLiveDiagnoses);
|
|
if (fhirModalQuality) {
|
|
var qs = fhirCodeQuality(b);
|
|
if (qs) {
|
|
fhirModalQuality.innerHTML = qs;
|
|
fhirModalQuality.style.display = 'block';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
});
|
|
fhirTabJson.addEventListener('click', function () {
|
|
fhirTabJson.classList.add('active');
|
|
fhirTabForm.classList.remove('active');
|
|
fhirModalJson.style.display = '';
|
|
fhirFormView.style.display = 'none';
|
|
});
|
|
|
|
// -- Track current bundle context for edits ----------------------
|
|
var _fhirOpdId = 0, _fhirSessionId = 0;
|
|
var _fhirIpdId = 0, _fhirPatientId = 0, _fhirAbhaId = '';
|
|
var _fhirLiveComplaints = [], _fhirLiveDiagnoses = [];
|
|
var _fhirSubmitMode = 'opd';
|
|
var _fhirSubmitTaskRow = null;
|
|
var _fhirSubmitAbha = '';
|
|
var _fhirInvoiceSource = '';
|
|
var _fhirInvoiceBillId = 0;
|
|
var _fhirInvoicePatientId = 0;
|
|
var _fhirPreviewUrl = '';
|
|
|
|
// -- Inline complaint SNOMED edit (event delegation on fhirFormView) -
|
|
var _editTimer = null;
|
|
fhirFormView.addEventListener('click', function(e) {
|
|
var editBtn = e.target.closest('.fhir-complaint-edit');
|
|
var cancelBtn = e.target.closest('.fhir-complaint-cancel');
|
|
var saveBtn = e.target.closest('.fhir-complaint-save');
|
|
var ddItem = e.target.closest('.fhir-snomed-dd-item');
|
|
|
|
var diagEditBtn = e.target.closest('.fhir-diag-edit');
|
|
var diagCancelBtn = e.target.closest('.fhir-diag-cancel');
|
|
var diagSaveBtn = e.target.closest('.fhir-diag-save');
|
|
var diagDdItem = e.target.closest('.fhir-diag-dd-item');
|
|
|
|
if (editBtn) {
|
|
var panel = fhirFormView.querySelector('.fhir-edit-panel[data-idx="' + editBtn.dataset.idx + '"]');
|
|
if (!panel) return;
|
|
// Close others
|
|
fhirFormView.querySelectorAll('.fhir-edit-panel,.fhir-diag-panel').forEach(function(p) { if (p !== panel) p.style.display = 'none'; });
|
|
panel.style.display = panel.style.display === 'none' ? '' : 'none';
|
|
}
|
|
if (diagEditBtn) {
|
|
var dPanel = fhirFormView.querySelector('.fhir-diag-panel[data-didx="' + diagEditBtn.dataset.didx + '"]');
|
|
if (!dPanel) return;
|
|
fhirFormView.querySelectorAll('.fhir-edit-panel,.fhir-diag-panel').forEach(function(p) { if (p !== dPanel) p.style.display = 'none'; });
|
|
dPanel.style.display = dPanel.style.display === 'none' ? '' : 'none';
|
|
}
|
|
if (cancelBtn) {
|
|
cancelBtn.closest('.fhir-edit-panel').style.display = 'none';
|
|
}
|
|
if (diagCancelBtn) {
|
|
diagCancelBtn.closest('.fhir-diag-panel').style.display = 'none';
|
|
}
|
|
if (diagDdItem) {
|
|
var dPanel2 = diagDdItem.closest('.fhir-diag-panel');
|
|
dPanel2.querySelector('.fhir-diag-id').value = diagDdItem.dataset.id || '';
|
|
dPanel2.querySelector('.fhir-diag-term').value = diagDdItem.dataset.term || '';
|
|
dPanel2.querySelector('.fhir-diag-search').value = diagDdItem.dataset.term || '';
|
|
dPanel2.querySelector('.fhir-diag-dd').style.display = 'none';
|
|
var dSel = dPanel2.querySelector('.fhir-diag-selected');
|
|
dSel.textContent = '\u2713 ' + (diagDdItem.dataset.id || '') + ' \u2014 ' + (diagDdItem.dataset.term || '');
|
|
dSel.style.display = '';
|
|
}
|
|
if (ddItem) {
|
|
var panel2 = ddItem.closest('.fhir-edit-panel');
|
|
panel2.querySelector('.fhir-snomed-id').value = ddItem.dataset.id || '';
|
|
panel2.querySelector('.fhir-snomed-term').value = ddItem.dataset.term || '';
|
|
panel2.querySelector('.fhir-snomed-search').value = ddItem.dataset.term || '';
|
|
panel2.querySelector('.fhir-snomed-dd').style.display = 'none';
|
|
var sel = panel2.querySelector('.fhir-snomed-selected');
|
|
sel.textContent = 'v ' + (ddItem.dataset.id || '') + ' - ' + (ddItem.dataset.term || '');
|
|
sel.style.display = '';
|
|
}
|
|
if (saveBtn) {
|
|
var panel3 = saveBtn.closest('.fhir-edit-panel');
|
|
var sId = panel3.querySelector('.fhir-snomed-id').value.trim();
|
|
var sTerm = panel3.querySelector('.fhir-snomed-term').value.trim();
|
|
var cText = panel3.dataset.complaintText || '';
|
|
if (!sId && !sTerm) {
|
|
// Try the search field text as the term if nothing selected
|
|
sTerm = panel3.querySelector('.fhir-snomed-search').value.trim();
|
|
}
|
|
var csrfN = document.querySelector('meta[name="csrf-token-name"]');
|
|
var csrfH = document.querySelector('meta[name="csrf-token-hash"]');
|
|
var body = new URLSearchParams({
|
|
opd_id: _fhirOpdId,
|
|
opd_session_id: _fhirSessionId,
|
|
complaint_text: cText,
|
|
snomed_concept_id: sId,
|
|
snomed_term: sTerm
|
|
});
|
|
body.append(csrfName, csrfHash);
|
|
saveBtn.disabled = true;
|
|
saveBtn.textContent = 'Saving...';
|
|
fetch('<?= base_url('Opd_prescription/fhir_complaint_recode') ?>', {
|
|
method: 'POST',
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: body.toString()
|
|
})
|
|
.then(function(r){ return r.json(); })
|
|
.then(function(res){
|
|
if (res.update == 1) {
|
|
// Update the CSRF token for next request
|
|
if (res.csrfName && res.csrfHash) { csrfName = res.csrfName; csrfHash = res.csrfHash; }
|
|
// Update the displayed complaint badge inline
|
|
var li = panel3.closest('li');
|
|
if (li && sId) {
|
|
var existing = li.querySelector('.fhir-snomed-badge');
|
|
if (existing) { existing.textContent = 'SNOMED\u00a0' + sId; }
|
|
else {
|
|
var badge = document.createElement('span');
|
|
badge.className = 'badge bg-info text-dark ms-1 fhir-snomed-badge';
|
|
badge.style.fontSize = '.7rem';
|
|
badge.textContent = 'SNOMED\u00a0' + sId;
|
|
var strong = li.querySelector('strong');
|
|
if (strong) strong.insertAdjacentElement('afterend', badge);
|
|
}
|
|
}
|
|
panel3.style.display = 'none';
|
|
} else {
|
|
alert(res.error_text || 'Save failed');
|
|
}
|
|
})
|
|
.catch(function(e){ alert('Error: ' + e.message); })
|
|
.finally(function(){ saveBtn.disabled = false; saveBtn.textContent = 'Save'; });
|
|
}
|
|
if (diagSaveBtn) {
|
|
var dPanel3 = diagSaveBtn.closest('.fhir-diag-panel');
|
|
var dId = dPanel3.querySelector('.fhir-diag-id').value.trim();
|
|
var dTerm = dPanel3.querySelector('.fhir-diag-term').value.trim();
|
|
var dText = dPanel3.dataset.diagText || '';
|
|
var dType = dPanel3.dataset.diagType || 'provisional';
|
|
if (!dId && !dTerm) { dTerm = dPanel3.querySelector('.fhir-diag-search').value.trim(); }
|
|
var dBody = new URLSearchParams({
|
|
opd_id: _fhirOpdId,
|
|
opd_session_id: _fhirSessionId,
|
|
diagnosis_type: dType,
|
|
diagnosis_text: dText,
|
|
snomed_concept_id: dId,
|
|
snomed_term: dTerm
|
|
});
|
|
dBody.append(csrfName, csrfHash);
|
|
diagSaveBtn.disabled = true;
|
|
diagSaveBtn.textContent = 'Saving\u2026';
|
|
fetch('<?= base_url('Opd_prescription/fhir_diagnosis_recode') ?>', {
|
|
method: 'POST',
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: dBody.toString()
|
|
})
|
|
.then(function(r){ return r.json(); })
|
|
.then(function(res){
|
|
if (res.update == 1) {
|
|
if (res.csrfName && res.csrfHash) { csrfName = res.csrfName; csrfHash = res.csrfHash; }
|
|
var dLi = dPanel3.closest('li');
|
|
if (dLi && dId) {
|
|
var dExisting = dLi.querySelector('.fhir-diag-snomed-badge');
|
|
if (dExisting) { dExisting.textContent = 'SNOMED\u00a0' + dId; }
|
|
else {
|
|
var dBadge = document.createElement('span');
|
|
dBadge.className = 'badge bg-success ms-1 fhir-diag-snomed-badge';
|
|
dBadge.style.fontSize = '.7rem';
|
|
dBadge.textContent = 'SNOMED\u00a0' + dId;
|
|
var dStrong = dLi.querySelector('strong');
|
|
if (dStrong) dStrong.insertAdjacentElement('afterend', dBadge);
|
|
}
|
|
}
|
|
dPanel3.style.display = 'none';
|
|
} else {
|
|
alert(res.error_text || 'Save failed');
|
|
}
|
|
})
|
|
.catch(function(e){ alert('Error: ' + e.message); })
|
|
.finally(function(){ diagSaveBtn.disabled = false; diagSaveBtn.textContent = 'Save'; });
|
|
}
|
|
});
|
|
fhirFormView.addEventListener('input', function(e) {
|
|
var searchInput = e.target.closest('.fhir-snomed-search');
|
|
var diagSearch = e.target.closest('.fhir-diag-search');
|
|
if (!searchInput && !diagSearch) return;
|
|
if (diagSearch) {
|
|
var dPanel4 = diagSearch.closest('.fhir-diag-panel');
|
|
var dDd = dPanel4.querySelector('.fhir-diag-dd');
|
|
var dQ = diagSearch.value.trim();
|
|
if (dQ.length < 2) { dDd.style.display = 'none'; return; }
|
|
clearTimeout(_editTimer);
|
|
_editTimer = setTimeout(function() {
|
|
fetch('<?= base_url('Opd_prescription/complaints_search') ?>?q=' + encodeURIComponent(dQ), {
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
})
|
|
.then(function(r){ return r.json(); })
|
|
.then(function(res){
|
|
dDd.innerHTML = '';
|
|
var rows = (res.rows || res.results || []);
|
|
if (!rows.length) { dDd.style.display = 'none'; return; }
|
|
rows.slice(0, 10).forEach(function(row) {
|
|
var id = row.concept_id || row.snomed_concept_id || '';
|
|
var name = row.name || row.term || row.snomed_term || '';
|
|
var btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'list-group-item list-group-item-action py-1 px-2 fhir-diag-dd-item';
|
|
btn.dataset.id = id;
|
|
btn.dataset.term = name;
|
|
btn.innerHTML = '<small>' + hesc(name) + (id ? ' <span class="text-muted">' + hesc(id) + '</span>' : '') + '</small>';
|
|
dDd.appendChild(btn);
|
|
});
|
|
dDd.style.display = '';
|
|
})
|
|
.catch(function(){ dDd.style.display = 'none'; });
|
|
}, 280);
|
|
return;
|
|
}
|
|
if (!searchInput) return;
|
|
var panel = searchInput.closest('.fhir-edit-panel');
|
|
var dd = panel.querySelector('.fhir-snomed-dd');
|
|
var q = searchInput.value.trim();
|
|
if (q.length < 2) { dd.style.display = 'none'; return; }
|
|
clearTimeout(_editTimer);
|
|
_editTimer = setTimeout(function() {
|
|
fetch('<?= base_url('Opd_prescription/complaints_search') ?>?q=' + encodeURIComponent(q), {
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
|
})
|
|
.then(function(r){ return r.json(); })
|
|
.then(function(res){
|
|
dd.innerHTML = '';
|
|
var rows = (res.rows || res.results || []);
|
|
if (!rows.length) { dd.style.display = 'none'; return; }
|
|
rows.slice(0, 10).forEach(function(row) {
|
|
var id = row.concept_id || row.snomed_concept_id || '';
|
|
var name = row.name || row.term || row.snomed_term || '';
|
|
var btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'list-group-item list-group-item-action py-1 px-2 fhir-snomed-dd-item';
|
|
btn.dataset.id = id;
|
|
btn.dataset.term = name;
|
|
btn.innerHTML = '<small>' + hesc(name) + (id ? ' <span class="text-muted">' + hesc(id) + '</span>' : '') + '</small>';
|
|
dd.appendChild(btn);
|
|
});
|
|
dd.style.display = '';
|
|
})
|
|
.catch(function(){ dd.style.display = 'none'; });
|
|
}, 280);
|
|
});
|
|
fhirFormView.addEventListener('focusout', function(e) {
|
|
var inPanel = e.target.closest('.fhir-edit-panel');
|
|
var inDPanel = e.target.closest('.fhir-diag-panel');
|
|
if (!inPanel && !inDPanel) return;
|
|
setTimeout(function() {
|
|
if (inPanel) { var d = inPanel.querySelector('.fhir-snomed-dd'); if (d) d.style.display = 'none'; }
|
|
if (inDPanel) { var dd2 = inDPanel.querySelector('.fhir-diag-dd'); if (dd2) dd2.style.display = 'none'; }
|
|
}, 200);
|
|
});
|
|
|
|
// -- Arrow-key / Enter navigation for SNOMED dropdowns -------------
|
|
fhirFormView.addEventListener('keydown', function(e) {
|
|
var key = e.key;
|
|
if (key !== 'ArrowDown' && key !== 'ArrowUp' && key !== 'Enter' && key !== 'Escape') return;
|
|
|
|
var searchInput = e.target.closest('.fhir-snomed-search, .fhir-diag-search');
|
|
var ddItem = e.target.closest('.fhir-snomed-dd-item, .fhir-diag-dd-item');
|
|
if (!searchInput && !ddItem) return;
|
|
|
|
// Determine which dropdown container we're in
|
|
var panel = e.target.closest('.fhir-edit-panel, .fhir-diag-panel');
|
|
if (!panel) return;
|
|
var dd = panel.querySelector('.fhir-snomed-dd, .fhir-diag-dd');
|
|
if (!dd) return;
|
|
var input = panel.querySelector('.fhir-snomed-search, .fhir-diag-search');
|
|
var items = Array.prototype.slice.call(dd.querySelectorAll('.list-group-item'));
|
|
if (!items.length && key !== 'Escape') return;
|
|
|
|
if (key === 'Escape') {
|
|
dd.style.display = 'none';
|
|
if (input) input.focus();
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (key === 'Enter' && ddItem) {
|
|
e.preventDefault();
|
|
ddItem.click();
|
|
return;
|
|
}
|
|
|
|
if (key === 'Enter' && searchInput) {
|
|
// Select first visible item on Enter if dropdown open
|
|
if (dd.style.display !== 'none' && items.length) {
|
|
e.preventDefault();
|
|
items[0].click();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
if (searchInput) {
|
|
// From input: focus first item
|
|
if (dd.style.display !== 'none' && items.length) { items[0].focus(); }
|
|
} else if (ddItem) {
|
|
var idx = items.indexOf(ddItem);
|
|
if (idx < items.length - 1) { items[idx + 1].focus(); } else { items[0].focus(); }
|
|
}
|
|
}
|
|
|
|
if (key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
if (ddItem) {
|
|
var idx2 = items.indexOf(ddItem);
|
|
if (idx2 > 0) { items[idx2 - 1].focus(); } else { if (input) input.focus(); }
|
|
}
|
|
}
|
|
});
|
|
|
|
// -- Render FHIR bundle as human-readable form ----------------------
|
|
function hesc(s) {
|
|
return String(s || '')
|
|
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
function fmtDate(s) {
|
|
if (!s) return '';
|
|
var d = s.toString().substring(0, 10).split('-');
|
|
return d.length === 3 ? d[2]+'/'+d[1]+'/'+d[0] : s.toString().substring(0,16);
|
|
}
|
|
function renderFhirBundleForm(bundle, liveComplaints, liveDiagnoses) {
|
|
liveComplaints = liveComplaints || [];
|
|
liveDiagnoses = liveDiagnoses || [];
|
|
var resources = (bundle.entry || []).map(function(e) { return e.resource; }).filter(Boolean);
|
|
function byType(t) { return resources.filter(function(r){ return r.resourceType === t; }); }
|
|
var resourcesByUrl = {};
|
|
(bundle.entry || []).forEach(function(entry) {
|
|
if (entry.fullUrl && entry.resource) resourcesByUrl[entry.fullUrl] = entry.resource;
|
|
});
|
|
var composition = byType('Composition')[0];
|
|
var patient = byType('Patient')[0];
|
|
var encounter = byType('Encounter')[0];
|
|
var practitioner = byType('Practitioner')[0];
|
|
var organization = byType('Organization')[0];
|
|
var invoice = byType('Invoice')[0];
|
|
var allConditions = byType('Condition');
|
|
var observations = byType('Observation');
|
|
var medications = byType('MedicationRequest');
|
|
var procedures = byType('Procedure');
|
|
var serviceReqs = byType('ServiceRequest');
|
|
var allergies = byType('AllergyIntolerance');
|
|
var carePlans = byType('CarePlan');
|
|
var documentRefs = byType('DocumentReference');
|
|
var immunizations = byType('Immunization');
|
|
|
|
(window._abdmFhirObjectUrls || []).forEach(function(url) { URL.revokeObjectURL(url); });
|
|
window._abdmFhirObjectUrls = [];
|
|
function attachmentObjectUrl(attachment) {
|
|
if (!attachment || !attachment.data) return '';
|
|
try {
|
|
var bytes = atob(attachment.data);
|
|
var buffer = new Uint8Array(bytes.length);
|
|
for (var i = 0; i < bytes.length; i++) buffer[i] = bytes.charCodeAt(i);
|
|
var url = URL.createObjectURL(new Blob([buffer], { type: attachment.contentType || 'application/octet-stream' }));
|
|
window._abdmFhirObjectUrls.push(url);
|
|
return url;
|
|
} catch (error) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
var coursePlans = [];
|
|
var advicePlans = [];
|
|
carePlans.forEach(function(cp) {
|
|
var title = String(cp.title || '').trim().toLowerCase();
|
|
if (title === 'course in hospital') {
|
|
coursePlans.push(cp);
|
|
} else {
|
|
advicePlans.push(cp);
|
|
}
|
|
});
|
|
|
|
var complaints = allConditions.filter(function(c) {
|
|
return (c.category || []).some(function(cat) { return (cat.text || '').toLowerCase().includes('complaint'); });
|
|
});
|
|
var diagnoses = allConditions.filter(function(c) {
|
|
return !(c.category || []).some(function(cat) { return (cat.text || '').toLowerCase().includes('complaint'); });
|
|
});
|
|
|
|
var html = '';
|
|
|
|
// -- Patient header
|
|
if (patient) {
|
|
var pname = ((patient.name || [{}])[0].text || '').trim();
|
|
var gender = (patient.gender || '').toUpperCase().charAt(0);
|
|
var dob = fmtDate(patient.birthDate);
|
|
var abha = (patient.identifier || []).find(function(i){ return (i.system||'').includes('abha'); });
|
|
var docName = practitioner ? ((practitioner.name || [{}])[0].text || '') : '';
|
|
var orgName = organization ? (organization.name || '') : '';
|
|
var encStart = encounter ? fmtDate((encounter.period || {}).start || '') : '';
|
|
html += '<div class="card border-primary mb-3">';
|
|
html += '<div class="card-header bg-primary text-white py-2 d-flex flex-wrap gap-2 align-items-center">';
|
|
html += '<strong class="fs-6">' + hesc(pname || 'Unknown Patient') + '</strong>';
|
|
if (gender) html += '<span class="badge bg-white text-primary">' + hesc(gender) + '</span>';
|
|
if (dob) html += '<small class="opacity-75">DOB: ' + hesc(dob) + '</small>';
|
|
if (abha) html += '<span class="badge bg-warning text-dark ms-auto">ABHA: ' + hesc(abha.value) + '</span>';
|
|
html += '</div>';
|
|
html += '<div class="card-body py-2 d-flex flex-wrap gap-3 small">';
|
|
if (docName) html += '<span><i class="bi bi-person-badge me-1"></i><strong>' + hesc(docName) + '</strong></span>';
|
|
if (encStart) html += '<span><i class="bi bi-calendar2 me-1"></i>' + hesc(encStart) + '</span>';
|
|
if (orgName) html += '<span><i class="bi bi-hospital me-1"></i>' + hesc(orgName) + '</span>';
|
|
html += '</div></div>';
|
|
}
|
|
|
|
// -- Invoice Record document metadata
|
|
if (composition && invoice) {
|
|
var compositionCoding = ((((composition.type || {}).coding) || [])[0]) || {};
|
|
var compositionIdentifier = (composition.identifier || {}).value || '';
|
|
var bundleIdentifier = (bundle.identifier || {}).value || '';
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Invoice Record</small></div>';
|
|
html += '<div class="card-body py-2 small"><div class="row g-2">';
|
|
html += '<div class="col-md-4"><span class="text-muted">Document</span><div class="fw-semibold">' + hesc(composition.title || 'Invoice Record') + '</div></div>';
|
|
html += '<div class="col-md-3"><span class="text-muted">Status</span><div class="text-capitalize">' + hesc(composition.status || '-') + '</div></div>';
|
|
html += '<div class="col-md-5"><span class="text-muted">Created</span><div>' + hesc(composition.date || bundle.timestamp || '-') + '</div></div>';
|
|
html += '<div class="col-md-4"><span class="text-muted">Composition Type</span><div>' + hesc(compositionCoding.display || (composition.type || {}).text || '-') + '</div></div>';
|
|
html += '<div class="col-md-3"><span class="text-muted">LOINC</span><div><code>' + hesc(compositionCoding.code || '-') + '</code></div></div>';
|
|
html += '<div class="col-md-5"><span class="text-muted">Bundle / Care Context Identifier</span><div><code>' + hesc(bundleIdentifier || compositionIdentifier || '-') + '</code></div></div>';
|
|
html += '</div></div></div>';
|
|
}
|
|
|
|
// -- Invoice details and line items
|
|
if (invoice) {
|
|
var invoiceIdentifier = ((invoice.identifier || [])[0] || {}).value || '';
|
|
var invoiceCoding = ((((invoice.type || {}).coding) || [])[0]) || {};
|
|
var totalRate = (((invoice.totalPriceComponent || [])[0] || {}).amount || {});
|
|
var totalNet = invoice.totalNet || {};
|
|
var totalGross = invoice.totalGross || {};
|
|
function money(amount) {
|
|
if (amount.value === undefined || amount.value === null || amount.value === '') return '-';
|
|
var numeric = Number(amount.value);
|
|
return (amount.currency || 'INR') + ' ' + (isNaN(numeric) ? hesc(amount.value) : numeric.toFixed(2));
|
|
}
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light d-flex justify-content-between align-items-center"><small class="fw-bold text-uppercase text-secondary">Invoice Details</small>';
|
|
html += '<span class="badge bg-success text-capitalize">' + hesc(invoice.status || 'issued') + '</span></div><div class="card-body py-2">';
|
|
html += '<div class="row g-2 small mb-3">';
|
|
html += '<div class="col-md-4"><span class="text-muted">Invoice Number</span><div class="fw-bold">' + hesc(invoiceIdentifier || invoice.id || '-') + '</div></div>';
|
|
html += '<div class="col-md-3"><span class="text-muted">Invoice Type</span><div>' + hesc(invoiceCoding.display || (invoice.type || {}).text || '-') + (invoiceCoding.code ? ' <code>' + hesc(invoiceCoding.code) + '</code>' : '') + '</div></div>';
|
|
html += '<div class="col-md-2"><span class="text-muted">Invoice Date</span><div>' + hesc(fmtDate(invoice.date) || '-') + '</div></div>';
|
|
html += '<div class="col-md-3"><span class="text-muted">Total Price Component</span><div class="fw-semibold">' + money(totalRate) + '</div></div>';
|
|
html += '<div class="col-md-3"><span class="text-muted">Net Total</span><div class="fw-semibold">' + money(totalNet) + '</div></div>';
|
|
html += '<div class="col-md-3"><span class="text-muted">Gross Total</span><div class="fw-semibold">' + money(totalGross) + '</div></div>';
|
|
html += '</div>';
|
|
var invoiceItems = invoice.lineItem || [];
|
|
if (invoiceItems.length) {
|
|
html += '<div class="table-responsive"><table class="table table-sm table-bordered align-middle mb-0"><thead class="table-light"><tr><th style="width:55px">#</th><th>Item</th><th>Component</th><th class="text-end">Amount</th></tr></thead><tbody>';
|
|
invoiceItems.forEach(function(item, index) {
|
|
var component = ((item.priceComponent || [])[0]) || {};
|
|
var componentCoding = ((((component.code || {}).coding) || [])[0]) || {};
|
|
var chargeReference = (item.chargeItemReference || {}).reference || '';
|
|
var chargeItem = resourcesByUrl[chargeReference] || {};
|
|
var itemName = (chargeItem.productCodeableConcept || {}).text
|
|
|| (chargeItem.code || {}).text
|
|
|| (item.chargeItemReference || {}).display
|
|
|| (item.chargeItemCodeableConcept || {}).text
|
|
|| 'Item';
|
|
var itemQuantity = chargeItem.quantity || {};
|
|
html += '<tr><td>' + hesc(item.sequence || index + 1) + '</td>';
|
|
html += '<td><div class="fw-semibold">' + hesc(itemName) + '</div>';
|
|
if (itemQuantity.value !== undefined) html += '<small class="text-muted">Qty: ' + hesc(itemQuantity.value) + ' ' + hesc(itemQuantity.unit || '') + '</small>';
|
|
html += '</td>';
|
|
html += '<td>' + hesc(componentCoding.display || component.type || '-') + (componentCoding.code ? ' <code>' + hesc(componentCoding.code) + '</code>' : '') + '</td>';
|
|
html += '<td class="text-end">' + money(component.amount || {}) + '</td></tr>';
|
|
});
|
|
html += '</tbody></table></div>';
|
|
} else {
|
|
html += '<div class="text-muted small">No invoice line items found.</div>';
|
|
}
|
|
html += '</div></div>';
|
|
}
|
|
|
|
// -- Health Document Record metadata
|
|
if (composition && !invoice && (documentRefs.length || (composition.title && (composition.title.toLowerCase().includes('document') || composition.title.toLowerCase().includes('certificate') || composition.title.toLowerCase().includes('form'))))) {
|
|
var compCoding = ((((composition.type || {}).coding) || [])[0]) || {};
|
|
var compIdentifier = (composition.identifier || {}).value || '';
|
|
var bundleIdentifier = (bundle.identifier || {}).value || '';
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light d-flex justify-content-between align-items-center">';
|
|
html += '<small class="fw-bold text-uppercase text-secondary">' + hesc(composition.title || 'Health Document Record') + '</small>';
|
|
html += '<span class="badge bg-success text-capitalize">' + hesc(composition.status || 'final') + '</span></div>';
|
|
html += '<div class="card-body py-2 small"><div class="row g-2">';
|
|
html += '<div class="col-md-4"><span class="text-muted">Document Title</span><div class="fw-semibold">' + hesc(composition.title || 'Health Document') + '</div></div>';
|
|
html += '<div class="col-md-3"><span class="text-muted">Document Date</span><div>' + hesc(fmtDate(composition.date || bundle.timestamp) || '-') + '</div></div>';
|
|
html += '<div class="col-md-5"><span class="text-muted">Record / Bundle ID</span><div><code>' + hesc(bundleIdentifier || compIdentifier || '-') + '</code></div></div>';
|
|
html += '<div class="col-md-4"><span class="text-muted">Type</span><div>' + hesc(compCoding.display || (composition.type || {}).text || 'Record artifact') + '</div></div>';
|
|
if (compCoding.code) {
|
|
html += '<div class="col-md-3"><span class="text-muted">Code</span><div><code>' + hesc(compCoding.code) + '</code></div></div>';
|
|
}
|
|
html += '</div></div></div>';
|
|
}
|
|
|
|
// -- Embedded Document / PDF / Image attachments
|
|
documentRefs.forEach(function(documentReference) {
|
|
var attachment = (((documentReference.content || [])[0] || {}).attachment) || {};
|
|
var cType = (attachment.contentType || '').toLowerCase();
|
|
var docName = attachment.title || documentReference.description || (invoice ? 'Invoice PDF' : 'Health Document');
|
|
var isPdf = cType.indexOf('pdf') !== -1;
|
|
var isImage = cType.indexOf('image/') === 0;
|
|
var docUrl = attachmentObjectUrl(attachment);
|
|
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light d-flex justify-content-between align-items-center">';
|
|
html += '<small class="fw-bold text-uppercase text-secondary">' + hesc(docName) + '</small>';
|
|
html += '<span class="badge bg-info text-dark">' + hesc(cType || 'document') + '</span></div>';
|
|
html += '<div class="card-body py-2">';
|
|
html += '<div class="d-flex flex-wrap align-items-center gap-2 mb-2">';
|
|
html += '<div class="flex-grow-1"><div class="fw-semibold">' + hesc(docName) + '</div>';
|
|
if (attachment.size) {
|
|
var sizeKb = (attachment.size / 1024).toFixed(1) + ' KB';
|
|
if (attachment.size > 1048576) sizeKb = (attachment.size / 1048576).toFixed(2) + ' MB';
|
|
html += '<small class="text-muted">Size: ' + sizeKb + ' • ' + hesc(cType) + '</small>';
|
|
} else {
|
|
html += '<small class="text-muted">' + (invoice ? 'Attached to this FHIR InvoiceRecord' : 'Attached health document payload') + '</small>';
|
|
}
|
|
html += '</div>';
|
|
if (docUrl) {
|
|
if (isPdf) {
|
|
html += '<a class="btn btn-sm btn-outline-primary" href="' + hesc(docUrl) + '" target="_blank" rel="noopener"><i class="bi bi-file-earmark-pdf me-1"></i>Open PDF</a>';
|
|
html += '<a class="btn btn-sm btn-primary" href="' + hesc(docUrl) + '" download="' + hesc(docName.replace(/\s+/g, '_') + '.pdf') + '"><i class="bi bi-download me-1"></i>Download PDF</a>';
|
|
} else if (isImage) {
|
|
html += '<a class="btn btn-sm btn-outline-primary" href="' + hesc(docUrl) + '" target="_blank" rel="noopener"><i class="bi bi-image me-1"></i>View Full Image</a>';
|
|
html += '<a class="btn btn-sm btn-primary" href="' + hesc(docUrl) + '" download="' + hesc(docName.replace(/\s+/g, '_')) + '"><i class="bi bi-download me-1"></i>Download</a>';
|
|
} else {
|
|
html += '<a class="btn btn-sm btn-outline-primary" href="' + hesc(docUrl) + '" target="_blank" rel="noopener">Open Document</a>';
|
|
}
|
|
} else {
|
|
html += '<span class="badge bg-danger">Invalid document attachment</span>';
|
|
}
|
|
html += '</div>';
|
|
|
|
if (isImage && docUrl) {
|
|
html += '<div class="text-center p-2 bg-light rounded border"><img src="' + hesc(docUrl) + '" style="max-width:100%;max-height:400px;object-fit:contain;" class="rounded shadow-sm" alt="Document Preview" /></div>';
|
|
} else if (isPdf && docUrl) {
|
|
html += '<div class="ratio ratio-16x9 border rounded overflow-hidden mt-2" style="min-height:350px;max-height:550px;"><iframe src="' + hesc(docUrl) + '" class="w-100 h-100" style="border:none;"></iframe></div>';
|
|
}
|
|
html += '</div></div>';
|
|
});
|
|
|
|
// -- Immunization Record
|
|
if (immunizations.length) {
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Vaccination Details</small></div>';
|
|
html += '<div class="table-responsive"><table class="table table-sm table-bordered align-middle mb-0"><thead class="table-light"><tr>';
|
|
html += '<th>Vaccine</th><th>Date</th><th>Doctor / Facility</th><th>Dose / Series</th><th>Lot</th><th>Target Disease</th></tr></thead><tbody>';
|
|
immunizations.forEach(function(immunization) {
|
|
var vaccineCoding = ((((immunization.vaccineCode || {}).coding) || [])[0]) || {};
|
|
var performerReference = ((((immunization.performer || [])[0] || {}).actor) || {});
|
|
var performerResource = resourcesByUrl[performerReference.reference || ''] || {};
|
|
var performerName = performerReference.display || (((performerResource.name || [])[0] || {}).text) || '-';
|
|
var locationReference = immunization.location || {};
|
|
var locationResource = resourcesByUrl[locationReference.reference || ''] || {};
|
|
var facilityName = locationReference.display || locationResource.name || '-';
|
|
var protocol = ((immunization.protocolApplied || [])[0]) || {};
|
|
var targetDisease = ((protocol.targetDisease || [])[0]) || {};
|
|
var targetCoding = (((targetDisease.coding || [])[0]) || {});
|
|
var dose = protocol.doseNumberPositiveInt || protocol.doseNumberString || '-';
|
|
html += '<tr><td><div class="fw-semibold">' + hesc((immunization.vaccineCode || {}).text || vaccineCoding.display || '-') + '</div>';
|
|
if (vaccineCoding.code) html += '<small class="text-muted">' + hesc(vaccineCoding.system || 'Code') + ': <code>' + hesc(vaccineCoding.code) + '</code></small>';
|
|
html += '</td><td>' + hesc(fmtDate(immunization.occurrenceDateTime) || '-') + '</td>';
|
|
html += '<td><div>' + hesc(performerName) + '</div><small class="text-muted">' + hesc(facilityName) + '</small></td>';
|
|
html += '<td><div>Dose ' + hesc(dose) + '</div><small class="text-muted">' + hesc(protocol.series || '-') + '</small></td>';
|
|
html += '<td>' + hesc(immunization.lotNumber || '-') + '</td>';
|
|
html += '<td>' + hesc(targetDisease.text || targetCoding.display || '-') + (targetCoding.code ? '<br><code>' + hesc(targetCoding.code) + '</code>' : '') + '</td></tr>';
|
|
});
|
|
html += '</tbody></table></div></div>';
|
|
}
|
|
|
|
// -- Complaints
|
|
if (complaints.length) {
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Chief Complaints</small></div><div class="card-body py-2">';
|
|
html += '<ul class="mb-0 ps-3">';
|
|
complaints.forEach(function(c, idx) {
|
|
var codeText = (c.code || {}).text || '';
|
|
var coding = (((c.code || {}).coding) || [])[0] || {};
|
|
var snomed = coding.code || '';
|
|
var display = coding.display || codeText;
|
|
var sev = ((c.severity || {}).coding || [{}])[0];
|
|
var sevText = (sev && sev.display) ? sev.display : ((c.severity || {}).text || '');
|
|
var note = ((c.note || [{}])[0] || {}).text || '';
|
|
// Resolve doctor's original text from live complaint_snomed_json
|
|
var doctorText = codeText;
|
|
var liveItem = null;
|
|
if (snomed && liveComplaints.length) {
|
|
for (var li = 0; li < liveComplaints.length; li++) {
|
|
if ((liveComplaints[li].concept_id || '') === snomed) { liveItem = liveComplaints[li]; break; }
|
|
}
|
|
}
|
|
if (!liveItem && liveComplaints.length > idx) { liveItem = liveComplaints[idx]; }
|
|
if (liveItem) { doctorText = liveItem.original_term || liveItem.term || codeText; }
|
|
html += '<li class="mb-2">';
|
|
html += '<div class="d-flex align-items-start gap-2">';
|
|
html += '<div class="flex-grow-1">';
|
|
html += '<strong>' + hesc(doctorText) + '</strong>';
|
|
if (snomed) html += ' <span class="badge bg-info text-dark fhir-snomed-badge" style="font-size:.7rem;">SNOMED ' + hesc(snomed) + '</span>';
|
|
if (sevText) html += ' <span class="badge bg-secondary" style="font-size:.7rem;">' + hesc(sevText) + '</span>';
|
|
if (note) html += ' <small class="text-muted">(' + hesc(note) + ')</small>';
|
|
if (display && display !== doctorText) html += ' <small class="text-secondary d-block">SNOMED: ' + hesc(display) + '</small>';
|
|
html += '</div>';
|
|
html += '<button type="button" class="btn btn-outline-secondary fhir-complaint-edit flex-shrink-0" data-idx="' + idx + '" style="font-size:.7rem;padding:1px 6px;">Edit</button>';
|
|
html += '</div>';
|
|
// inline edit panel
|
|
html += '<div class="fhir-edit-panel border rounded p-2 mt-1 bg-light" data-idx="' + idx + '" data-complaint-text="' + hesc(doctorText) + '" style="display:none;">';
|
|
html += '<div class="mb-1 small text-muted">Doctor wrote: <em>' + hesc(doctorText) + '</em> — assign SNOMED code:</div>';
|
|
html += '<div class="position-relative mb-1">';
|
|
html += '<input type="text" class="form-control form-control-sm fhir-snomed-search" placeholder="Type to search SNOMED..." value="' + hesc(doctorText) + '">';
|
|
html += '<div class="list-group shadow position-absolute w-100 fhir-snomed-dd" style="z-index:2000;display:none;max-height:180px;overflow-y:auto;"></div>';
|
|
html += '</div>';
|
|
html += '<div class="fhir-snomed-selected small text-success mb-1" style="display:none;"></div>';
|
|
html += '<input type="hidden" class="fhir-snomed-id" value="' + hesc(snomed) + '">';
|
|
html += '<input type="hidden" class="fhir-snomed-term" value="' + hesc(doctorText) + '">';
|
|
html += '<div class="d-flex gap-2"><button type="button" class="btn btn-sm btn-primary fhir-complaint-save">Save</button>';
|
|
html += '<button type="button" class="btn btn-sm btn-outline-secondary fhir-complaint-cancel">Cancel</button></div>';
|
|
html += '</div>';
|
|
html += '</li>';
|
|
});
|
|
html += '</ul></div></div>';
|
|
}
|
|
|
|
// -- Diagnoses
|
|
if (diagnoses.length) {
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Diagnosis</small></div><div class="card-body py-2">';
|
|
html += '<ul class="mb-0 ps-3">';
|
|
diagnoses.forEach(function(c, didx) {
|
|
var text = (c.code || {}).text || '';
|
|
var coding = (((c.code || {}).coding) || [])[0] || {};
|
|
var snomed = coding.code || '';
|
|
var display = coding.display || text;
|
|
var verif = (((c.verificationStatus || {}).coding) || [])[0] || {};
|
|
var isProvisional = (verif.code || 'provisional') !== 'confirmed';
|
|
var diagType = isProvisional ? 'provisional' : 'confirmed';
|
|
// Resolve doctor's original text from live diagnosis data
|
|
var doctorDiagText = text;
|
|
var liveDiag = null;
|
|
if (liveDiagnoses.length) {
|
|
for (var ldi = 0; ldi < liveDiagnoses.length; ldi++) {
|
|
if ((liveDiagnoses[ldi].type || '') === diagType) { liveDiag = liveDiagnoses[ldi]; break; }
|
|
}
|
|
if (!liveDiag && liveDiagnoses.length > didx) { liveDiag = liveDiagnoses[didx]; }
|
|
}
|
|
if (liveDiag) { doctorDiagText = liveDiag.term || text; }
|
|
html += '<li class="mb-2">';
|
|
html += '<div class="d-flex align-items-start gap-2">';
|
|
html += '<div class="flex-grow-1">';
|
|
html += '<strong>' + hesc(doctorDiagText) + '</strong>';
|
|
if (snomed) html += ' <span class="badge bg-success fhir-diag-snomed-badge" style="font-size:.7rem;">SNOMED ' + hesc(snomed) + '</span>';
|
|
html += ' <span class="badge ' + (isProvisional ? 'bg-warning text-dark' : 'bg-success-subtle text-success-emphasis border border-success') + '" style="font-size:.7rem;">' + (isProvisional ? 'Provisional' : 'Confirmed') + '</span>';
|
|
if (display && display !== doctorDiagText) html += ' <small class="text-secondary d-block">SNOMED: ' + hesc(display) + '</small>';
|
|
html += '</div>';
|
|
html += '<button type="button" class="btn btn-outline-secondary fhir-diag-edit flex-shrink-0" data-didx="' + didx + '" style="font-size:.7rem;padding:1px 6px;">Edit</button>';
|
|
html += '</div>';
|
|
html += '<div class="fhir-diag-panel border rounded p-2 mt-1 bg-light" data-didx="' + didx + '" data-diag-text="' + hesc(doctorDiagText) + '" data-diag-type="' + hesc(diagType) + '" style="display:none;">';
|
|
html += '<div class="mb-1 small text-muted">Doctor wrote: <em>' + hesc(doctorDiagText) + '</em> — assign SNOMED code:</div>';
|
|
html += '<div class="position-relative mb-1">';
|
|
html += '<input type="text" class="form-control form-control-sm fhir-diag-search" placeholder="Type to search SNOMED\u2026" value="' + hesc(doctorDiagText) + '">';
|
|
html += '<div class="list-group shadow position-absolute w-100 fhir-diag-dd" style="z-index:2000;display:none;max-height:180px;overflow-y:auto;"></div>';
|
|
html += '</div>';
|
|
html += '<div class="fhir-diag-selected small text-success mb-1" style="display:none;"></div>';
|
|
html += '<input type="hidden" class="fhir-diag-id" value="' + hesc(snomed) + '">';
|
|
html += '<input type="hidden" class="fhir-diag-term" value="' + hesc(display || doctorDiagText) + '">';
|
|
html += '<div class="d-flex gap-2"><button type="button" class="btn btn-sm btn-primary fhir-diag-save">Save</button>';
|
|
html += '<button type="button" class="btn btn-sm btn-outline-secondary fhir-diag-cancel">Cancel</button></div>';
|
|
html += '</div>';
|
|
html += '</li>';
|
|
});
|
|
html += '</ul></div></div>';
|
|
}
|
|
|
|
// -- Observation Groups (Vitals vs Lab Results)
|
|
if (observations.length) {
|
|
var loincLabel = { '8867-4':'HR','59408-5':'SpO','8480-6':'BP Sys','8462-4':'BP Dia','8310-5':'Temp','9279-1':'RR','8302-2':'Height','29463-7':'Weight' };
|
|
var vitalLoincCodes = { '8867-4':1, '59408-5':1, '8480-6':1, '8462-4':1, '8310-5':1, '9279-1':1, '8302-2':1, '29463-7':1 };
|
|
|
|
function meaningfulText(v) {
|
|
var s = String(v || '').trim().toLowerCase();
|
|
if (!s) return false;
|
|
return ['-', '--', 'na', 'n/a', 'nil', 'none', 'not done'].indexOf(s) === -1;
|
|
}
|
|
|
|
function getObservationCategoryText(o) {
|
|
var categories = o.category || [];
|
|
for (var i = 0; i < categories.length; i++) {
|
|
var t = (categories[i] || {}).text || '';
|
|
if (t && String(t).trim() !== '') return String(t).trim();
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function isVitalObservation(o) {
|
|
var categories = o.category || [];
|
|
for (var ci = 0; ci < categories.length; ci++) {
|
|
var cat = categories[ci] || {};
|
|
var catText = (cat.text || '').toLowerCase();
|
|
if (catText.indexOf('vital') !== -1) {
|
|
return true;
|
|
}
|
|
var codings = cat.coding || [];
|
|
for (var cj = 0; cj < codings.length; cj++) {
|
|
var c = codings[cj] || {};
|
|
var system = (c.system || '').toLowerCase();
|
|
var code = (c.code || '').toLowerCase();
|
|
if ((system.indexOf('observation-category') !== -1 && code === 'vital-signs') || code === 'vital-signs') {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
var coding = (((o.code || {}).coding) || [])[0] || {};
|
|
var code = coding.code || '';
|
|
return !!vitalLoincCodes[code];
|
|
}
|
|
|
|
function renderObservationCard(sectionTitle, items) {
|
|
if (!items.length) {
|
|
return;
|
|
}
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-secondary">' + hesc(sectionTitle) + '</small></div><div class="card-body py-2"><div class="d-flex flex-wrap gap-2">';
|
|
items.forEach(function(o) {
|
|
var loinc = (((o.code || {}).coding) || [])[0] || {};
|
|
var label = loincLabel[loinc.code] || (o.code || {}).text || loinc.display || '';
|
|
var val = (o.valueQuantity || {}).value;
|
|
var unit = (o.valueQuantity || {}).unit || '';
|
|
if (val === undefined || val === null) {
|
|
val = o.valueString || o.valueCodeableConcept && o.valueCodeableConcept.text || '';
|
|
}
|
|
if (val === undefined || val === null || !meaningfulText(val)) {
|
|
return;
|
|
}
|
|
if (typeof val === 'number') {
|
|
val = (loinc.code === '8310-5' || loinc.code === '8302-2' || loinc.code === '39156-5') ? Number(val.toFixed(1)) : Number(val.toFixed(2));
|
|
} else if (!isNaN(parseFloat(val)) && isFinite(val) && String(val).indexOf('.') !== -1) {
|
|
var parsed = parseFloat(val);
|
|
val = (loinc.code === '8310-5' || loinc.code === '8302-2' || loinc.code === '39156-5') ? Number(parsed.toFixed(1)) : Number(parsed.toFixed(2));
|
|
}
|
|
html += '<div class="border rounded px-2 py-1 text-center" style="min-width:80px;">';
|
|
html += '<div class="small text-muted" style="font-size:.7rem;">' + hesc(label) + '</div>';
|
|
html += '<div class="fw-bold">' + hesc(val) + '</div>';
|
|
html += '<div class="text-muted" style="font-size:.7rem;">' + hesc(unit);
|
|
if (loinc.code) html += ' <span class="text-secondary" style="font-size:.65rem;">' + hesc(loinc.code) + '</span>';
|
|
html += '</div></div>';
|
|
});
|
|
html += '</div></div></div>';
|
|
}
|
|
|
|
var admissionObservations = observations.filter(function(o) {
|
|
var cat = getObservationCategoryText(o).toLowerCase();
|
|
return cat.indexOf('admission') !== -1;
|
|
});
|
|
var dischargeObservations = observations.filter(function(o) {
|
|
var cat = getObservationCategoryText(o).toLowerCase();
|
|
return cat.indexOf('discharge') !== -1;
|
|
});
|
|
var remainingObservations = observations.filter(function(o) {
|
|
var cat = getObservationCategoryText(o).toLowerCase();
|
|
return cat.indexOf('admission') === -1 && cat.indexOf('discharge') === -1;
|
|
});
|
|
var vitalObservations = remainingObservations.filter(isVitalObservation);
|
|
var labObservations = remainingObservations.filter(function(o) { return !isVitalObservation(o); });
|
|
|
|
renderObservationCard('Condition on Admission Time', admissionObservations);
|
|
renderObservationCard('Condition on Discharge Time', dischargeObservations);
|
|
renderObservationCard('Vitals', vitalObservations);
|
|
renderObservationCard('Lab Results', labObservations);
|
|
}
|
|
|
|
// -- Medications
|
|
if (medications.length) {
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Medications</small></div><div class="card-body py-2">';
|
|
html += '<ol class="mb-0 ps-3">';
|
|
medications.forEach(function(m) {
|
|
var drug = (m.medicationCodeableConcept || {}).text || '';
|
|
var di = (m.dosageInstruction || [{}])[0] || {};
|
|
var dosage = di.text || '';
|
|
var method = (di.method || {}).text || '';
|
|
html += '<li class="mb-1"><strong>' + hesc(drug) + '</strong>';
|
|
if (dosage) html += ' <span class="text-muted small">- ' + hesc(dosage) + (method ? ' (' + hesc(method) + ')' : '') + '</span>';
|
|
html += '</li>';
|
|
});
|
|
html += '</ol></div></div>';
|
|
}
|
|
|
|
// -- Procedures / Surgeries
|
|
if (procedures.length) {
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Procedures</small></div><div class="card-body py-2">';
|
|
html += '<ul class="mb-0 ps-3">';
|
|
procedures.forEach(function(p) {
|
|
var text = (p.code || {}).text || ((((p.code || {}).coding) || [])[0] || {}).display || '';
|
|
var at = fmtDate(p.performedDateTime || '');
|
|
html += '<li class="mb-1"><strong>' + hesc(text || 'Procedure') + '</strong>';
|
|
if (at) html += ' <span class="text-muted small">(' + hesc(at) + ')</span>';
|
|
html += '</li>';
|
|
});
|
|
html += '</ul></div></div>';
|
|
}
|
|
|
|
// -- Investigations
|
|
if (serviceReqs.length) {
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Investigations</small></div><div class="card-body py-2">';
|
|
html += '<ul class="mb-0 ps-3">';
|
|
serviceReqs.forEach(function(s) {
|
|
var text = (s.code || {}).text || '';
|
|
var loinc = (((s.code || {}).coding) || [])[0] || {};
|
|
var normalized = String(text || '').trim().toLowerCase();
|
|
if (!normalized || normalized === '-' || normalized === '--' || normalized === 'na' || normalized === 'n/a' || normalized === 'nil' || normalized === 'none' || normalized === 'not done') {
|
|
return;
|
|
}
|
|
html += '<li class="mb-1"><strong>' + hesc(text) + '</strong>';
|
|
if (loinc.code) html += ' <span class="badge bg-info text-dark" style="font-size:.7rem;">LOINC ' + hesc(loinc.code) + '</span>';
|
|
html += '</li>';
|
|
});
|
|
html += '</ul></div></div>';
|
|
}
|
|
|
|
// -- Allergy & Risk Profile
|
|
if (allergies.length) {
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Allergies / Risk Profile</small></div><div class="card-body py-2">';
|
|
html += '<ul class="mb-0 ps-3">';
|
|
allergies.forEach(function(a) {
|
|
var text = (a.code || {}).text || '';
|
|
var note = (((a.note || [])[0]) || {}).text || '';
|
|
var criticality = (a.criticality || '').toUpperCase();
|
|
html += '<li class="mb-1"><strong>' + hesc(text || 'Allergy') + '</strong>';
|
|
if (criticality) html += ' <span class="badge bg-warning text-dark" style="font-size:.7rem;">' + hesc(criticality) + '</span>';
|
|
if (note) html += ' <small class="text-muted d-block">' + hesc(note) + '</small>';
|
|
html += '</li>';
|
|
});
|
|
html += '</ul></div></div>';
|
|
}
|
|
|
|
// -- Course In Hospital
|
|
if (coursePlans.length) {
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Course In Hospital</small></div><div class="card-body py-2">';
|
|
html += '<ul class="mb-0 ps-3">';
|
|
coursePlans.forEach(function(cp) {
|
|
var title = cp.title || 'Care Plan';
|
|
var desc = cp.description || '';
|
|
html += '<li class="mb-2"><strong>' + hesc(title) + '</strong>';
|
|
if (desc) html += '<div class="text-muted small">' + hesc(desc) + '</div>';
|
|
html += '</li>';
|
|
});
|
|
html += '</ul></div></div>';
|
|
}
|
|
|
|
// -- Discharge Advice / Follow-up
|
|
if (advicePlans.length) {
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Discharge Advice & Follow-up</small></div><div class="card-body py-2">';
|
|
html += '<ul class="mb-0 ps-3">';
|
|
advicePlans.forEach(function(cp) {
|
|
var title = cp.title || 'Care Plan';
|
|
var desc = cp.description || '';
|
|
html += '<li class="mb-2"><strong>' + hesc(title) + '</strong>';
|
|
if (desc) html += '<div class="text-muted small">' + hesc(desc) + '</div>';
|
|
html += '</li>';
|
|
});
|
|
html += '</ul></div></div>';
|
|
}
|
|
|
|
// -- Attached Documents & Reports (PDFs, Images, Scanned Documents, Invoices)
|
|
if (documentRefs.length) {
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light d-flex justify-content-between align-items-center"><small class="fw-bold text-uppercase text-secondary">Attached Documents & Reports</small><span class="badge bg-secondary">' + documentRefs.length + '</span></div><div class="card-body py-2">';
|
|
html += '<div class="row g-2">';
|
|
documentRefs.forEach(function(docRef) {
|
|
var contentList = docRef.content || [];
|
|
contentList.forEach(function(c) {
|
|
var attachment = c.attachment || {};
|
|
var title = attachment.title || docRef.description || ((docRef.type || {}).text) || 'Attached Document';
|
|
var contentType = attachment.contentType || 'application/octet-stream';
|
|
|
|
var attachmentData = attachment.data;
|
|
if (!attachmentData && attachment.url) {
|
|
var binaryRes = resourcesByUrl[attachment.url] || resourcesByUrl['urn:uuid:' + attachment.url];
|
|
if (!binaryRes) {
|
|
var binCleanId = String(attachment.url).replace(/^urn:uuid:/, '');
|
|
binaryRes = (byType('Binary') || []).find(function(b) { return b.id === binCleanId || 'urn:uuid:' + b.id === attachment.url; });
|
|
}
|
|
if (binaryRes) {
|
|
attachmentData = binaryRes.data;
|
|
if (binaryRes.contentType) {
|
|
contentType = binaryRes.contentType;
|
|
}
|
|
}
|
|
}
|
|
|
|
var fileUrl = attachmentObjectUrl({ data: attachmentData, contentType: contentType });
|
|
var isPdf = contentType === 'application/pdf';
|
|
var isImage = contentType.indexOf('image/') === 0;
|
|
|
|
html += '<div class="col-md-6">';
|
|
html += '<div class="border rounded p-2 d-flex align-items-center gap-2 bg-light-subtle">';
|
|
if (isPdf) {
|
|
html += '<i class="bi bi-file-earmark-pdf fs-4 text-danger flex-shrink-0"></i>';
|
|
} else if (isImage) {
|
|
html += '<i class="bi bi-file-earmark-image fs-4 text-primary flex-shrink-0"></i>';
|
|
} else {
|
|
html += '<i class="bi bi-file-earmark-text fs-4 text-secondary flex-shrink-0"></i>';
|
|
}
|
|
html += '<div class="flex-grow-1 min-w-0"><div class="fw-semibold text-truncate small" title="' + hesc(title) + '">' + hesc(title) + '</div>';
|
|
html += '<small class="text-muted d-block" style="font-size:.7rem;">' + hesc(contentType) + '</small></div>';
|
|
if (fileUrl) {
|
|
html += '<a class="btn btn-sm btn-outline-primary py-0 px-2 me-1" style="font-size:.75rem;" href="' + hesc(fileUrl) + '" target="_blank" rel="noopener">Open</a>';
|
|
html += '<a class="btn btn-sm btn-primary py-0 px-2" style="font-size:.75rem;" href="' + hesc(fileUrl) + '" download="' + hesc(title) + '"><i class="bi bi-download"></i></a>';
|
|
} else {
|
|
html += '<span class="badge bg-secondary" style="font-size:.65rem;">No binary data</span>';
|
|
}
|
|
html += '</div></div>';
|
|
});
|
|
});
|
|
html += '</div></div></div>';
|
|
}
|
|
|
|
// -- Practitioner
|
|
if (practitioner) {
|
|
var pName = ((practitioner.name || [{}])[0].text || '').trim();
|
|
var pGiven = (((practitioner.name || [{}])[0].given) || []).join(' ').trim();
|
|
var pFam = ((practitioner.name || [{}])[0].family || '').trim();
|
|
var pPfx = (((practitioner.name || [{}])[0].prefix) || []).join(' ').trim();
|
|
var pDisplay = pName || (pPfx ? pPfx + ' ' : '') + pGiven + (pFam ? ' ' + pFam : '');
|
|
var hprId = ((practitioner.identifier || []).find(function(i){ return (i.system||'').includes('hpr'); }) || {}).value || '';
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Practitioner (Doctor)</small></div>';
|
|
html += '<div class="card-body py-2 small d-flex flex-wrap gap-3 align-items-center">';
|
|
html += '<span><i class="bi bi-person-badge me-1 text-primary"></i><strong>' + hesc(pDisplay || 'Unknown') + '</strong></span>';
|
|
if (hprId) html += '<span class="text-muted">HPR ID: <code>' + hesc(hprId) + '</code></span>';
|
|
html += '</div></div>';
|
|
}
|
|
|
|
// -- Organization
|
|
if (organization) {
|
|
var oName = (organization.name || '').trim();
|
|
var oIdent = (organization.identifier || []).find(function(i){ return (i.system||'').includes('facility') || (i.system||'').includes('hfr'); });
|
|
var oCode = oIdent ? (oIdent.value || '') : '';
|
|
html += '<div class="card mb-2"><div class="card-header py-1 bg-light"><small class="fw-bold text-uppercase text-secondary">Organization (Facility)</small></div>';
|
|
html += '<div class="card-body py-2 small d-flex flex-wrap gap-3 align-items-center">';
|
|
html += '<span><i class="bi bi-hospital me-1 text-primary"></i><strong>' + hesc(oName || '(Name not configured)') + '</strong></span>';
|
|
if (oCode) html += '<span class="text-muted">HFR ID: <code>' + hesc(oCode) + '</code></span>';
|
|
if (!oName) html += '<span class="badge bg-warning text-dark" style="font-size:.7rem;" title="Set H_name in Hospital Settings">Name missing</span>';
|
|
html += '</div></div>';
|
|
}
|
|
|
|
return html || '<p class="text-muted p-3">No structured clinical data found in bundle.</p>';
|
|
}
|
|
|
|
function fhirCodeQuality(bundle) {
|
|
var entries = (bundle && bundle.entry) ? bundle.entry : [];
|
|
var complaints = [], diagnoses = [], services = [], obs = [], meds = [];
|
|
entries.forEach(function(e) {
|
|
var r = e.resource;
|
|
if (!r) return;
|
|
if (r.resourceType === 'Observation') { obs.push(r); return; }
|
|
if (r.resourceType === 'MedicationRequest') { meds.push(r); return; }
|
|
if (r.resourceType === 'ServiceRequest') { services.push(r); return; }
|
|
if (r.resourceType === 'Condition') {
|
|
var cats = r.category || [];
|
|
var isComplaint = cats.some(function(c) { return (c.text || '').toLowerCase().includes('complaint'); });
|
|
if (isComplaint) complaints.push(r); else diagnoses.push(r);
|
|
}
|
|
});
|
|
function hasSnomedCoding(r) {
|
|
return (((r.code || {}).coding) || []).some(function(c) { return c.code && c.code !== ''; });
|
|
}
|
|
function hasLoincCoding(r) {
|
|
return (((r.code || {}).coding) || []).some(function(c) { return (c.system || '').includes('loinc') && c.code; });
|
|
}
|
|
var items = [];
|
|
if (complaints.length) {
|
|
var n = complaints.filter(hasSnomedCoding).length;
|
|
items.push('<span class="badge ' + (n === complaints.length ? 'bg-success' : 'bg-warning text-dark') + ' me-1">Complaints ' + n + '/' + complaints.length + ' SNOMED</span>');
|
|
}
|
|
if (diagnoses.length) {
|
|
var n = diagnoses.filter(hasSnomedCoding).length;
|
|
items.push('<span class="badge ' + (n === diagnoses.length ? 'bg-success' : 'bg-warning text-dark') + ' me-1">Diagnoses ' + n + '/' + diagnoses.length + ' SNOMED</span>');
|
|
}
|
|
if (obs.length) {
|
|
var vitalLoincCodes = { '8867-4':1, '59408-5':1, '8480-6':1, '8462-4':1, '8310-5':1, '9279-1':1, '8302-2':1, '29463-7':1 };
|
|
var vitalObs = 0;
|
|
var labObs = 0;
|
|
obs.forEach(function(o) {
|
|
var code = ((((o.code || {}).coding) || [])[0] || {}).code || '';
|
|
if (vitalLoincCodes[code]) {
|
|
vitalObs++;
|
|
} else {
|
|
labObs++;
|
|
}
|
|
});
|
|
if (vitalObs > 0) {
|
|
items.push('<span class="badge bg-success me-1">Vitals ' + vitalObs + ' LOINC</span>');
|
|
}
|
|
if (labObs > 0) {
|
|
items.push('<span class="badge bg-primary me-1">Lab Results ' + labObs + ' LOINC</span>');
|
|
}
|
|
}
|
|
if (services.length) {
|
|
var n = services.filter(hasLoincCoding).length;
|
|
items.push('<span class="badge ' + (n === services.length ? 'bg-success' : 'bg-warning text-dark') + ' me-1">Investigations ' + n + '/' + services.length + ' LOINC</span>');
|
|
}
|
|
if (meds.length) {
|
|
items.push('<span class="badge bg-secondary me-1">Medications ' + meds.length + '</span>');
|
|
}
|
|
return items.length ? '<small class="text-muted me-1">Code quality:</small>' + items.join('') : '';
|
|
}
|
|
|
|
function openFhirModal(url, title, options) {
|
|
options = options || {};
|
|
_fhirPreviewUrl = url;
|
|
_fhirSubmitMode = options.submitMode || 'opd';
|
|
_fhirSubmitTaskRow = options.taskRow || null;
|
|
_fhirSubmitAbha = (options.abhaId || '').trim();
|
|
_fhirInvoiceSource = (options.invoiceSource || '').trim();
|
|
_fhirInvoiceBillId = parseInt(options.invoiceBillId || '0', 10) || 0;
|
|
_fhirInvoicePatientId = parseInt(options.invoicePatientId || '0', 10) || 0;
|
|
var rowPushStatus = (options.pushStatus || '').toString().trim().toLowerCase();
|
|
|
|
_fhirOpdId = 0; _fhirSessionId = 0;
|
|
_fhirIpdId = 0; _fhirPatientId = 0; _fhirAbhaId = '';
|
|
try {
|
|
var urlObj = new URL(url, window.location.origin);
|
|
if (urlObj.searchParams.has('ipd_id')) _fhirIpdId = parseInt(urlObj.searchParams.get('ipd_id') || '0', 10) || 0;
|
|
if (urlObj.searchParams.has('patient_id')) _fhirPatientId = parseInt(urlObj.searchParams.get('patient_id') || '0', 10) || 0;
|
|
if (urlObj.searchParams.has('abha_id')) _fhirAbhaId = (urlObj.searchParams.get('abha_id') || '').trim();
|
|
} catch (e) {}
|
|
|
|
if (fhirModalTitle) fhirModalTitle.textContent = title || 'FHIR Preview';
|
|
if (fhirModalMeta) { fhirModalMeta.textContent = 'Loading...'; fhirModalMeta.className = 'flex-grow-1 small text-muted'; }
|
|
if (fhirModalQuality) { fhirModalQuality.innerHTML = ''; fhirModalQuality.style.display = 'none'; }
|
|
if (fhirFormView) fhirFormView.innerHTML = '<p class="text-muted p-3">Loading...</p>';
|
|
if (fhirModalJson) fhirModalJson.value = '{\n "loading": true\n}';
|
|
// Reset to Form View tab
|
|
fhirTabForm.classList.add('active'); fhirTabJson.classList.remove('active');
|
|
fhirFormView.style.display = ''; fhirModalJson.style.display = 'none';
|
|
if (fhirModalDataBadge) { fhirModalDataBadge.className = 'badge bg-warning text-dark'; fhirModalDataBadge.textContent = 'CHECKING'; }
|
|
if (fhirModalHttpBadge) { fhirModalHttpBadge.className = 'badge bg-warning text-dark'; fhirModalHttpBadge.textContent = 'HTTP ...'; }
|
|
|
|
if (btnSubmitFhirToAbdm) {
|
|
btnSubmitFhirToAbdm.disabled = false;
|
|
if (_fhirSubmitMode === 'task' || _fhirIpdId > 0 || (url && url.indexOf('ipd_discharge') !== -1)) {
|
|
btnSubmitFhirToAbdm.className = 'btn btn-sm btn-outline-warning';
|
|
btnSubmitFhirToAbdm.innerHTML = '<i class="bi bi-check2-square"></i> Verify & Submit to Gateway';
|
|
btnSubmitFhirToAbdm.title = 'Submit directly to ABDM gateway after FHIR verification';
|
|
} else if (_fhirSubmitMode === 'opd_gateway') {
|
|
btnSubmitFhirToAbdm.className = 'btn btn-sm btn-outline-warning';
|
|
btnSubmitFhirToAbdm.innerHTML = '<i class="bi bi-cloud-upload"></i> Submit to Gateway';
|
|
btnSubmitFhirToAbdm.title = 'Force gateway push for this OPD consult';
|
|
} else if (_fhirSubmitMode === 'invoice') {
|
|
btnSubmitFhirToAbdm.className = 'btn btn-sm btn-outline-warning';
|
|
btnSubmitFhirToAbdm.innerHTML = '<i class="bi bi-cloud-upload"></i> Push Invoice';
|
|
btnSubmitFhirToAbdm.title = 'Push this InvoiceRecord to the ABDM gateway';
|
|
if (['linked', 'queued', 'pushed'].indexOf(rowPushStatus) !== -1) {
|
|
btnSubmitFhirToAbdm.disabled = true;
|
|
btnSubmitFhirToAbdm.className = 'btn btn-sm btn-outline-secondary';
|
|
btnSubmitFhirToAbdm.innerHTML = '<i class="bi bi-check2-circle"></i> Already Submitted';
|
|
}
|
|
} else {
|
|
btnSubmitFhirToAbdm.className = defaultSubmitBtnClass;
|
|
btnSubmitFhirToAbdm.innerHTML = defaultSubmitBtnHtml;
|
|
btnSubmitFhirToAbdm.title = defaultSubmitBtnTitle;
|
|
if (['linked', 'queued'].indexOf(rowPushStatus) !== -1) {
|
|
btnSubmitFhirToAbdm.disabled = true;
|
|
btnSubmitFhirToAbdm.className = 'btn btn-sm btn-outline-secondary';
|
|
btnSubmitFhirToAbdm.innerHTML = '<i class="bi bi-check2-circle"></i> Already Submitted';
|
|
btnSubmitFhirToAbdm.title = 'Record is already queued or linked.';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (btnRegenerateFhirModal) {
|
|
btnRegenerateFhirModal.disabled = false;
|
|
btnRegenerateFhirModal.style.display = '';
|
|
btnRegenerateFhirModal.innerHTML = '<i class="bi bi-arrow-clockwise"></i> Recreate FHIR Record';
|
|
btnRegenerateFhirModal.title = 'Re-generate FHIR bundle fresh from current clinical and patient data';
|
|
}
|
|
|
|
fhirPreviewModal.show();
|
|
|
|
var controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
var timeoutId = controller ? setTimeout(function () { controller.abort(); }, 25000) : null;
|
|
|
|
fetch(url, {
|
|
method: 'GET',
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
|
|
signal: controller ? controller.signal : undefined
|
|
})
|
|
.then(function (r) {
|
|
if (timeoutId) clearTimeout(timeoutId);
|
|
return r.text().then(function (b) { return { ok: r.ok, status: r.status, body: b }; });
|
|
})
|
|
.then(function (res) {
|
|
var parsed = null;
|
|
try { parsed = JSON.parse(res.body || '{}'); } catch (e) { parsed = { raw: res.body || '' }; }
|
|
var bundle = (parsed && parsed.resourceType === 'Bundle') ? parsed : ((parsed && parsed.bundle) ? parsed.bundle : ((parsed && parsed.fhir_bundle) ? parsed.fhir_bundle : null));
|
|
var hasFhir = bundle !== null;
|
|
_fhirOpdId = parseInt(parsed.opd_id || '0', 10) || 0;
|
|
_fhirSessionId = parseInt(parsed.opd_session_id || '0', 10) || 0;
|
|
if (parsed.ipd_id) _fhirIpdId = parseInt(parsed.ipd_id, 10) || _fhirIpdId;
|
|
if (parsed.patient_id) _fhirPatientId = parseInt(parsed.patient_id, 10) || _fhirPatientId;
|
|
if (parsed.abha_id) _fhirAbhaId = (parsed.abha_id || _fhirAbhaId).trim();
|
|
_fhirLiveComplaints = Array.isArray(parsed.live_complaints) ? parsed.live_complaints : [];
|
|
_fhirLiveDiagnoses = Array.isArray(parsed.live_diagnoses) ? parsed.live_diagnoses : [];
|
|
if (fhirModalJson) fhirModalJson.value = JSON.stringify(bundle || parsed, null, 2);
|
|
if (fhirFormView) {
|
|
if (hasFhir) {
|
|
fhirFormView.innerHTML = renderFhirBundleForm(bundle, _fhirLiveComplaints, _fhirLiveDiagnoses);
|
|
} else if (!res.ok) {
|
|
var errMsg = (parsed && (parsed.message || parsed.error || parsed.error_text)) ? (parsed.message || parsed.error || parsed.error_text) : (parsed && parsed.raw ? parsed.raw : ('HTTP ' + res.status + ' error.'));
|
|
fhirFormView.innerHTML = '<div class="alert alert-danger m-3"><i class="bi bi-exclamation-triangle"></i> <strong>Error (' + res.status + '):</strong> ' + hesc(errMsg) + '</div>';
|
|
} else {
|
|
fhirFormView.innerHTML = '<p class="text-muted p-3">No FHIR bundle generated yet.</p>';
|
|
}
|
|
}
|
|
if (fhirModalMeta) { fhirModalMeta.textContent = res.ok ? 'Loaded (' + res.status + ')' : 'Error (' + res.status + ')'; fhirModalMeta.className = 'flex-grow-1 small ' + (res.ok ? 'text-success' : 'text-danger'); }
|
|
if (fhirModalDataBadge) { fhirModalDataBadge.className = 'badge ' + (hasFhir ? 'bg-success' : 'bg-danger'); fhirModalDataBadge.textContent = hasFhir ? 'HAS FHIR' : 'NOT GENERATED'; }
|
|
if (fhirModalHttpBadge) { fhirModalHttpBadge.className = 'badge ' + (res.ok ? 'bg-success' : 'bg-danger'); fhirModalHttpBadge.textContent = 'HTTP ' + res.status; }
|
|
if (fhirModalQuality && hasFhir) {
|
|
var qs = fhirCodeQuality(bundle);
|
|
if (qs) { fhirModalQuality.innerHTML = qs; fhirModalQuality.style.display = 'block'; }
|
|
}
|
|
})
|
|
.catch(function (e) {
|
|
if (timeoutId) clearTimeout(timeoutId);
|
|
var isTimeout = e.name === 'AbortError';
|
|
var msg = isTimeout ? 'Request timed out after 25 seconds. Please check server or retry.' : e.message;
|
|
if (fhirFormView) fhirFormView.innerHTML = '<div class="alert alert-danger m-3"><i class="bi bi-exclamation-triangle"></i> <strong>Request failed:</strong> ' + hesc(msg) + '</div>';
|
|
if (fhirModalMeta) { fhirModalMeta.textContent = 'Request failed: ' + msg; fhirModalMeta.className = 'flex-grow-1 small text-danger'; }
|
|
if (fhirModalDataBadge) { fhirModalDataBadge.className = 'badge bg-danger'; fhirModalDataBadge.textContent = 'ERROR'; }
|
|
if (fhirModalHttpBadge) { fhirModalHttpBadge.className = 'badge bg-danger'; fhirModalHttpBadge.textContent = isTimeout ? 'TIMEOUT' : 'HTTP ERR'; }
|
|
});
|
|
}
|
|
|
|
// Apply JSON changes function
|
|
function applyFhirJsonChanges(onSaved) {
|
|
var raw = (fhirModalJson ? fhirModalJson.value : '').trim();
|
|
if (!raw) {
|
|
alert('JSON content is empty.');
|
|
return false;
|
|
}
|
|
var parsed = null;
|
|
try {
|
|
parsed = JSON.parse(raw);
|
|
} catch (e) {
|
|
alert('Invalid JSON syntax:\n' + e.message);
|
|
if (fhirModalMeta) {
|
|
fhirModalMeta.textContent = 'JSON syntax error: ' + e.message;
|
|
fhirModalMeta.className = 'flex-grow-1 small text-danger';
|
|
}
|
|
return false;
|
|
}
|
|
|
|
var bundle = (parsed && parsed.resourceType === 'Bundle') ? parsed : ((parsed && parsed.bundle) ? parsed.bundle : ((parsed && parsed.fhir_bundle) ? parsed.fhir_bundle : null));
|
|
if (!bundle || bundle.resourceType !== 'Bundle') {
|
|
alert('The JSON must be a valid FHIR Bundle (resourceType: "Bundle").');
|
|
return false;
|
|
}
|
|
|
|
// Format textarea neatly
|
|
if (fhirModalJson) {
|
|
fhirModalJson.value = JSON.stringify(bundle, null, 2);
|
|
}
|
|
|
|
// Refresh Form View
|
|
if (fhirFormView) {
|
|
fhirFormView.innerHTML = renderFhirBundleForm(bundle, _fhirLiveComplaints, _fhirLiveDiagnoses);
|
|
}
|
|
|
|
// Refresh Code Quality badges
|
|
if (fhirModalQuality) {
|
|
var qs = fhirCodeQuality(bundle);
|
|
if (qs) {
|
|
fhirModalQuality.innerHTML = qs;
|
|
fhirModalQuality.style.display = 'block';
|
|
} else {
|
|
fhirModalQuality.innerHTML = '';
|
|
fhirModalQuality.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
if (fhirModalDataBadge) {
|
|
fhirModalDataBadge.className = 'badge bg-success';
|
|
fhirModalDataBadge.textContent = 'HAS FHIR';
|
|
}
|
|
|
|
var targetIpdId = _fhirIpdId || (_fhirSubmitTaskRow && (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase() === 'ipd_discharge_publish' ? parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) : 0);
|
|
var targetPatientId = _fhirPatientId || (_fhirSubmitTaskRow ? parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) : 0);
|
|
var targetAbha = (_fhirSubmitAbha || _fhirAbhaId || '').trim();
|
|
|
|
if (targetIpdId > 0) {
|
|
var origBtnHtml = btnUpdateFhirJson ? btnUpdateFhirJson.innerHTML : '';
|
|
if (btnUpdateFhirJson) {
|
|
btnUpdateFhirJson.disabled = true;
|
|
btnUpdateFhirJson.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Saving...';
|
|
}
|
|
|
|
post('<?= base_url('AbdmGateway/save_ipd_discharge_fhir') ?>', {
|
|
ipd_id: targetIpdId,
|
|
patient_id: targetPatientId,
|
|
abha_id: targetAbha,
|
|
fhir_json: fhirModalJson.value
|
|
}, function (res) {
|
|
if (btnUpdateFhirJson) {
|
|
btnUpdateFhirJson.disabled = false;
|
|
btnUpdateFhirJson.innerHTML = '<i class="bi bi-check-lg"></i> JSON Updated!';
|
|
window.setTimeout(function() {
|
|
btnUpdateFhirJson.innerHTML = origBtnHtml || '<i class="bi bi-check2-circle"></i> Update JSON';
|
|
}, 2000);
|
|
}
|
|
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
|
var err = (res && (res.error_text || res.error || res.message)) || 'Failed to save updated JSON';
|
|
if (fhirModalMeta) {
|
|
fhirModalMeta.textContent = 'Updated in preview: ' + err;
|
|
fhirModalMeta.className = 'flex-grow-1 small text-warning';
|
|
}
|
|
if (typeof onSaved === 'function') onSaved(false);
|
|
return;
|
|
}
|
|
if (fhirModalMeta) {
|
|
fhirModalMeta.textContent = 'JSON validated & saved to database.';
|
|
fhirModalMeta.className = 'flex-grow-1 small text-success';
|
|
}
|
|
if (typeof onSaved === 'function') onSaved(true);
|
|
});
|
|
return true;
|
|
}
|
|
|
|
if (btnUpdateFhirJson) {
|
|
var prevHtml = btnUpdateFhirJson.innerHTML;
|
|
btnUpdateFhirJson.innerHTML = '<i class="bi bi-check-lg"></i> Updated!';
|
|
window.setTimeout(function() {
|
|
btnUpdateFhirJson.innerHTML = prevHtml;
|
|
}, 2000);
|
|
}
|
|
if (fhirModalMeta) {
|
|
fhirModalMeta.textContent = 'JSON validated and applied.';
|
|
fhirModalMeta.className = 'flex-grow-1 small text-success';
|
|
}
|
|
if (typeof onSaved === 'function') onSaved(true);
|
|
return true;
|
|
}
|
|
|
|
if (btnUpdateFhirJson) {
|
|
btnUpdateFhirJson.addEventListener('click', function () {
|
|
applyFhirJsonChanges();
|
|
});
|
|
}
|
|
|
|
// Regenerate / Recreate FHIR bundle in modal
|
|
btnRegenerateFhirModal.addEventListener('click', function () {
|
|
if (!_fhirPreviewUrl) {
|
|
alert('No FHIR preview URL available for this record.');
|
|
return;
|
|
}
|
|
if (!confirm('Recreate FHIR Record fresh from latest clinical and patient data?')) return;
|
|
var freshUrl = _fhirPreviewUrl + (_fhirPreviewUrl.indexOf('?') !== -1 ? '&' : '?') + 'force_rebuild=1&refresh=' + Date.now();
|
|
openFhirModal(freshUrl, fhirModalTitle ? fhirModalTitle.textContent : 'FHIR Preview', {
|
|
submitMode: _fhirSubmitMode,
|
|
taskRow: _fhirSubmitTaskRow,
|
|
abhaId: _fhirSubmitAbha,
|
|
invoiceSource: _fhirInvoiceSource,
|
|
invoiceBillId: _fhirInvoiceBillId,
|
|
invoicePatientId: _fhirInvoicePatientId
|
|
});
|
|
});
|
|
|
|
// Update Codes dropdown - navigate to master workspaces
|
|
(function () {
|
|
function navTo(url, label) {
|
|
fhirPreviewModal.hide();
|
|
if (typeof window.load_form === 'function') {
|
|
window.load_form(url, label);
|
|
} else {
|
|
window.location.href = url;
|
|
}
|
|
}
|
|
var btnInvest = document.getElementById('btnGoInvestMaster');
|
|
var btnClinical = document.getElementById('btnGoClinicalMaster');
|
|
if (btnInvest) btnInvest.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
navTo('<?= base_url('Opd_prescription/opd_invest_master') ?>', 'Investigation Master');
|
|
});
|
|
if (btnClinical) btnClinical.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
navTo('<?= base_url('Opd_prescription/clinical_master_workspace') ?>', 'Clinical Master');
|
|
});
|
|
})();
|
|
|
|
btnSubmitFhirToAbdm.addEventListener('click', function () {
|
|
var btn = this;
|
|
|
|
if (_fhirSubmitMode === 'invoice') {
|
|
if (!_fhirInvoiceSource || _fhirInvoiceBillId <= 0 || _fhirInvoicePatientId <= 0) {
|
|
alert('Invoice source data is incomplete.');
|
|
return;
|
|
}
|
|
if (!confirm('Push this verified InvoiceRecord to the ABDM gateway now?')) {
|
|
return;
|
|
}
|
|
|
|
var originalInvoiceHtml = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Pushing...';
|
|
post('<?= base_url('AbdmGateway/share_invoice_source_bundle') ?>', {
|
|
source: _fhirInvoiceSource,
|
|
bill_id: _fhirInvoiceBillId,
|
|
patient_id: _fhirInvoicePatientId,
|
|
consent_handle: ''
|
|
}, function (res) {
|
|
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalInvoiceHtml;
|
|
var errorText = (res && (res.error_text || res.error || res.message)) || 'Invoice push failed';
|
|
setStatus(errorText, true);
|
|
alert('Invoice push failed:\n' + errorText);
|
|
return;
|
|
}
|
|
|
|
var invoiceRow = _fhirSubmitTaskRow;
|
|
if (invoiceRow) {
|
|
invoiceRow.setAttribute('data-push-status', res.status === 'local_stored' ? 'local_only' : 'queued');
|
|
var statusBadge = invoiceRow.querySelector('.invoice-status-badge');
|
|
if (statusBadge) {
|
|
statusBadge.textContent = res.status === 'local_stored' ? 'Local Only' : 'Submitted';
|
|
statusBadge.className = 'badge ' + (res.status === 'local_stored' ? 'bg-info' : 'bg-warning text-dark') + ' invoice-status-badge';
|
|
}
|
|
var details = invoiceRow.querySelector('.invoice-details');
|
|
if (details) {
|
|
details.innerHTML = res.queue_id
|
|
? '<div><strong>Queue:</strong> ' + hesc(String(res.queue_id)) + '</div>'
|
|
: '<span>' + hesc(res.message || '-') + '</span>';
|
|
}
|
|
var rowPushButton = invoiceRow.querySelector('.btn-invoice-push');
|
|
if (rowPushButton && res.status !== 'local_stored') {
|
|
rowPushButton.disabled = true;
|
|
}
|
|
}
|
|
|
|
btn.className = 'btn btn-sm btn-success';
|
|
btn.innerHTML = '<i class="bi bi-check-circle"></i> ' + (res.status === 'local_stored' ? 'Stored Locally' : 'Submitted');
|
|
setStatus(res.message || (res.status === 'local_stored' ? 'Invoice stored locally; patient ABHA is unavailable.' : 'Invoice submitted to ABDM gateway.'));
|
|
alert(res.message || (res.status === 'local_stored' ? 'Invoice stored locally; patient ABHA is unavailable.' : 'Invoice submitted to ABDM gateway.'));
|
|
});
|
|
return;
|
|
}
|
|
|
|
var isIpdMode = (_fhirSubmitTaskRow && (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase() === 'ipd_discharge_publish') || (_fhirIpdId > 0 && !_fhirInvoiceSource);
|
|
|
|
if (_fhirSubmitMode === 'task' || isIpdMode) {
|
|
var effectiveAbha = (_fhirSubmitAbha || _fhirAbhaId || (_fhirSubmitTaskRow && _fhirSubmitTaskRow.querySelector('.abha-input') ? _fhirSubmitTaskRow.querySelector('.abha-input').value : '')).trim();
|
|
|
|
if (!confirm('FHIR verified. Submit this record to ABDM gateway now?')) {
|
|
return;
|
|
}
|
|
|
|
if (_fhirSubmitTaskRow) {
|
|
var rowInput = _fhirSubmitTaskRow.querySelector('.abha-input');
|
|
if (rowInput && effectiveAbha) {
|
|
rowInput.value = effectiveAbha;
|
|
}
|
|
}
|
|
|
|
var taskType = _fhirSubmitTaskRow ? (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase() : (isIpdMode ? 'ipd_discharge_publish' : '');
|
|
|
|
if (taskType === 'lab_report_publish' || taskType === 'radiology_report_publish') {
|
|
var labReqId = _fhirSubmitTaskRow ? parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) : 0;
|
|
var patientId = _fhirSubmitTaskRow ? parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) : 0;
|
|
if (labReqId <= 0 || patientId <= 0) {
|
|
alert('Task data missing (lab_req_id/patient_id).');
|
|
return;
|
|
}
|
|
|
|
var origHtmlTask = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Submitting...';
|
|
|
|
post('<?= base_url('AbdmGateway/share_diagnosis_report_bundle') ?>', {
|
|
lab_req_id: labReqId,
|
|
patient_id: patientId,
|
|
abha_id: effectiveAbha,
|
|
consent_handle: ''
|
|
}, function (res) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = origHtmlTask;
|
|
|
|
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
|
var err = (res && (res.error || res.error_text || res.message)) ? (res.error || res.error_text || res.message) : 'Gateway submit failed';
|
|
setStatus(err, true);
|
|
alert('Submission failed:\n' + err);
|
|
return;
|
|
}
|
|
|
|
var queueText = res.queue_id ? (' Queue ID: ' + res.queue_id) : '';
|
|
var warningText = res.warning ? (' Warning: ' + res.warning) : '';
|
|
setSubmissionStatus('Submitted to ABDM gateway.' + queueText + warningText, res.queue_id || '');
|
|
if (_fhirSubmitTaskRow) {
|
|
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
|
if (badge) {
|
|
badge.textContent = 'IN_PROGRESS';
|
|
badge.className = 'badge bg-info status-pill';
|
|
}
|
|
}
|
|
alert('Submitted to ABDM gateway.' + queueText + warningText);
|
|
fhirPreviewModal.hide();
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (taskType === 'ipd_discharge_publish' || isIpdMode) {
|
|
var ipdId = _fhirIpdId || (_fhirSubmitTaskRow ? parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) : 0);
|
|
var ipdPatientId = _fhirPatientId || (_fhirSubmitTaskRow ? parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) : 0);
|
|
if (ipdId <= 0) {
|
|
alert('Task data missing (ipd_id).');
|
|
return;
|
|
}
|
|
|
|
var currentJson = (fhirModalJson ? fhirModalJson.value : '').trim();
|
|
if (currentJson) {
|
|
try {
|
|
JSON.parse(currentJson);
|
|
} catch (e) {
|
|
alert('Cannot submit: JSON contains syntax errors:\n' + e.message);
|
|
return;
|
|
}
|
|
}
|
|
|
|
var origHtmlIpd = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Submitting...';
|
|
|
|
post('<?= base_url('AbdmGateway/share_ipd_discharge_bundle') ?>', {
|
|
ipd_id: ipdId,
|
|
patient_id: ipdPatientId,
|
|
abha_id: effectiveAbha,
|
|
consent_handle: '',
|
|
fhir_override_json: currentJson,
|
|
force_new_record: document.getElementById('chkForceNewRecord') && document.getElementById('chkForceNewRecord').checked ? 1 : 0
|
|
}, function (res) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = origHtmlIpd;
|
|
|
|
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
|
var err = (res && (res.error || res.error_text || res.message)) ? (res.error || res.error_text || res.message) : 'Gateway submit failed';
|
|
setStatus(err, true);
|
|
alert('Submission failed:\n' + err);
|
|
return;
|
|
}
|
|
|
|
var queueText = res.queue_id ? (' Queue ID: ' + res.queue_id) : '';
|
|
var warningText = res.warning ? (' Warning: ' + res.warning) : '';
|
|
setSubmissionStatus('Submitted to ABDM gateway.' + queueText + warningText, res.queue_id || '');
|
|
if (_fhirSubmitTaskRow) {
|
|
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
|
if (badge) {
|
|
badge.textContent = 'IN_PROGRESS';
|
|
badge.className = 'badge bg-info status-pill';
|
|
}
|
|
}
|
|
alert('Submitted to ABDM gateway.' + queueText + warningText);
|
|
fhirPreviewModal.hide();
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (taskType === 'immunization_record_publish') {
|
|
var immunizationId = parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) || 0;
|
|
var immunizationPatientId = parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) || 0;
|
|
if (immunizationId <= 0 || immunizationPatientId <= 0) {
|
|
alert('Task data missing (record_id/patient_id).');
|
|
return;
|
|
}
|
|
|
|
var currentJson = (fhirModalJson ? fhirModalJson.value : '').trim();
|
|
if (currentJson) {
|
|
try {
|
|
JSON.parse(currentJson);
|
|
} catch (e) {
|
|
alert('Cannot submit: JSON contains syntax errors:\n' + e.message);
|
|
return;
|
|
}
|
|
}
|
|
|
|
var origHtmlImm = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Submitting...';
|
|
|
|
post('<?= base_url('AbdmGateway/share_immunization_bundle') ?>', {
|
|
task_id: parseInt(_fhirSubmitTaskRow.getAttribute('data-task-id') || '0', 10) || 0,
|
|
patient_id: immunizationPatientId,
|
|
record_id: immunizationId,
|
|
abha_id: effectiveAbha,
|
|
fhir_override_json: currentJson,
|
|
force_new_record: document.getElementById('chkForceNewRecord') && document.getElementById('chkForceNewRecord').checked ? 1 : 0,
|
|
push_to_gateway: 1
|
|
}, function (res) {
|
|
btn.disabled = false;
|
|
btn.innerHTML = origHtmlImm;
|
|
|
|
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
|
var err = (res && (res.error_text || res.error || res.message)) || 'Unknown error';
|
|
setStatus(err, true);
|
|
alert('ABDM Bridge push failed:\n' + err);
|
|
return;
|
|
}
|
|
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
|
if (badge) {
|
|
badge.textContent = 'BRIDGE SUBMITTED';
|
|
badge.className = 'badge bg-info status-pill';
|
|
}
|
|
setStatus('Submitted to ABDM Bridge. Queue ID: ' + (res.queue_id || '-') + '; care context: ' + (res.care_context_reference || '-'));
|
|
alert('Submitted to ABDM Bridge. Care Context: ' + (res.care_context_reference || '-'));
|
|
fhirPreviewModal.hide();
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (taskType === 'wellness_record_publish') {
|
|
var wellnessOpdId = parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) || 0;
|
|
var wellnessPatientId = parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) || 0;
|
|
if (wellnessPatientId <= 0) {
|
|
alert('Task data missing (patient_id).');
|
|
return;
|
|
}
|
|
|
|
post('<?= base_url('AbdmGateway/share_wellness_bundle') ?>', {
|
|
task_id: parseInt(_fhirSubmitTaskRow.getAttribute('data-task-id') || '0', 10) || 0,
|
|
patient_id: wellnessPatientId,
|
|
opd_id: wellnessOpdId,
|
|
abha_id: _fhirSubmitAbha,
|
|
push_to_gateway: 1
|
|
}, function (res) {
|
|
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
|
alert('ABDM Bridge push failed:\n' + ((res && (res.error_text || res.error || res.message)) || 'Unknown error'));
|
|
return;
|
|
}
|
|
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
|
if (badge) {
|
|
badge.textContent = 'BRIDGE SUBMITTED';
|
|
badge.className = 'badge bg-info status-pill';
|
|
}
|
|
setStatus('Submitted to ABDM Bridge. Queue ID: ' + (res.queue_id || '-') + '; care context: ' + (res.care_context_reference || '-'));
|
|
fhirPreviewModal.hide();
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (taskType === 'health_document_publish') {
|
|
var healthDocId = parseInt(_fhirSubmitTaskRow.getAttribute('data-entity-id') || '0', 10) || 0;
|
|
var healthDocPatientId = parseInt(_fhirSubmitTaskRow.getAttribute('data-patient-id') || '0', 10) || 0;
|
|
if (healthDocPatientId <= 0 || healthDocId <= 0) {
|
|
alert('Task data missing (record_id/patient_id).');
|
|
return;
|
|
}
|
|
|
|
post('<?= base_url('AbdmGateway/share_health_document_bundle') ?>', {
|
|
task_id: parseInt(_fhirSubmitTaskRow.getAttribute('data-task-id') || '0', 10) || 0,
|
|
patient_id: healthDocPatientId,
|
|
record_id: healthDocId,
|
|
abha_id: _fhirSubmitAbha,
|
|
push_to_gateway: 1
|
|
}, function (res) {
|
|
if (!res || parseInt(res.ok || '0', 10) !== 1) {
|
|
alert('ABDM Bridge push failed:\n' + ((res && (res.error_text || res.error || res.message)) || 'Unknown error'));
|
|
return;
|
|
}
|
|
var badge = _fhirSubmitTaskRow.querySelector('.status-pill');
|
|
if (badge) {
|
|
badge.textContent = 'BRIDGE SUBMITTED';
|
|
badge.className = 'badge bg-info status-pill';
|
|
}
|
|
setStatus('Submitted to ABDM Bridge. Queue ID: ' + (res.queue_id || '-') + '; care context: ' + (res.care_context_reference || '-'));
|
|
fhirPreviewModal.hide();
|
|
});
|
|
return;
|
|
}
|
|
|
|
runAction(_fhirSubmitTaskRow, 'submit', _fhirSubmitAbha);
|
|
fhirPreviewModal.hide();
|
|
return;
|
|
}
|
|
|
|
if (_fhirOpdId <= 0) { alert('No FHIR bundle loaded.'); return; }
|
|
var submitConfirm = 'Submit OPD #' + _fhirOpdId + ' to ABDM gateway now?\nThis will force push_to_gateway=1.';
|
|
if (!confirm(submitConfirm)) return;
|
|
var origHtml = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.textContent = 'Submitting...';
|
|
var body = new URLSearchParams({ opd_id: _fhirOpdId, opd_session_id: _fhirSessionId });
|
|
body.append('push_to_gateway', '1');
|
|
body.append(csrfName, csrfHash);
|
|
var submitTimeoutMs = 150000;
|
|
var submitController = new AbortController();
|
|
var submitTimer = window.setTimeout(function () { submitController.abort(); }, submitTimeoutMs);
|
|
fetch('<?= base_url('Opd_prescription/fhir_bundle_submit') ?>', {
|
|
method: 'POST',
|
|
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: body.toString(),
|
|
signal: submitController.signal
|
|
})
|
|
.then(function (r) {
|
|
window.clearTimeout(submitTimer);
|
|
return r.text().then(function (raw) {
|
|
var res = {};
|
|
try {
|
|
res = raw ? JSON.parse(raw) : {};
|
|
} catch (e) {
|
|
throw new Error('Invalid server response (HTTP ' + r.status + ')');
|
|
}
|
|
if (!r.ok && typeof res.ok === 'undefined') {
|
|
res.ok = 0;
|
|
res.http_code = r.status;
|
|
res.message = res.message || ('HTTP ' + r.status);
|
|
}
|
|
return res;
|
|
});
|
|
})
|
|
.then(function (res) {
|
|
if (res.csrfName && res.csrfHash) { csrfName = res.csrfName; csrfHash = res.csrfHash; }
|
|
if (res.ok == 1) {
|
|
btn.className = 'btn btn-sm btn-success';
|
|
btn.innerHTML = '<i class="bi bi-check-circle"></i> Submitted v';
|
|
|
|
var rowSubmitBtn = document.querySelector('.btn-opd-consult-submit-gateway[data-opd-id="' + String(_fhirOpdId) + '"]');
|
|
if (rowSubmitBtn) {
|
|
var row = rowSubmitBtn.closest('tr');
|
|
if (row) {
|
|
row.setAttribute('data-push-status', 'queued');
|
|
var statusBadge = row.querySelector('.opd-consult-status-badge');
|
|
if (statusBadge) {
|
|
statusBadge.textContent = 'Submitted';
|
|
statusBadge.className = 'badge bg-warning text-dark opd-consult-status-badge';
|
|
}
|
|
var detailsCell = row.querySelector('.opd-consult-details');
|
|
if (detailsCell) {
|
|
var queueHtml = res.queue_id ? ('<div><strong>Queue:</strong> ' + String(res.queue_id) + '</div>') : '';
|
|
var bridgeHtml = res.bridge_record_id ? ('<div><strong>Bridge:</strong> #' + String(res.bridge_record_id) + '</div>') : '';
|
|
detailsCell.innerHTML = '<div>Submitted to gateway and waiting for link workflow.</div>' + queueHtml + bridgeHtml;
|
|
}
|
|
}
|
|
rowSubmitBtn.disabled = false;
|
|
rowSubmitBtn.className = 'btn btn-sm btn-outline-secondary btn-opd-consult-submit-gateway ms-1';
|
|
rowSubmitBtn.textContent = 'Submit Again';
|
|
}
|
|
|
|
var qInfo = res.queue_id ? ' Queue ID: ' + res.queue_id : '';
|
|
alert(' Bundle submitted to ABDM bridge.\n' + (res.message || '') + qInfo);
|
|
} else {
|
|
btn.disabled = false;
|
|
btn.innerHTML = origHtml;
|
|
alert(' Submission failed:\n' + (res.message || res.error_text || 'Check bridge configuration.') + (res.http_code ? ' (HTTP ' + res.http_code + ')' : ''));
|
|
}
|
|
})
|
|
.catch(function (e) {
|
|
window.clearTimeout(submitTimer);
|
|
btn.disabled = false;
|
|
btn.innerHTML = origHtml;
|
|
var msg = (e && e.name === 'AbortError')
|
|
? 'Request timed out before server response. Please retry.'
|
|
: (e && e.message ? e.message : 'Request failed');
|
|
alert(' Error: ' + msg);
|
|
});
|
|
});
|
|
|
|
document.getElementById('btnCopyFhirModal').addEventListener('click', function () {
|
|
var btn = this;
|
|
var jsonText = fhirModalJson ? (fhirModalJson.textContent || '{}') : '{}';
|
|
if (! navigator.clipboard || ! navigator.clipboard.writeText) { setStatus('Clipboard API unavailable.', true); return; }
|
|
navigator.clipboard.writeText(jsonText).then(function () {
|
|
var orig = btn.textContent; btn.textContent = 'Copied v'; btn.className = 'btn btn-sm btn-success';
|
|
window.setTimeout(function () { btn.textContent = orig; btn.className = 'btn btn-sm btn-outline-secondary'; }, 1500);
|
|
}).catch(function (e) { setStatus('Copy failed: ' + e.message, true); });
|
|
});
|
|
|
|
// OPD Consult Publish - FHIR preview buttons open in modal
|
|
document.querySelectorAll('.btn-opd-consult-fhir').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var opdId = parseInt(btn.getAttribute('data-opd-id') || '0', 10);
|
|
var tr = btn.closest('tr');
|
|
var pushStatus = tr ? (tr.getAttribute('data-push-status') || '') : '';
|
|
if (opdId <= 0) return;
|
|
openFhirModal('<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + opdId, 'FHIR Preview - OPD #' + opdId, { pushStatus: pushStatus });
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.btn-opd-consult-submit-gateway').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var opdId = parseInt(btn.getAttribute('data-opd-id') || '0', 10);
|
|
var abhaId = (btn.getAttribute('data-abha-id') || '').trim();
|
|
if (opdId <= 0) return;
|
|
openFhirModal(
|
|
'<?= base_url('Opd_prescription/fhir_bundle_preview') ?>/' + opdId,
|
|
'Gateway Submit - OPD #' + opdId,
|
|
{ submitMode: 'opd_gateway', abhaId: abhaId }
|
|
);
|
|
});
|
|
});
|
|
|
|
function openInvoicePreview(row, pushIntent) {
|
|
var source = (row.getAttribute('data-invoice-source') || '').trim();
|
|
var billId = parseInt(row.getAttribute('data-bill-id') || '0', 10) || 0;
|
|
var patientId = parseInt(row.getAttribute('data-patient-id') || '0', 10) || 0;
|
|
var pushStatus = (row.getAttribute('data-push-status') || '').trim();
|
|
if (!source || billId <= 0 || patientId <= 0) {
|
|
setStatus('Invoice source data is incomplete.', true);
|
|
return;
|
|
}
|
|
var previewUrl = '<?= base_url('AbdmGateway/invoice_fhir_preview') ?>'
|
|
+ '?source=' + encodeURIComponent(source)
|
|
+ '&bill_id=' + encodeURIComponent(String(billId))
|
|
+ '&patient_id=' + encodeURIComponent(String(patientId));
|
|
openFhirModal(previewUrl, (pushIntent ? 'Review & Push' : 'FHIR Preview') + ' - Invoice #' + billId, {
|
|
submitMode: 'invoice',
|
|
taskRow: row,
|
|
pushStatus: pushStatus,
|
|
invoiceSource: source,
|
|
invoiceBillId: billId,
|
|
invoicePatientId: patientId
|
|
});
|
|
}
|
|
|
|
document.querySelectorAll('.btn-invoice-preview').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var row = btn.closest('tr');
|
|
if (row) openInvoicePreview(row, false);
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.btn-invoice-push').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var row = btn.closest('tr');
|
|
if (row) openInvoicePreview(row, true);
|
|
});
|
|
});
|
|
|
|
var dashboardRangeForm = document.getElementById('dashboardRangeForm');
|
|
if (dashboardRangeForm) {
|
|
dashboardRangeForm.addEventListener('submit', function (event) {
|
|
if (typeof window.load_form !== 'function') {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
var dateFrom = document.getElementById('dashboardDateFrom').value;
|
|
var dateTo = document.getElementById('dashboardDateTo').value;
|
|
var rangeUrl = refreshUrl
|
|
+ '?date_from=' + encodeURIComponent(dateFrom)
|
|
+ '&date_to=' + encodeURIComponent(dateTo);
|
|
window.load_form(rangeUrl, 'ABDM Task Board');
|
|
});
|
|
}
|
|
|
|
document.getElementById('btnRefresh').addEventListener('click', function () {
|
|
if (typeof window.load_form === 'function') {
|
|
window.load_form(refreshUrl, 'ABDM Task Board');
|
|
return;
|
|
}
|
|
window.location.href = refreshUrl;
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|