ABDM: add manual Consent Request form with editable HI types + date range (Phase 2)
- New Patient::abdm_content_request_custom(int \) endpoint builds a custom
consent{} block (purpose, hiTypes, permission.dateRange, dataEraseAt) from
user-submitted hi_types[]/date_from/date_to/purpose form fields, instead of
always using M3HiuWorkflowService::buildDefaultConsent()'s hardcoded
OPConsultation+DiagnosticReport / 365-day defaults. Reuses the existing
requester/hiu-id resolution and hiTypes normalization already built into
preparePayloadForOperation() for the consent_request operation, so no
M3HiuWorkflowService changes were needed.
- New route (POST) patient/abdm_content_request_custom/(:num).
- New 'Custom Request' button + '#abdmCustomConsentModal' form
(Patient_Profile_Opd_V.php) with HI type checkboxes (OPConsultation,
DiagnosticReport, Prescription, DischargeSummary, HealthDocument,
ImmunizationRecord, Wellness, Invoice), Valid From/To date inputs
(defaulted to today-365d..today), and a Purpose text field. On submit,
reuses the existing auto-flow polling loop (runAutoFlowStep) with the
returned request_id to track status through to data fetch.
- Live-verified end-to-end on patient 11 (Devender Singh): submitted a
request for OPConsultation + DiagnosticReport + Prescription; sandbox
granted only OPConsultation/DiagnosticReport, and the Phase 1 consent
detail modal correctly showed Prescription as DENIED alongside the two
GRANTED types, confirming the partial-grant per-HI-type breakdown works
correctly end-to-end.
This commit is contained in:
parent
7f47045c55
commit
48d19f0ff7
|
|
@ -502,6 +502,7 @@ $routes->group('billing', function($routes) {
|
|||
$routes->get('patient/abdm_content_auto_flow/(:num)', 'Patient::abdm_content_auto_flow/$1');
|
||||
$routes->get('patient/abdm_content_fetch_only/(:num)', 'Patient::abdm_content_fetch_only/$1');
|
||||
$routes->get('patient/abdm_consent_detail/(:num)', 'Patient::abdm_consent_detail/$1');
|
||||
$routes->post('patient/abdm_content_request_custom/(:num)', 'Patient::abdm_content_request_custom/$1');
|
||||
$routes->get('patient/abdm_timeline/(:num)', 'Patient::abdm_timeline/$1');
|
||||
$routes->post('patient/save_profile_image/(:num)', 'Patient::save_profile_image/$1');
|
||||
$routes->match(['GET', 'POST'], 'patient/patient_file_upload/(:num)', 'Patient::patient_file_upload/$1');
|
||||
|
|
|
|||
|
|
@ -1320,6 +1320,165 @@ class Patient extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual consent request form submission — lets the user pick specific Health
|
||||
* Information Types and an editable validity date range instead of always
|
||||
* using the hardcoded defaults from M3HiuWorkflowService::buildDefaultConsent().
|
||||
* Returns the same shape as abdm_content_request() so the existing JS auto-flow
|
||||
* polling (runAutoFlowStep) can pick up the request_id and continue tracking
|
||||
* consent status / data fetch to completion.
|
||||
*/
|
||||
public function abdm_content_request_custom(int $pno)
|
||||
{
|
||||
if (! $this->db->tableExists('patient_master')) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'patient_master table not found',
|
||||
]);
|
||||
}
|
||||
|
||||
$patientRow = $this->db->table('patient_master')->where('id', $pno)->get()->getRowArray();
|
||||
if (! is_array($patientRow)) {
|
||||
return $this->response->setStatusCode(404)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Patient not found',
|
||||
]);
|
||||
}
|
||||
|
||||
$abhaContext = $this->buildAbhaProfileContext($patientRow);
|
||||
if ($abhaContext['abha_address'] === '') {
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'ABHA Address not available for this patient.',
|
||||
'abha' => $abhaContext,
|
||||
]);
|
||||
}
|
||||
|
||||
if ((int) ($abhaContext['is_verified'] ?? 0) !== 1) {
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'ABHA is not marked as verified in HMS. Please verify ABHA first, then request content.',
|
||||
'abha' => $abhaContext,
|
||||
]);
|
||||
}
|
||||
|
||||
$hiTypesInput = $this->request->getPost('hi_types');
|
||||
if (! is_array($hiTypesInput)) {
|
||||
$decoded = json_decode((string) $hiTypesInput, true);
|
||||
$hiTypesInput = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
$hiTypes = $this->normalizeHiTypesList($hiTypesInput);
|
||||
if ($hiTypes === []) {
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Select at least one Health Information Type.',
|
||||
]);
|
||||
}
|
||||
|
||||
$dateFromInput = trim((string) $this->request->getPost('date_from'));
|
||||
$dateToInput = trim((string) $this->request->getPost('date_to'));
|
||||
$purposeInput = trim((string) $this->request->getPost('purpose'));
|
||||
|
||||
$istTz = new \DateTimeZone('Asia/Kolkata');
|
||||
$utcTz = new \DateTimeZone('UTC');
|
||||
$nowIst = new \DateTimeImmutable('now', $istTz);
|
||||
|
||||
try {
|
||||
$fromIst = $dateFromInput !== '' ? new \DateTimeImmutable($dateFromInput, $istTz) : $nowIst->modify('-365 days');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Invalid "Valid From" date.',
|
||||
]);
|
||||
}
|
||||
try {
|
||||
$toIst = $dateToInput !== '' ? new \DateTimeImmutable($dateToInput, $istTz) : $nowIst;
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Invalid "Valid To" date.',
|
||||
]);
|
||||
}
|
||||
if ($fromIst->getTimestamp() >= $toIst->getTimestamp()) {
|
||||
return $this->response->setStatusCode(422)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => '"Valid From" date must be earlier than "Valid To" date.',
|
||||
]);
|
||||
}
|
||||
|
||||
$safeNowIst = $nowIst->modify('-120 seconds');
|
||||
if ($toIst->getTimestamp() > $safeNowIst->getTimestamp()) {
|
||||
$toIst = $safeNowIst;
|
||||
}
|
||||
|
||||
$fromUtc = $fromIst->setTime(0, 0, 0)->setTimezone($utcTz)->format('Y-m-d\TH:i:s.000\Z');
|
||||
$toUtc = $toIst->setTimezone($utcTz)->format('Y-m-d\TH:i:s.000\Z');
|
||||
$eraseAtUtc = $nowIst->modify('+365 days')->setTime(23, 59, 59)->setTimezone($utcTz)->format('Y-m-d\TH:i:s.000\Z');
|
||||
|
||||
$consent = [
|
||||
'purpose' => [
|
||||
'code' => 'CAREMGT',
|
||||
'text' => $purposeInput !== '' ? $purposeInput : 'Care Management',
|
||||
'refUri' => 'https://abdm.gov.in',
|
||||
],
|
||||
'patient' => [
|
||||
'id' => $abhaContext['abha_address'],
|
||||
],
|
||||
'hiTypes' => $hiTypes,
|
||||
'permission' => [
|
||||
'accessMode' => 'VIEW',
|
||||
'dateRange' => [
|
||||
'from' => $fromUtc,
|
||||
'to' => $toUtc,
|
||||
],
|
||||
'dataEraseAt' => $eraseAtUtc,
|
||||
'frequency' => [
|
||||
'unit' => 'HOUR',
|
||||
'value' => 1,
|
||||
'repeats' => 0,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$service = new \App\Libraries\Abdm\M3HiuWorkflowService();
|
||||
$payload = [
|
||||
'patient_id' => $pno,
|
||||
'abha_address' => $abhaContext['abha_address'],
|
||||
'consent' => $consent,
|
||||
];
|
||||
|
||||
try {
|
||||
$result = $service->runOperation('consent_request', $payload);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => 'Unable to start content request: ' . $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
$ok = (int) ($result['ok'] ?? 0) === 1;
|
||||
$httpCode = (int) ($result['http_code'] ?? ($ok ? 200 : 422));
|
||||
if ($httpCode < 100 || $httpCode > 599) {
|
||||
$httpCode = $ok ? 200 : 422;
|
||||
}
|
||||
|
||||
if (! $ok) {
|
||||
return $this->response->setStatusCode($httpCode)->setJSON([
|
||||
'ok' => 0,
|
||||
'error' => (string) ($result['error_text'] ?? 'Content request failed.'),
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setStatusCode($httpCode)->setJSON([
|
||||
'ok' => 1,
|
||||
'message' => 'Consent/content request created successfully.',
|
||||
'workflow_state' => 'REQUESTED',
|
||||
'request_id' => (string) ($result['request_id'] ?? ''),
|
||||
'data' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
public function abdm_content_auto_flow(int $pno)
|
||||
{
|
||||
if (! $this->db->tableExists('patient_master')) {
|
||||
|
|
|
|||
|
|
@ -316,6 +316,9 @@ if ($patientPhotoPath === '') {
|
|||
<span class="abdm-btn-spinner spinner-border spinner-border-sm me-1 d-none" role="status" aria-hidden="true"></span>
|
||||
<span id="btnAutoAbdmFlowLabel">Fetch ABDM Records</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="btnCustomAbdmRequest" data-bs-toggle="modal" data-bs-target="#abdmCustomConsentModal" <?= ($abhaIsVerified && $patientAbhaAddress !== '') ? '' : 'disabled' ?>>
|
||||
Custom Request
|
||||
</button>
|
||||
<button type="button" class="btn btn-success d-none" id="btnFetchRecordsOnly">
|
||||
<span class="abdm-btn-spinner spinner-border spinner-border-sm me-1 d-none" role="status" aria-hidden="true"></span>
|
||||
<span id="btnFetchRecordsOnlyLabel">Fetch Records</span>
|
||||
|
|
@ -433,6 +436,94 @@ if ($patientPhotoPath === '') {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="abdmCustomConsentModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">New Consent Request</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-semibold">Health Information Types</label>
|
||||
<div class="row row-cols-2 row-cols-md-3 g-2" id="abdmCustomHiTypes">
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="OPConsultation" id="hiOPConsultation" checked>
|
||||
<label class="form-check-label" for="hiOPConsultation">OP Consultation</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="DiagnosticReport" id="hiDiagnosticReport" checked>
|
||||
<label class="form-check-label" for="hiDiagnosticReport">Diagnostic Report</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="Prescription" id="hiPrescription">
|
||||
<label class="form-check-label" for="hiPrescription">Prescription</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="DischargeSummary" id="hiDischargeSummary">
|
||||
<label class="form-check-label" for="hiDischargeSummary">Discharge Summary</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="HealthDocument" id="hiHealthDocument">
|
||||
<label class="form-check-label" for="hiHealthDocument">Health Document</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="ImmunizationRecord" id="hiImmunizationRecord">
|
||||
<label class="form-check-label" for="hiImmunizationRecord">Immunization Record</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="Wellness" id="hiWellness">
|
||||
<label class="form-check-label" for="hiWellness">Wellness</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="Invoice" id="hiInvoice">
|
||||
<label class="form-check-label" for="hiInvoice">Invoice</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold" for="abdmCustomDateFrom">Valid From</label>
|
||||
<input type="date" class="form-control" id="abdmCustomDateFrom">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-semibold" for="abdmCustomDateTo">Valid To</label>
|
||||
<input type="date" class="form-control" id="abdmCustomDateTo">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label fw-semibold" for="abdmCustomPurpose">Purpose</label>
|
||||
<input type="text" class="form-control" id="abdmCustomPurpose" value="Care Management">
|
||||
</div>
|
||||
<div class="small text-danger d-none" id="abdmCustomConsentError"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="btnSendCustomConsent">
|
||||
<span class="spinner-border spinner-border-sm me-1 d-none" id="abdmCustomConsentSpinner" role="status" aria-hidden="true"></span>
|
||||
Send Consent Request
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
|
|
@ -442,6 +533,7 @@ $(function() {
|
|||
var abdmAutoFlowUrl = '<?= base_url('billing/patient/abdm_content_auto_flow/' . (int) ($patient->id ?? 0)) ?>';
|
||||
var abdmFetchOnlyUrl = '<?= base_url('billing/patient/abdm_content_fetch_only/' . (int) ($patient->id ?? 0)) ?>';
|
||||
var abdmConsentDetailUrl = '<?= base_url('billing/patient/abdm_consent_detail/' . (int) ($patient->id ?? 0)) ?>';
|
||||
var abdmCustomRequestUrl = '<?= base_url('billing/patient/abdm_content_request_custom/' . (int) ($patient->id ?? 0)) ?>';
|
||||
var abdmDocDetailBaseUrl = '<?= base_url('billing/patient/abdm_document_detail/' . (int) ($patient->id ?? 0)) ?>';
|
||||
var currentAbdmRows = [];
|
||||
var autoFlowTimer = null;
|
||||
|
|
@ -1045,6 +1137,80 @@ $(function() {
|
|||
loadAbdmConsentDetail();
|
||||
});
|
||||
|
||||
$('#abdmCustomConsentModal').on('show.bs.modal', function() {
|
||||
$('#abdmCustomConsentError').addClass('d-none').text('');
|
||||
if (!$('#abdmCustomDateFrom').val()) {
|
||||
var from = new Date();
|
||||
from.setDate(from.getDate() - 365);
|
||||
$('#abdmCustomDateFrom').val(from.toISOString().slice(0, 10));
|
||||
}
|
||||
if (!$('#abdmCustomDateTo').val()) {
|
||||
$('#abdmCustomDateTo').val(new Date().toISOString().slice(0, 10));
|
||||
}
|
||||
});
|
||||
|
||||
$(document).off('click.abdmOpd', '#btnSendCustomConsent').on('click.abdmOpd', '#btnSendCustomConsent', function() {
|
||||
if (autoFlowRunning || fetchOnlyRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
var hiTypes = [];
|
||||
$('#abdmCustomHiTypes input:checked').each(function() {
|
||||
hiTypes.push($(this).val());
|
||||
});
|
||||
if (!hiTypes.length) {
|
||||
$('#abdmCustomConsentError').removeClass('d-none').text('Select at least one Health Information Type.');
|
||||
return;
|
||||
}
|
||||
|
||||
var dateFrom = $('#abdmCustomDateFrom').val();
|
||||
var dateTo = $('#abdmCustomDateTo').val();
|
||||
if (dateFrom && dateTo && dateFrom >= dateTo) {
|
||||
$('#abdmCustomConsentError').removeClass('d-none').text('"Valid From" date must be earlier than "Valid To" date.');
|
||||
return;
|
||||
}
|
||||
|
||||
var $btn = $('#btnSendCustomConsent');
|
||||
$btn.prop('disabled', true);
|
||||
$('#abdmCustomConsentSpinner').removeClass('d-none');
|
||||
$('#abdmCustomConsentError').addClass('d-none').text('');
|
||||
|
||||
$.ajax({
|
||||
url: abdmCustomRequestUrl,
|
||||
method: 'POST',
|
||||
data: {
|
||||
hi_types: hiTypes,
|
||||
date_from: dateFrom,
|
||||
date_to: dateTo,
|
||||
purpose: $('#abdmCustomPurpose').val()
|
||||
}
|
||||
}).done(function(data) {
|
||||
if (!data || data.ok !== 1) {
|
||||
throw new Error((data && data.error) || 'Consent request failed.');
|
||||
}
|
||||
|
||||
var bsModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('abdmCustomConsentModal'));
|
||||
bsModal.hide();
|
||||
|
||||
stopAutoFlowLoop();
|
||||
autoFlowAttempts = 0;
|
||||
autoFlowNeedsFreshRequest = false;
|
||||
autoFlowRequestId = (data.request_id || '').toString();
|
||||
setFlowProgress(1);
|
||||
autoFlowRunning = true;
|
||||
applyConsentButtonState();
|
||||
$('#abdmStatusBox').removeClass('text-danger').addClass('text-muted').text('Sending custom consent request to patient\'s ABHA app...');
|
||||
runAutoFlowStep();
|
||||
}).fail(function(xhr) {
|
||||
var data = xhr.responseJSON;
|
||||
var errMsg = (data && data.error) || 'Consent request failed.';
|
||||
$('#abdmCustomConsentError').removeClass('d-none').text(errMsg);
|
||||
}).always(function() {
|
||||
$btn.prop('disabled', false);
|
||||
$('#abdmCustomConsentSpinner').addClass('d-none');
|
||||
});
|
||||
});
|
||||
|
||||
$('[data-bs-toggle="tooltip"]').tooltip();
|
||||
|
||||
applyConsentButtonState();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user