Enhance camera capture/edit permissions and fix OPD AdviceBlock rendering
This commit is contained in:
parent
fd8e96747d
commit
540de8c76e
|
|
@ -113,6 +113,7 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'beta.access' => 'Can access beta-level features',
|
||||
'billing.access' => 'Can access billing module',
|
||||
'opd.doctor-panel.access' => 'Can access OPD doctor work panel (doctor-only)',
|
||||
'media.image.preupload-edit' => 'Can rotate/crop images before upload (requires hospital setting ON)',
|
||||
|
||||
'billing.patient.edit-name-anytime' => 'Can edit patient name even after 24-hour restriction',
|
||||
'billing.opd.edit' => 'Can edit OPD registrations',
|
||||
|
|
@ -199,6 +200,7 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'users.*',
|
||||
'beta.*',
|
||||
'billing.*',
|
||||
'media.image.preupload-edit',
|
||||
'opd.doctor-panel.access',
|
||||
'settings.bed_status.view',
|
||||
'settings.charges.access',
|
||||
|
|
@ -221,6 +223,7 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'users.delete',
|
||||
'beta.access',
|
||||
'billing.*',
|
||||
'media.image.preupload-edit',
|
||||
'billing.patient.edit-name-anytime',
|
||||
'opd.doctor-panel.access',
|
||||
'settings.bed_status.view',
|
||||
|
|
@ -244,6 +247,7 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'users.edit',
|
||||
'beta.access',
|
||||
'billing.*',
|
||||
'media.image.preupload-edit',
|
||||
'opd.doctor-panel.access',
|
||||
'settings.bed_status.view',
|
||||
'settings.charges.access',
|
||||
|
|
|
|||
|
|
@ -2934,6 +2934,7 @@ class Opd extends BaseController
|
|||
if (!empty($data['rx_advices']) && is_array($data['rx_advices'])) {
|
||||
$parts = [];
|
||||
$partsLocal = [];
|
||||
$partsPreferred = [];
|
||||
foreach ($data['rx_advices'] as $adv) {
|
||||
$txt = trim((string) ($adv['advice_txt'] ?? $adv['advice'] ?? ''));
|
||||
if ($txt !== '') {
|
||||
|
|
@ -2945,10 +2946,51 @@ class Opd extends BaseController
|
|||
if ($localTxt !== '') {
|
||||
$partsLocal[] = $localTxt;
|
||||
}
|
||||
|
||||
$preferredTxt = $localTxt !== '' ? $localTxt : $txt;
|
||||
if ($preferredTxt !== '') {
|
||||
$partsPreferred[] = $preferredTxt;
|
||||
}
|
||||
}
|
||||
|
||||
if ($rxAdvice === '' && !empty($parts)) {
|
||||
$rxAdvice = implode(', ', array_values(array_unique($parts)));
|
||||
// Build final printable advice as: typed advice + selected advice
|
||||
// (selected uses Hindi when available, else English), with de-duplication.
|
||||
$typedAdviceLines = [];
|
||||
$typedRaw = trim((string) $rxAdvice);
|
||||
if ($typedRaw !== '') {
|
||||
$typedRaw = str_replace(["\r\n", "\r"], "\n", $typedRaw);
|
||||
$typedAdviceLines = array_values(array_filter(array_map(
|
||||
static fn(string $v): string => trim($v),
|
||||
explode("\n", $typedRaw)
|
||||
), static fn(string $v): bool => $v !== ''));
|
||||
}
|
||||
|
||||
$selectedPreferred = !empty($partsPreferred)
|
||||
? array_values(array_unique($partsPreferred))
|
||||
: array_values(array_unique($parts));
|
||||
|
||||
$seen = [];
|
||||
$combined = [];
|
||||
foreach ($typedAdviceLines as $line) {
|
||||
$key = mb_strtolower(trim($line));
|
||||
if ($key === '' || isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$key] = true;
|
||||
$combined[] = $line;
|
||||
}
|
||||
foreach ($selectedPreferred as $line) {
|
||||
$line = trim((string) $line);
|
||||
$key = mb_strtolower($line);
|
||||
if ($key === '' || isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$key] = true;
|
||||
$combined[] = $line;
|
||||
}
|
||||
|
||||
if (!empty($combined)) {
|
||||
$rxAdvice = implode("\n", $combined);
|
||||
}
|
||||
|
||||
if (!empty($partsLocal)) {
|
||||
|
|
@ -2986,9 +3028,6 @@ class Opd extends BaseController
|
|||
$complaintLocal = $this->translateToLocalPatientText($complaintText);
|
||||
$diagnosisLocal = $this->translateToLocalPatientText($diagnosisText);
|
||||
$investigationLocal = $this->translateToLocalPatientText($rxInvestigation);
|
||||
if ($rxAdviceLocal === '') {
|
||||
$rxAdviceLocal = $this->translateToLocalPatientText($rxAdvice);
|
||||
}
|
||||
|
||||
if (strtolower(trim($complaintLocal)) === strtolower(trim($complaintText))) {
|
||||
$complaintLocal = '';
|
||||
|
|
@ -4200,18 +4239,10 @@ class Opd extends BaseController
|
|||
$tokens['RemarksBlock'] = (string) ($tokens['Prescriber_Remarks'] ?? '');
|
||||
$adviceBlockBody = trim((string) ($tokens['advice'] ?? ''));
|
||||
$adviceBlockLocal = trim((string) ($tokens['advice_local'] ?? ''));
|
||||
if ($adviceBlockBody !== '' || $adviceBlockLocal !== '') {
|
||||
$adviceBlockLines = [];
|
||||
if ($adviceBlockBody !== '') {
|
||||
$adviceBlockLines[] = '<strong>Advice</strong> : ' . $adviceBlockBody;
|
||||
}
|
||||
if ($adviceBlockLocal !== '') {
|
||||
$adviceBlockLines[] = '<strong>सलाह</strong> : ' . $adviceBlockLocal;
|
||||
}
|
||||
$tokens['AdviceBlock'] = implode('<br>', $adviceBlockLines);
|
||||
} else {
|
||||
$tokens['AdviceBlock'] = '';
|
||||
}
|
||||
$adviceBlockPreferred = $adviceBlockBody !== '' ? $adviceBlockBody : $adviceBlockLocal;
|
||||
$tokens['AdviceBlock'] = $adviceBlockPreferred !== ''
|
||||
? ('<strong>Advice</strong> : ' . $adviceBlockPreferred)
|
||||
: '';
|
||||
$tokens['NextVisitBlock'] = (string) ($tokens['next_visit'] ?? '');
|
||||
|
||||
$tokens['RxTable'] = $medicalHtml;
|
||||
|
|
@ -4296,7 +4327,8 @@ class Opd extends BaseController
|
|||
$tokens['content'] = (string) ($tokens['content'] ?? '');
|
||||
|
||||
$adviceLocal = trim((string) ($tokens['advice_local'] ?? ''));
|
||||
$tokens['advice_local_line'] = $adviceLocal !== ''
|
||||
$adviceMain = trim((string) ($tokens['advice'] ?? ''));
|
||||
$tokens['advice_local_line'] = ($adviceMain === '' && $adviceLocal !== '')
|
||||
? ('<br><strong>सलाह:</strong> ' . $adviceLocal)
|
||||
: '';
|
||||
|
||||
|
|
@ -6062,10 +6094,15 @@ class Opd extends BaseController
|
|||
$personInfo[0]->age = get_age_1($personInfo[0]->dob ?? null, $personInfo[0]->age ?? '', $personInfo[0]->age_in_month ?? '', $personInfo[0]->estimate_dob ?? '', $opdRow->apointment_date ?? null);
|
||||
}
|
||||
|
||||
$hospitalEnabled = hospital_setting_value('ALLOW_IMAGE_PREUPLOAD_EDIT', '0') === '1';
|
||||
$user = function_exists('auth') ? auth()->user() : null;
|
||||
$userAllowed = $user && method_exists($user, 'can') && $user->can('media.image.preupload-edit');
|
||||
|
||||
return view('billing/opd_scan_modal_body', [
|
||||
'opdid' => $opdid,
|
||||
'opd_master' => $opdMaster,
|
||||
'person_info' => $personInfo,
|
||||
'allow_image_preupload_edit' => ($hospitalEnabled && $userAllowed) ? 1 : 0,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -953,10 +953,14 @@ class Patient extends BaseController
|
|||
}
|
||||
|
||||
$profileFilePath = $this->getProfileFilePath((int) ($patient->profile_file_id ?? 0));
|
||||
$hospitalEnabled = hospital_setting_value('ALLOW_IMAGE_PREUPLOAD_EDIT', '0') === '1';
|
||||
$user = function_exists('auth') ? auth()->user() : null;
|
||||
$userAllowed = $user && method_exists($user, 'can') && $user->can('media.image.preupload-edit');
|
||||
|
||||
return view('billing/Patient_Profile_Image_V', [
|
||||
'patient' => $patient,
|
||||
'profileFilePath' => $profileFilePath,
|
||||
'allow_image_preupload_edit' => ($hospitalEnabled && $userAllowed) ? 1 : 0,
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class HospitalProfile extends BaseController
|
|||
'sidebar_auto_hide_seconds' => $this->readSettingValue('SIDEBAR_AUTO_HIDE_SECONDS') !== ''
|
||||
? $this->readSettingValue('SIDEBAR_AUTO_HIDE_SECONDS')
|
||||
: '7',
|
||||
'allow_image_preupload_edit' => $this->readSettingValue('ALLOW_IMAGE_PREUPLOAD_EDIT') === '1' ? '1' : '0',
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +70,7 @@ class HospitalProfile extends BaseController
|
|||
$pharmacyGst = trim((string) $this->request->getPost('pharmacy_gst'));
|
||||
$sidebarAutoHideSecondsRaw = trim((string) $this->request->getPost('sidebar_auto_hide_seconds'));
|
||||
$sidebarAutoHideSeconds = $sidebarAutoHideSecondsRaw === '' ? 7 : (int) $sidebarAutoHideSecondsRaw;
|
||||
$allowImagePreuploadEdit = $this->request->getPost('allow_image_preupload_edit') === '1' ? '1' : '0';
|
||||
|
||||
if ($name === '' || $address === '') {
|
||||
return $this->response->setJSON([
|
||||
|
|
@ -134,6 +136,9 @@ class HospitalProfile extends BaseController
|
|||
if ($this->upsertSettingValue('SIDEBAR_AUTO_HIDE_SECONDS', (string) $sidebarAutoHideSeconds)) {
|
||||
$savedCount++;
|
||||
}
|
||||
if ($this->upsertSettingValue('ALLOW_IMAGE_PREUPLOAD_EDIT', $allowImagePreuploadEdit)) {
|
||||
$savedCount++;
|
||||
}
|
||||
|
||||
$logoResult = $this->processLogoUpload(
|
||||
$this->request->getFile('hospital_logo'),
|
||||
|
|
@ -244,6 +249,7 @@ class HospitalProfile extends BaseController
|
|||
'H_Med_GST',
|
||||
'H_Med_logo',
|
||||
'SIDEBAR_AUTO_HIDE_SECONDS',
|
||||
'ALLOW_IMAGE_PREUPLOAD_EDIT',
|
||||
])->delete();
|
||||
|
||||
if ($logo !== '') {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,14 @@
|
|||
<input type="number" class="form-control" id="sidebar_auto_hide_seconds" min="0" max="60" step="1" value="<?= esc($sidebar_auto_hide_seconds ?? '7') ?>" placeholder="7">
|
||||
<div class="form-text">Desktop only. Set 0 to disable auto-hide.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label d-block">Image Edit Permission</label>
|
||||
<div class="form-check form-switch mt-2">
|
||||
<input class="form-check-input" type="checkbox" id="allow_image_preupload_edit" <?= ($allow_image_preupload_edit ?? '0') === '1' ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="allow_image_preupload_edit">Allow rotate/crop before image upload</label>
|
||||
</div>
|
||||
<div class="form-text">Global master switch. User must also be granted permission <strong>media.image.preupload-edit</strong> from User Management → Give Permissions.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-7">
|
||||
<label class="form-label">Hospital Logo</label>
|
||||
|
|
@ -159,6 +167,7 @@
|
|||
fd.append('hospital_hfr_id', ($('#hospital_hfr_id').val() || '').trim());
|
||||
fd.append('footer_version', ($('#footer_version').val() || '').trim());
|
||||
fd.append('sidebar_auto_hide_seconds', ($('#sidebar_auto_hide_seconds').val() || '').trim());
|
||||
fd.append('allow_image_preupload_edit', $('#allow_image_preupload_edit').is(':checked') ? '1' : '0');
|
||||
fd.append('pharmacy_name', ($('#pharmacy_name').val() || '').trim());
|
||||
fd.append('pharmacy_address', ($('#pharmacy_address').val() || '').trim());
|
||||
fd.append('pharmacy_phone', ($('#pharmacy_phone').val() || '').trim());
|
||||
|
|
@ -275,6 +284,7 @@
|
|||
$('#hospital_hfr_id').val('');
|
||||
$('#footer_version').val('');
|
||||
$('#sidebar_auto_hide_seconds').val('7');
|
||||
$('#allow_image_preupload_edit').prop('checked', false);
|
||||
$('#pharmacy_name').val('');
|
||||
$('#pharmacy_address').val('');
|
||||
$('#pharmacy_phone').val('');
|
||||
|
|
|
|||
|
|
@ -29,7 +29,16 @@
|
|||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded p-2" style="min-height: 200px;">
|
||||
<video id="camera" autoplay playsinline class="w-100 rounded" style="max-height: 240px;"></video>
|
||||
<video id="camera" autoplay muted playsinline class="w-100 rounded" style="max-height: 240px;"></video>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<label for="profile_camera_select" class="form-label mb-1 small">Camera</label>
|
||||
<div class="d-flex gap-2 align-items-start">
|
||||
<select id="profile_camera_select" class="form-select form-select-sm">
|
||||
<option value="">Default camera</option>
|
||||
</select>
|
||||
<button type="button" id="profile_camera_refresh" class="btn btn-outline-secondary btn-sm">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-2">
|
||||
<button type="button" id="start_camera" class="btn btn-outline-primary btn-sm">Start Camera</button>
|
||||
|
|
@ -37,6 +46,7 @@
|
|||
<button type="button" id="capture_btn" class="btn btn-primary btn-sm" disabled>Capture</button>
|
||||
<button type="button" id="retake_btn" class="btn btn-outline-info btn-sm" disabled>Retake</button>
|
||||
</div>
|
||||
<div id="camera_resolution" class="small text-muted mt-2">Camera: detecting resolution...</div>
|
||||
<div id="camera_status" class="small text-muted mt-2">Camera is idle.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
|
|
@ -57,14 +67,14 @@
|
|||
|
||||
<hr class="my-3">
|
||||
|
||||
<form method="post" enctype="multipart/form-data"
|
||||
<form method="post" enctype="multipart/form-data" id="profile_upload_form"
|
||||
action="<?= base_url('billing/patient/patient_file_upload') ?>/<?= esc($patient->id) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="doc_type" value="profile">
|
||||
<input type="hidden" name="update_profile" value="1">
|
||||
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="file" name="upload_file" class="form-control" accept="image/*">
|
||||
<input type="file" name="upload_file" id="profile_upload_file" class="form-control" accept="image/*">
|
||||
<button type="submit" class="btn btn-success">Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -92,31 +102,272 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="profileEditModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edit Capture</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex gap-2 flex-wrap mb-2">
|
||||
<button type="button" id="profile_rotate_left" class="btn btn-outline-secondary btn-sm">Rotate Left</button>
|
||||
<button type="button" id="profile_rotate_right" class="btn btn-outline-secondary btn-sm">Rotate Right</button>
|
||||
<button type="button" id="profile_crop_btn" class="btn btn-outline-primary btn-sm">Crop Selection</button>
|
||||
<button type="button" id="profile_reset_edit" class="btn btn-outline-warning btn-sm">Reset</button>
|
||||
</div>
|
||||
<div class="small text-muted mb-2">Drag on the image to select crop area. Rotate if needed, then save.</div>
|
||||
<div class="border rounded p-2 text-center bg-light">
|
||||
<canvas id="profile_edit_canvas" style="max-width:100%;cursor:crosshair;"></canvas>
|
||||
</div>
|
||||
<div id="profile_edit_status" class="small text-muted mt-2">No crop selected.</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" id="profile_save_edit" class="btn btn-primary btn-sm">Save and Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
var saveUrl = '<?= base_url('billing/patient/save_profile_image') ?>/<?= esc($patient->id) ?>';
|
||||
var csrfName = '<?= csrf_token() ?>';
|
||||
var csrfValue = $('input[name="<?= csrf_token() ?>"]').first().val() || '<?= csrf_hash() ?>';
|
||||
var allowImagePreuploadEdit = <?= !empty($allow_image_preupload_edit) ? 'true' : 'false' ?>;
|
||||
|
||||
var stream = null;
|
||||
var video = document.getElementById('camera');
|
||||
var canvas = document.getElementById('snapshot');
|
||||
var $cameraSelect = $('#profile_camera_select');
|
||||
var $cameraRefresh = $('#profile_camera_refresh');
|
||||
var $startBtn = $('#start_camera');
|
||||
var $stopBtn = $('#stop_camera');
|
||||
var $captureBtn = $('#capture_btn');
|
||||
var $cameraStatus = $('#camera_status');
|
||||
var $cameraResolution = $('#camera_resolution');
|
||||
var $retakeBtn = $('#retake_btn');
|
||||
var $clearHistory = $('#clear_history');
|
||||
var historyList = [];
|
||||
var currentIndex = -1;
|
||||
var modalEl = document.getElementById('capturePreviewModal');
|
||||
var modalInstance = modalEl ? new bootstrap.Modal(modalEl) : null;
|
||||
var editModalEl = document.getElementById('profileEditModal');
|
||||
var editModalInstance = editModalEl ? new bootstrap.Modal(editModalEl) : null;
|
||||
var editCanvas = document.getElementById('profile_edit_canvas');
|
||||
var editCtx = editCanvas ? editCanvas.getContext('2d') : null;
|
||||
var $editStatus = $('#profile_edit_status');
|
||||
var cameraStorageKey = 'profile_image_preferred_camera';
|
||||
var selectedDeviceId = '';
|
||||
var videoConstraints = {
|
||||
width: { ideal: 1920, max: 3840 },
|
||||
height: { ideal: 1080, max: 2160 },
|
||||
frameRate: { ideal: 30, max: 30 }
|
||||
};
|
||||
var editState = {
|
||||
originalDataUrl: '',
|
||||
currentDataUrl: '',
|
||||
sourceType: 'capture',
|
||||
sourceFilename: 'capture.jpg',
|
||||
image: null,
|
||||
imageRect: null,
|
||||
selection: null,
|
||||
dragging: false,
|
||||
dragStart: null
|
||||
};
|
||||
|
||||
try {
|
||||
selectedDeviceId = window.localStorage.getItem(cameraStorageKey) || '';
|
||||
} catch (e) {
|
||||
selectedDeviceId = '';
|
||||
}
|
||||
|
||||
function setStatus(type, message) {
|
||||
var klass = type === 'success' ? 'alert-success' : 'alert-danger';
|
||||
$('#upload_status').html('<div class="alert ' + klass + ' py-1 mb-0">' + message + '</div>');
|
||||
}
|
||||
|
||||
function dataUrlToBlob(dataUrl) {
|
||||
var parts = (dataUrl || '').split(',');
|
||||
if (parts.length < 2) {
|
||||
return null;
|
||||
}
|
||||
var mimeMatch = parts[0].match(/data:(.*?);base64/);
|
||||
var mime = mimeMatch ? mimeMatch[1] : 'image/jpeg';
|
||||
var binary = window.atob(parts[1]);
|
||||
var len = binary.length;
|
||||
var bytes = new window.Uint8Array(len);
|
||||
for (var i = 0; i < len; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return new window.Blob([bytes], { type: mime });
|
||||
}
|
||||
|
||||
function isEditableImageFile(file) {
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
var type = (file.type || '').toLowerCase();
|
||||
if (type.indexOf('image/') === 0) {
|
||||
return true;
|
||||
}
|
||||
var name = (file.name || '').toLowerCase();
|
||||
return /\.(jpg|jpeg|png|webp|gif)$/.test(name);
|
||||
}
|
||||
|
||||
function readFileAsDataUrl(file, onDone) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
var reader = new window.FileReader();
|
||||
reader.onload = function(evt) {
|
||||
if (typeof onDone === 'function') {
|
||||
onDone((evt && evt.target) ? (evt.target.result || '') : '');
|
||||
}
|
||||
};
|
||||
reader.onerror = function() {
|
||||
setStatus('error', 'Unable to read selected image.');
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function getCanvasPoint(evt) {
|
||||
var rect = editCanvas.getBoundingClientRect();
|
||||
return {
|
||||
x: (evt.clientX || 0) - rect.left,
|
||||
y: (evt.clientY || 0) - rect.top
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSelection(start, end, bounds) {
|
||||
var x1 = clamp(Math.min(start.x, end.x), bounds.x, bounds.x + bounds.width);
|
||||
var y1 = clamp(Math.min(start.y, end.y), bounds.y, bounds.y + bounds.height);
|
||||
var x2 = clamp(Math.max(start.x, end.x), bounds.x, bounds.x + bounds.width);
|
||||
var y2 = clamp(Math.max(start.y, end.y), bounds.y, bounds.y + bounds.height);
|
||||
return {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: Math.max(0, x2 - x1),
|
||||
height: Math.max(0, y2 - y1)
|
||||
};
|
||||
}
|
||||
|
||||
function drawEditorCanvas() {
|
||||
if (!editCanvas || !editCtx || !editState.image) {
|
||||
return;
|
||||
}
|
||||
|
||||
var maxWidth = Math.min(900, Math.max(320, window.innerWidth - 140));
|
||||
var maxHeight = Math.min(560, Math.max(240, window.innerHeight - 260));
|
||||
var imageWidth = editState.image.width || 1;
|
||||
var imageHeight = editState.image.height || 1;
|
||||
var scale = Math.min(maxWidth / imageWidth, maxHeight / imageHeight, 1);
|
||||
var drawWidth = Math.max(1, Math.round(imageWidth * scale));
|
||||
var drawHeight = Math.max(1, Math.round(imageHeight * scale));
|
||||
|
||||
editCanvas.width = drawWidth;
|
||||
editCanvas.height = drawHeight;
|
||||
editCtx.clearRect(0, 0, drawWidth, drawHeight);
|
||||
editCtx.drawImage(editState.image, 0, 0, drawWidth, drawHeight);
|
||||
editState.imageRect = { x: 0, y: 0, width: drawWidth, height: drawHeight };
|
||||
|
||||
if (editState.selection && editState.selection.width > 2 && editState.selection.height > 2) {
|
||||
editCtx.save();
|
||||
editCtx.strokeStyle = '#0d6efd';
|
||||
editCtx.lineWidth = 2;
|
||||
editCtx.setLineDash([6, 4]);
|
||||
editCtx.strokeRect(editState.selection.x, editState.selection.y, editState.selection.width, editState.selection.height);
|
||||
editCtx.fillStyle = 'rgba(13, 110, 253, 0.12)';
|
||||
editCtx.fillRect(editState.selection.x, editState.selection.y, editState.selection.width, editState.selection.height);
|
||||
editCtx.restore();
|
||||
$editStatus.text('Crop area: ' + Math.round(editState.selection.width) + ' x ' + Math.round(editState.selection.height));
|
||||
} else {
|
||||
$editStatus.text('No crop selected.');
|
||||
}
|
||||
}
|
||||
|
||||
function loadEditorImage(dataUrl) {
|
||||
if (!dataUrl) {
|
||||
return;
|
||||
}
|
||||
var img = new window.Image();
|
||||
img.onload = function() {
|
||||
editState.currentDataUrl = dataUrl;
|
||||
editState.image = img;
|
||||
editState.selection = null;
|
||||
drawEditorCanvas();
|
||||
};
|
||||
img.src = dataUrl;
|
||||
}
|
||||
|
||||
function rotateEditorImage(direction) {
|
||||
if (!editState.image) {
|
||||
return;
|
||||
}
|
||||
var source = document.createElement('canvas');
|
||||
var sourceCtx = source.getContext('2d');
|
||||
var srcWidth = editState.image.width;
|
||||
var srcHeight = editState.image.height;
|
||||
var clockwise = direction === 'right';
|
||||
|
||||
source.width = srcHeight;
|
||||
source.height = srcWidth;
|
||||
sourceCtx.save();
|
||||
if (clockwise) {
|
||||
sourceCtx.translate(source.width, 0);
|
||||
sourceCtx.rotate(Math.PI / 2);
|
||||
} else {
|
||||
sourceCtx.translate(0, source.height);
|
||||
sourceCtx.rotate(-Math.PI / 2);
|
||||
}
|
||||
sourceCtx.drawImage(editState.image, 0, 0);
|
||||
sourceCtx.restore();
|
||||
loadEditorImage(source.toDataURL('image/jpeg', 0.95));
|
||||
}
|
||||
|
||||
function cropEditorImage() {
|
||||
if (!editState.image || !editState.selection || !editState.imageRect) {
|
||||
$editStatus.text('Select a crop area first.');
|
||||
return;
|
||||
}
|
||||
var selection = editState.selection;
|
||||
if (selection.width < 8 || selection.height < 8) {
|
||||
$editStatus.text('Crop area is too small.');
|
||||
return;
|
||||
}
|
||||
|
||||
var scaleX = editState.image.width / editState.imageRect.width;
|
||||
var scaleY = editState.image.height / editState.imageRect.height;
|
||||
var srcX = Math.round(selection.x * scaleX);
|
||||
var srcY = Math.round(selection.y * scaleY);
|
||||
var srcW = Math.round(selection.width * scaleX);
|
||||
var srcH = Math.round(selection.height * scaleY);
|
||||
|
||||
var cropCanvas = document.createElement('canvas');
|
||||
var cropCtx = cropCanvas.getContext('2d');
|
||||
cropCanvas.width = Math.max(1, srcW);
|
||||
cropCanvas.height = Math.max(1, srcH);
|
||||
cropCtx.drawImage(editState.image, srcX, srcY, srcW, srcH, 0, 0, cropCanvas.width, cropCanvas.height);
|
||||
loadEditorImage(cropCanvas.toDataURL('image/jpeg', 0.95));
|
||||
}
|
||||
|
||||
function openCaptureEditor(dataUrl, sourceType, sourceFilename) {
|
||||
editState.originalDataUrl = dataUrl;
|
||||
editState.currentDataUrl = dataUrl;
|
||||
editState.sourceType = sourceType || 'capture';
|
||||
editState.sourceFilename = sourceFilename || (editState.sourceType === 'upload' ? 'upload.jpg' : 'capture.jpg');
|
||||
editState.selection = null;
|
||||
loadEditorImage(dataUrl);
|
||||
if (editModalInstance) {
|
||||
editModalInstance.show();
|
||||
}
|
||||
}
|
||||
|
||||
function setCameraState(active) {
|
||||
$startBtn.prop('disabled', active);
|
||||
$stopBtn.prop('disabled', !active);
|
||||
|
|
@ -125,9 +376,91 @@ $(function() {
|
|||
$cameraStatus.text(active ? 'Camera is active.' : 'Camera is idle.');
|
||||
}
|
||||
|
||||
function startCamera() {
|
||||
function saveSelectedDevice(deviceId) {
|
||||
selectedDeviceId = deviceId || '';
|
||||
try {
|
||||
if (selectedDeviceId) {
|
||||
window.localStorage.setItem(cameraStorageKey, selectedDeviceId);
|
||||
} else {
|
||||
window.localStorage.removeItem(cameraStorageKey);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
function getPreferredConstraints(deviceId) {
|
||||
var constraints = {
|
||||
width: videoConstraints.width,
|
||||
height: videoConstraints.height,
|
||||
frameRate: videoConstraints.frameRate
|
||||
};
|
||||
if (deviceId) {
|
||||
constraints.deviceId = { exact: deviceId };
|
||||
}
|
||||
return constraints;
|
||||
}
|
||||
|
||||
function refreshCameraList(preferredDeviceId) {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.mediaDevices.enumerateDevices().then(function(devices) {
|
||||
var videoDevices = [];
|
||||
devices.forEach(function(device) {
|
||||
if (device.kind === 'videoinput') {
|
||||
videoDevices.push(device);
|
||||
}
|
||||
});
|
||||
|
||||
var currentValue = preferredDeviceId || selectedDeviceId || $cameraSelect.val() || '';
|
||||
var optionsHtml = '<option value="">Default camera</option>';
|
||||
videoDevices.forEach(function(device, index) {
|
||||
var label = device.label || ('Camera ' + (index + 1));
|
||||
var value = device.deviceId || '';
|
||||
var selectedAttr = value && value === currentValue ? ' selected' : '';
|
||||
optionsHtml += '<option value="' + $('<div>').text(value).html() + '"' + selectedAttr + '>' + $('<div>').text(label).html() + '</option>';
|
||||
});
|
||||
$cameraSelect.html(optionsHtml);
|
||||
if (currentValue) {
|
||||
$cameraSelect.val(currentValue);
|
||||
if ($cameraSelect.val() !== currentValue) {
|
||||
$cameraSelect.val('');
|
||||
saveSelectedDevice('');
|
||||
}
|
||||
} else {
|
||||
$cameraSelect.val('');
|
||||
}
|
||||
}).catch(function() {
|
||||
});
|
||||
}
|
||||
|
||||
function updateResolutionLabel() {
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
if (stream && stream.getVideoTracks) {
|
||||
var track = stream.getVideoTracks()[0];
|
||||
if (track && track.getSettings) {
|
||||
var settings = track.getSettings();
|
||||
width = parseInt(settings.width || 0, 10) || 0;
|
||||
height = parseInt(settings.height || 0, 10) || 0;
|
||||
}
|
||||
}
|
||||
if ((!width || !height) && video) {
|
||||
width = parseInt(video.videoWidth || 0, 10) || width;
|
||||
height = parseInt(video.videoHeight || 0, 10) || height;
|
||||
}
|
||||
if (width > 0 && height > 0) {
|
||||
$cameraResolution.text('Camera: ' + width + ' x ' + height);
|
||||
} else {
|
||||
$cameraResolution.text('Camera: detecting resolution...');
|
||||
}
|
||||
}
|
||||
|
||||
function startCamera(deviceId) {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
$cameraStatus.text('Camera not supported in this browser.');
|
||||
$cameraResolution.text('Camera: not supported in this browser');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -135,17 +468,38 @@ $(function() {
|
|||
stopCamera();
|
||||
}
|
||||
|
||||
navigator.mediaDevices.getUserMedia({ video: true })
|
||||
var preferredDeviceId = typeof deviceId === 'string' ? deviceId : ($cameraSelect.val() || selectedDeviceId || '');
|
||||
|
||||
function attachStream(s, persistedDeviceId) {
|
||||
stream = s;
|
||||
video.srcObject = s;
|
||||
video.onloadedmetadata = function() {
|
||||
video.play().catch(function() {});
|
||||
updateResolutionLabel();
|
||||
};
|
||||
setCameraState(true);
|
||||
saveSelectedDevice(persistedDeviceId || '');
|
||||
refreshCameraList(persistedDeviceId || '');
|
||||
setTimeout(updateResolutionLabel, 300);
|
||||
setTimeout(updateResolutionLabel, 1000);
|
||||
}
|
||||
|
||||
navigator.mediaDevices.getUserMedia({ video: getPreferredConstraints(preferredDeviceId), audio: false })
|
||||
.then(function(s) {
|
||||
stream = s;
|
||||
video.srcObject = s;
|
||||
setCameraState(true);
|
||||
attachStream(s, preferredDeviceId);
|
||||
})
|
||||
.catch(function(err) {
|
||||
var msg = (err && err.name === 'NotAllowedError')
|
||||
? 'Camera access denied.'
|
||||
: 'Camera not available.';
|
||||
$cameraStatus.text(msg);
|
||||
.catch(function() {
|
||||
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
|
||||
.then(function(s) {
|
||||
attachStream(s, '');
|
||||
})
|
||||
.catch(function(err) {
|
||||
var msg = (err && err.name === 'NotAllowedError')
|
||||
? 'Camera access denied.'
|
||||
: 'Camera not available.';
|
||||
$cameraStatus.text(msg);
|
||||
$cameraResolution.text('Camera: unavailable');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -165,6 +519,7 @@ $(function() {
|
|||
video.srcObject = null;
|
||||
}
|
||||
setCameraState(false);
|
||||
$cameraResolution.text('Camera: stopped');
|
||||
}
|
||||
|
||||
window.pageCleanup = function() {
|
||||
|
|
@ -173,6 +528,15 @@ $(function() {
|
|||
|
||||
$startBtn.on('click', startCamera);
|
||||
$stopBtn.on('click', stopCamera);
|
||||
$cameraSelect.on('change', function() {
|
||||
var deviceId = $(this).val() || '';
|
||||
saveSelectedDevice(deviceId);
|
||||
startCamera(deviceId);
|
||||
});
|
||||
$cameraRefresh.on('click', function() {
|
||||
refreshCameraList($cameraSelect.val() || selectedDeviceId || '');
|
||||
startCamera($cameraSelect.val() || selectedDeviceId || '');
|
||||
});
|
||||
$retakeBtn.on('click', function() {
|
||||
$('#snapshot').hide();
|
||||
$('#results').html('Captured image will appear here.');
|
||||
|
|
@ -187,7 +551,8 @@ $(function() {
|
|||
});
|
||||
|
||||
setCameraState(false);
|
||||
startCamera();
|
||||
refreshCameraList(selectedDeviceId);
|
||||
startCamera(selectedDeviceId);
|
||||
|
||||
$('#capture_btn').on('click', function() {
|
||||
if (!stream || !video || !canvas) {
|
||||
|
|
@ -199,12 +564,35 @@ $(function() {
|
|||
|
||||
var width = video.videoWidth || 640;
|
||||
var height = video.videoHeight || 480;
|
||||
if (stream && stream.getVideoTracks) {
|
||||
var track = stream.getVideoTracks()[0];
|
||||
if (track && track.getSettings) {
|
||||
var settings = track.getSettings();
|
||||
width = parseInt(settings.width || width || 0, 10) || width;
|
||||
height = parseInt(settings.height || height || 0, 10) || height;
|
||||
}
|
||||
}
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(video, 0, 0, width, height);
|
||||
|
||||
var dataUri = canvas.toDataURL('image/jpeg', 0.9);
|
||||
var dataUri = canvas.toDataURL('image/jpeg', 0.95);
|
||||
if (!allowImagePreuploadEdit) {
|
||||
persistProfileImageDataUri(dataUri);
|
||||
return;
|
||||
}
|
||||
openCaptureEditor(dataUri, 'capture', 'capture.jpg');
|
||||
});
|
||||
|
||||
function persistProfileImageDataUri(dataUri) {
|
||||
if (!dataUri) {
|
||||
setStatus('error', 'Capture not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
$('#results').html('');
|
||||
$('#snapshot').show();
|
||||
$('#upload_status').html('');
|
||||
|
|
@ -251,8 +639,90 @@ $(function() {
|
|||
$history.children('img').each(function(i) {
|
||||
$(this).attr('data-index', i);
|
||||
});
|
||||
|
||||
if (editModalInstance) {
|
||||
editModalInstance.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function saveEditedProfileCapture() {
|
||||
persistProfileImageDataUri(editState.currentDataUrl);
|
||||
}
|
||||
|
||||
$('#profile_upload_form').on('submit', function(e) {
|
||||
if (!allowImagePreuploadEdit) {
|
||||
return;
|
||||
}
|
||||
var fileInput = document.getElementById('profile_upload_file');
|
||||
var file = fileInput && fileInput.files ? fileInput.files[0] : null;
|
||||
if (!file || !isEditableImageFile(file)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
setStatus('success', 'Opening image editor...');
|
||||
readFileAsDataUrl(file, function(dataUrl) {
|
||||
if (!dataUrl) {
|
||||
setStatus('error', 'Unable to read selected image.');
|
||||
return;
|
||||
}
|
||||
openCaptureEditor(dataUrl, 'upload', file.name || 'upload.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
$('#profile_rotate_left').on('click', function() {
|
||||
rotateEditorImage('left');
|
||||
});
|
||||
|
||||
$('#profile_rotate_right').on('click', function() {
|
||||
rotateEditorImage('right');
|
||||
});
|
||||
|
||||
$('#profile_crop_btn').on('click', function() {
|
||||
cropEditorImage();
|
||||
});
|
||||
|
||||
$('#profile_reset_edit').on('click', function() {
|
||||
loadEditorImage(editState.originalDataUrl);
|
||||
});
|
||||
|
||||
$('#profile_save_edit').on('click', function() {
|
||||
saveEditedProfileCapture();
|
||||
});
|
||||
|
||||
if (editCanvas) {
|
||||
editCanvas.addEventListener('mousedown', function(evt) {
|
||||
if (!editState.imageRect) {
|
||||
return;
|
||||
}
|
||||
editState.dragging = true;
|
||||
editState.dragStart = getCanvasPoint(evt);
|
||||
editState.selection = normalizeSelection(editState.dragStart, editState.dragStart, editState.imageRect);
|
||||
drawEditorCanvas();
|
||||
});
|
||||
editCanvas.addEventListener('mousemove', function(evt) {
|
||||
if (!editState.dragging || !editState.dragStart || !editState.imageRect) {
|
||||
return;
|
||||
}
|
||||
editState.selection = normalizeSelection(editState.dragStart, getCanvasPoint(evt), editState.imageRect);
|
||||
drawEditorCanvas();
|
||||
});
|
||||
editCanvas.addEventListener('mouseup', function(evt) {
|
||||
if (!editState.dragging || !editState.dragStart || !editState.imageRect) {
|
||||
return;
|
||||
}
|
||||
editState.selection = normalizeSelection(editState.dragStart, getCanvasPoint(evt), editState.imageRect);
|
||||
editState.dragging = false;
|
||||
editState.dragStart = null;
|
||||
drawEditorCanvas();
|
||||
});
|
||||
editCanvas.addEventListener('mouseleave', function() {
|
||||
if (editState.dragging) {
|
||||
editState.dragging = false;
|
||||
editState.dragStart = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('#capture_history').on('click', 'img', function() {
|
||||
var idx = parseInt($(this).attr('data-index'), 10);
|
||||
if (isNaN(idx)) {
|
||||
|
|
@ -305,6 +775,15 @@ $(function() {
|
|||
stopCamera();
|
||||
});
|
||||
|
||||
if (editModalEl) {
|
||||
editModalEl.addEventListener('hidden.bs.modal', function() {
|
||||
editState.selection = null;
|
||||
editState.dragging = false;
|
||||
editState.dragStart = null;
|
||||
$editStatus.text('No crop selected.');
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (document.hidden) {
|
||||
stopCamera();
|
||||
|
|
|
|||
|
|
@ -605,6 +605,15 @@
|
|||
<!-- Panel: Method 2 - Scan QR -->
|
||||
<div id="abhareg_panel_qr" class="abhareg-panel d-none">
|
||||
<p class="text-muted small mb-2">Point camera at patient's ABHA QR code (from PHR app or printed card).</p>
|
||||
<div class="mb-2" style="max-width:300px;">
|
||||
<label for="abhareg_qr_camera_select" class="form-label small mb-1">Camera</label>
|
||||
<div class="d-flex gap-2 align-items-start">
|
||||
<select id="abhareg_qr_camera_select" class="form-select form-select-sm">
|
||||
<option value="">Default camera</option>
|
||||
</select>
|
||||
<button type="button" id="abhareg_qr_camera_refresh" class="btn btn-outline-secondary btn-sm">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="abhareg_qr_reader" style="width:300px;max-width:100%;"></div>
|
||||
<div id="abhareg_qr_result" class="mt-2"></div>
|
||||
<div class="mt-2">
|
||||
|
|
@ -1032,8 +1041,18 @@
|
|||
(function() {
|
||||
var regTxnId = null;
|
||||
var html5QrCode = null;
|
||||
var qrScriptLoaded = false;
|
||||
var qrScriptLoading = false;
|
||||
var qrCameraStorageKey = 'abha_qr_preferred_camera';
|
||||
var qrSelectedCamera = '';
|
||||
var csrf = function() { return $('input[name="<?= csrf_token() ?>"]').first().val(); };
|
||||
|
||||
try {
|
||||
qrSelectedCamera = window.localStorage.getItem(qrCameraStorageKey) || '';
|
||||
} catch (e) {
|
||||
qrSelectedCamera = '';
|
||||
}
|
||||
|
||||
var methodMeta = {
|
||||
number: { icon: '🆔', title: 'By ABHA Number/Address', sub: 'Validate then OTP verify' },
|
||||
qr: { icon: '📷', title: 'Scan Patient ABHA QR', sub: 'Camera → ABHA extracted → OTP verify' },
|
||||
|
|
@ -1079,6 +1098,128 @@
|
|||
$('#' + id).html(msg ? '<div class="alert alert-' + type + ' py-2">' + msg + '</div>' : '');
|
||||
}
|
||||
|
||||
function saveQrSelectedCamera(cameraId) {
|
||||
qrSelectedCamera = cameraId || '';
|
||||
try {
|
||||
if (qrSelectedCamera) {
|
||||
window.localStorage.setItem(qrCameraStorageKey, qrSelectedCamera);
|
||||
} else {
|
||||
window.localStorage.removeItem(qrCameraStorageKey);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
function populateQrCameraSelect(cameras, preferredCameraId) {
|
||||
var $select = $('#abhareg_qr_camera_select');
|
||||
if (!$select.length) {
|
||||
return;
|
||||
}
|
||||
var currentValue = preferredCameraId || qrSelectedCamera || $select.val() || '';
|
||||
var optionsHtml = '<option value="">Default camera</option>';
|
||||
(cameras || []).forEach(function(camera, index) {
|
||||
var label = camera.label || ('Camera ' + (index + 1));
|
||||
var value = camera.id || camera.deviceId || '';
|
||||
var selectedAttr = value && value === currentValue ? ' selected' : '';
|
||||
optionsHtml += '<option value="' + $('<div>').text(value).html() + '"' + selectedAttr + '>' + $('<div>').text(label).html() + '</option>';
|
||||
});
|
||||
$select.html(optionsHtml);
|
||||
if (currentValue) {
|
||||
$select.val(currentValue);
|
||||
if ($select.val() !== currentValue) {
|
||||
$select.val('');
|
||||
saveQrSelectedCamera('');
|
||||
}
|
||||
} else {
|
||||
$select.val('');
|
||||
}
|
||||
}
|
||||
|
||||
function loadQrScannerScript(onReady) {
|
||||
if (qrScriptLoaded && typeof window.Html5Qrcode !== 'undefined') {
|
||||
onReady();
|
||||
return;
|
||||
}
|
||||
if (qrScriptLoading) {
|
||||
var waitReady = function() {
|
||||
if (qrScriptLoaded && typeof window.Html5Qrcode !== 'undefined') {
|
||||
onReady();
|
||||
} else {
|
||||
window.setTimeout(waitReady, 100);
|
||||
}
|
||||
};
|
||||
waitReady();
|
||||
return;
|
||||
}
|
||||
|
||||
qrScriptLoading = true;
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js';
|
||||
script.onload = function() {
|
||||
qrScriptLoading = false;
|
||||
qrScriptLoaded = true;
|
||||
onReady();
|
||||
};
|
||||
script.onerror = function() {
|
||||
qrScriptLoading = false;
|
||||
$('#abhareg_qr_result').html('<div class="alert alert-warning py-2">Unable to load QR scanner library.</div>');
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function startQrScanner(cameraId) {
|
||||
if (typeof window.Html5Qrcode === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedCameraId = typeof cameraId === 'string' ? cameraId : ($('#abhareg_qr_camera_select').val() || qrSelectedCamera || '');
|
||||
|
||||
if (!html5QrCode) {
|
||||
html5QrCode = new window.Html5Qrcode('abhareg_qr_reader');
|
||||
}
|
||||
|
||||
$('#abhareg_qr_stop_btn').removeClass('d-none');
|
||||
$('#abhareg_qr_result').html('');
|
||||
|
||||
window.Html5Qrcode.getCameras().then(function(cameras) {
|
||||
populateQrCameraSelect(cameras, selectedCameraId);
|
||||
|
||||
var cameraConfig = selectedCameraId
|
||||
? { deviceId: { exact: selectedCameraId } }
|
||||
: { facingMode: 'environment' };
|
||||
|
||||
html5QrCode.start(
|
||||
cameraConfig,
|
||||
{ fps: 10, qrbox: { width: 250, height: 250 } },
|
||||
function(decodedText) {
|
||||
stopQrScanner();
|
||||
var abha = decodedText.trim();
|
||||
var numMatch = abha.match(/\d{14}/);
|
||||
if (numMatch) { abha = numMatch[0]; }
|
||||
$('#abhareg_qr_result').html('<div class="alert alert-success py-2"><i class="bi bi-qr-code me-2"></i>QR scanned: <strong>' + abha + '</strong></div>');
|
||||
abhaRegSelectMethod('number');
|
||||
$('#abhareg_abha_input').val(abha);
|
||||
$('#abhareg_validate_btn').trigger('click');
|
||||
},
|
||||
function() {}
|
||||
).then(function() {
|
||||
saveQrSelectedCamera(selectedCameraId);
|
||||
}).catch(function(err) {
|
||||
if (selectedCameraId) {
|
||||
saveQrSelectedCamera('');
|
||||
$('#abhareg_qr_camera_select').val('');
|
||||
stopQrScanner().then(function() {
|
||||
initQrScanner('');
|
||||
});
|
||||
return;
|
||||
}
|
||||
$('#abhareg_qr_result').html('<div class="alert alert-warning py-2">Camera error: ' + err + '. Please allow camera access.</div>');
|
||||
});
|
||||
}).catch(function(err) {
|
||||
$('#abhareg_qr_result').html('<div class="alert alert-warning py-2">Unable to list cameras: ' + err + '</div>');
|
||||
});
|
||||
}
|
||||
|
||||
function showRegResult(resp) {
|
||||
var abhaNum = resp.abha_number || '';
|
||||
var abhaDisp = abhaNum.replace(/(\d{2})(\d{4})(\d{4})(\d{4})/, '$1-$2-$3-$4');
|
||||
|
|
@ -1245,42 +1386,36 @@
|
|||
});
|
||||
|
||||
// --- Method 2: QR Scanner ---
|
||||
function initQrScanner() {
|
||||
if (html5QrCode) { return; }
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js';
|
||||
script.onload = function() {
|
||||
html5QrCode = new Html5Qrcode('abhareg_qr_reader');
|
||||
$('#abhareg_qr_stop_btn').removeClass('d-none');
|
||||
html5QrCode.start(
|
||||
{ facingMode: 'environment' },
|
||||
{ fps: 10, qrbox: { width: 250, height: 250 } },
|
||||
function(decodedText) {
|
||||
stopQrScanner();
|
||||
var abha = decodedText.trim();
|
||||
var numMatch = abha.match(/\d{14}/);
|
||||
if (numMatch) { abha = numMatch[0]; }
|
||||
$('#abhareg_qr_result').html('<div class="alert alert-success py-2"><i class="bi bi-qr-code me-2"></i>QR scanned: <strong>' + abha + '</strong></div>');
|
||||
abhaRegSelectMethod('number');
|
||||
$('#abhareg_abha_input').val(abha);
|
||||
$('#abhareg_validate_btn').trigger('click');
|
||||
},
|
||||
function() {}
|
||||
).catch(function(err) {
|
||||
$('#abhareg_qr_result').html('<div class="alert alert-warning py-2">Camera error: ' + err + '. Please allow camera access.</div>');
|
||||
});
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
function initQrScanner(cameraId) {
|
||||
loadQrScannerScript(function() {
|
||||
startQrScanner(cameraId);
|
||||
});
|
||||
}
|
||||
|
||||
function stopQrScanner() {
|
||||
if (html5QrCode) {
|
||||
html5QrCode.stop().catch(function(){}).then(function() { html5QrCode = null; });
|
||||
$('#abhareg_qr_stop_btn').addClass('d-none');
|
||||
$('#abhareg_qr_stop_btn').addClass('d-none');
|
||||
if (!html5QrCode) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return html5QrCode.stop().catch(function(){}).then(function() {
|
||||
html5QrCode = null;
|
||||
});
|
||||
}
|
||||
|
||||
$('#abhareg_qr_stop_btn').on('click', function() { stopQrScanner(); });
|
||||
$('#abhareg_qr_camera_select').on('change', function() {
|
||||
var cameraId = ($(this).val() || '').toString();
|
||||
saveQrSelectedCamera(cameraId);
|
||||
stopQrScanner().then(function() {
|
||||
initQrScanner(cameraId);
|
||||
});
|
||||
});
|
||||
$('#abhareg_qr_camera_refresh').on('click', function() {
|
||||
saveQrSelectedCamera(($('#abhareg_qr_camera_select').val() || '').toString());
|
||||
stopQrScanner().then(function() {
|
||||
initQrScanner(qrSelectedCamera || '');
|
||||
});
|
||||
});
|
||||
|
||||
// Enter key on OTP fields
|
||||
$('#abhareg_num_otp_input').on('keypress', function(e) {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,17 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<video id="opd_scan_video" autoplay playsinline style="width:100%;max-width:220px;border:1px solid #ced4da;border-radius:4px;"></video>
|
||||
<video id="opd_scan_video" autoplay muted playsinline style="width:100%;max-width:220px;border:1px solid #ced4da;border-radius:4px;"></video>
|
||||
<div class="mt-2">
|
||||
<label for="opd_scan_camera_select" class="form-label mb-1 small">Camera</label>
|
||||
<div class="d-flex gap-2 align-items-start">
|
||||
<select id="opd_scan_camera_select" class="form-select form-select-sm">
|
||||
<option value="">Default camera</option>
|
||||
</select>
|
||||
<button type="button" id="opd_scan_camera_refresh" class="btn btn-outline-secondary btn-sm">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="opd_scan_resolution" class="small text-muted mt-1">Camera: detecting resolution...</div>
|
||||
<canvas id="opd_scan_canvas" style="display:none;"></canvas>
|
||||
<input type="hidden" id="opd_scan_opdid" value="<?= esc((int) ($opdid ?? 0)) ?>">
|
||||
<div class="mt-2 d-flex gap-2">
|
||||
|
|
@ -43,6 +53,34 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="opd_scan_edit_modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Edit Capture</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex gap-2 flex-wrap mb-2">
|
||||
<button type="button" id="opd_scan_rotate_left" class="btn btn-outline-secondary btn-sm">Rotate Left</button>
|
||||
<button type="button" id="opd_scan_rotate_right" class="btn btn-outline-secondary btn-sm">Rotate Right</button>
|
||||
<button type="button" id="opd_scan_crop_btn" class="btn btn-outline-primary btn-sm">Crop Selection</button>
|
||||
<button type="button" id="opd_scan_reset_edit" class="btn btn-outline-warning btn-sm">Reset</button>
|
||||
</div>
|
||||
<div class="small text-muted mb-2">Drag on the image to select crop area. Rotate if needed, then save.</div>
|
||||
<div class="border rounded p-2 text-center bg-light">
|
||||
<canvas id="opd_scan_edit_canvas" style="max-width:100%;cursor:crosshair;"></canvas>
|
||||
</div>
|
||||
<div id="opd_scan_edit_status" class="small text-muted mt-2">No crop selected.</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" id="opd_scan_save_edit" class="btn btn-primary btn-sm">Save and Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
// Keep references to camera stream and frequently used DOM elements.
|
||||
|
|
@ -56,8 +94,41 @@
|
|||
var $results = $('#opd_scan_results');
|
||||
var $textBox = $('#opd_scan_text_box');
|
||||
var $aiResult = $('#opd_scan_ai_result');
|
||||
var $cameraSelect = $('#opd_scan_camera_select');
|
||||
var $cameraRefresh = $('#opd_scan_camera_refresh');
|
||||
var $resolution = $('#opd_scan_resolution');
|
||||
var allowImagePreuploadEdit = <?= !empty($allow_image_preupload_edit) ? 'true' : 'false' ?>;
|
||||
var editModalEl = document.getElementById('opd_scan_edit_modal');
|
||||
var editModal = editModalEl && window.bootstrap ? new bootstrap.Modal(editModalEl) : null;
|
||||
var editCanvas = document.getElementById('opd_scan_edit_canvas');
|
||||
var editCtx = editCanvas ? editCanvas.getContext('2d') : null;
|
||||
var $editStatus = $('#opd_scan_edit_status');
|
||||
var opdid = parseInt($('#opd_scan_opdid').val() || '0', 10);
|
||||
var latestFileId = 0;
|
||||
var cameraStorageKey = 'opd_scan_preferred_camera';
|
||||
var selectedDeviceId = '';
|
||||
var editState = {
|
||||
originalDataUrl: '',
|
||||
currentDataUrl: '',
|
||||
sourceType: 'capture',
|
||||
sourceFilename: 'capture.jpg',
|
||||
image: null,
|
||||
imageRect: null,
|
||||
selection: null,
|
||||
dragging: false,
|
||||
dragStart: null
|
||||
};
|
||||
var videoConstraints = {
|
||||
width: { ideal: 1920, max: 3840 },
|
||||
height: { ideal: 1080, max: 2160 },
|
||||
frameRate: { ideal: 30, max: 30 }
|
||||
};
|
||||
|
||||
try {
|
||||
selectedDeviceId = window.localStorage.getItem(cameraStorageKey) || '';
|
||||
} catch (e) {
|
||||
selectedDeviceId = '';
|
||||
}
|
||||
|
||||
function getCsrfPair() {
|
||||
var input = document.querySelector('input[name="<?= csrf_token() ?>"]');
|
||||
|
|
@ -77,31 +148,345 @@
|
|||
}
|
||||
}
|
||||
|
||||
function startCamera() {
|
||||
// Start webcam stream for quick bedside capture.
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
$results.html('<div class="text-danger">Camera not supported in this browser.</div>');
|
||||
function dataUrlToBlob(dataUrl) {
|
||||
var parts = (dataUrl || '').split(',');
|
||||
if (parts.length < 2) {
|
||||
return null;
|
||||
}
|
||||
var mimeMatch = parts[0].match(/data:(.*?);base64/);
|
||||
var mime = mimeMatch ? mimeMatch[1] : 'image/jpeg';
|
||||
var binary = window.atob(parts[1]);
|
||||
var len = binary.length;
|
||||
var bytes = new window.Uint8Array(len);
|
||||
for (var i = 0; i < len; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return new window.Blob([bytes], { type: mime });
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
function getCanvasPoint(evt) {
|
||||
var rect = editCanvas.getBoundingClientRect();
|
||||
return {
|
||||
x: (evt.clientX || 0) - rect.left,
|
||||
y: (evt.clientY || 0) - rect.top
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSelection(start, end, bounds) {
|
||||
var x1 = clamp(Math.min(start.x, end.x), bounds.x, bounds.x + bounds.width);
|
||||
var y1 = clamp(Math.min(start.y, end.y), bounds.y, bounds.y + bounds.height);
|
||||
var x2 = clamp(Math.max(start.x, end.x), bounds.x, bounds.x + bounds.width);
|
||||
var y2 = clamp(Math.max(start.y, end.y), bounds.y, bounds.y + bounds.height);
|
||||
return {
|
||||
x: x1,
|
||||
y: y1,
|
||||
width: Math.max(0, x2 - x1),
|
||||
height: Math.max(0, y2 - y1)
|
||||
};
|
||||
}
|
||||
|
||||
function drawEditorCanvas() {
|
||||
if (!editCanvas || !editCtx || !editState.image) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
|
||||
var maxWidth = Math.min(900, Math.max(320, window.innerWidth - 140));
|
||||
var maxHeight = Math.min(560, Math.max(240, window.innerHeight - 260));
|
||||
var imageWidth = editState.image.width || 1;
|
||||
var imageHeight = editState.image.height || 1;
|
||||
var scale = Math.min(maxWidth / imageWidth, maxHeight / imageHeight, 1);
|
||||
var drawWidth = Math.max(1, Math.round(imageWidth * scale));
|
||||
var drawHeight = Math.max(1, Math.round(imageHeight * scale));
|
||||
|
||||
editCanvas.width = drawWidth;
|
||||
editCanvas.height = drawHeight;
|
||||
editCtx.clearRect(0, 0, drawWidth, drawHeight);
|
||||
editCtx.drawImage(editState.image, 0, 0, drawWidth, drawHeight);
|
||||
editState.imageRect = { x: 0, y: 0, width: drawWidth, height: drawHeight };
|
||||
|
||||
if (editState.selection && editState.selection.width > 2 && editState.selection.height > 2) {
|
||||
editCtx.save();
|
||||
editCtx.strokeStyle = '#0d6efd';
|
||||
editCtx.lineWidth = 2;
|
||||
editCtx.setLineDash([6, 4]);
|
||||
editCtx.strokeRect(editState.selection.x, editState.selection.y, editState.selection.width, editState.selection.height);
|
||||
editCtx.fillStyle = 'rgba(13, 110, 253, 0.12)';
|
||||
editCtx.fillRect(editState.selection.x, editState.selection.y, editState.selection.width, editState.selection.height);
|
||||
editCtx.restore();
|
||||
$editStatus.text('Crop area: ' + Math.round(editState.selection.width) + ' x ' + Math.round(editState.selection.height));
|
||||
} else {
|
||||
$editStatus.text('No crop selected.');
|
||||
}
|
||||
}
|
||||
|
||||
function loadEditorImage(dataUrl, preserveSelection) {
|
||||
if (!dataUrl) {
|
||||
return;
|
||||
}
|
||||
var img = new window.Image();
|
||||
img.onload = function() {
|
||||
editState.currentDataUrl = dataUrl;
|
||||
editState.image = img;
|
||||
if (!preserveSelection) {
|
||||
editState.selection = null;
|
||||
}
|
||||
drawEditorCanvas();
|
||||
};
|
||||
img.src = dataUrl;
|
||||
}
|
||||
|
||||
function rotateEditorImage(direction) {
|
||||
if (!editState.image) {
|
||||
return;
|
||||
}
|
||||
var source = document.createElement('canvas');
|
||||
var sourceCtx = source.getContext('2d');
|
||||
var srcWidth = editState.image.width;
|
||||
var srcHeight = editState.image.height;
|
||||
var clockwise = direction === 'right';
|
||||
|
||||
source.width = srcHeight;
|
||||
source.height = srcWidth;
|
||||
sourceCtx.save();
|
||||
if (clockwise) {
|
||||
sourceCtx.translate(source.width, 0);
|
||||
sourceCtx.rotate(Math.PI / 2);
|
||||
} else {
|
||||
sourceCtx.translate(0, source.height);
|
||||
sourceCtx.rotate(-Math.PI / 2);
|
||||
}
|
||||
sourceCtx.drawImage(editState.image, 0, 0);
|
||||
sourceCtx.restore();
|
||||
loadEditorImage(source.toDataURL('image/jpeg', 0.95));
|
||||
}
|
||||
|
||||
function cropEditorImage() {
|
||||
if (!editState.image || !editState.selection || !editState.imageRect) {
|
||||
$editStatus.text('Select a crop area first.');
|
||||
return;
|
||||
}
|
||||
var selection = editState.selection;
|
||||
if (selection.width < 8 || selection.height < 8) {
|
||||
$editStatus.text('Crop area is too small.');
|
||||
return;
|
||||
}
|
||||
|
||||
var scaleX = editState.image.width / editState.imageRect.width;
|
||||
var scaleY = editState.image.height / editState.imageRect.height;
|
||||
var srcX = Math.round(selection.x * scaleX);
|
||||
var srcY = Math.round(selection.y * scaleY);
|
||||
var srcW = Math.round(selection.width * scaleX);
|
||||
var srcH = Math.round(selection.height * scaleY);
|
||||
|
||||
var cropCanvas = document.createElement('canvas');
|
||||
var cropCtx = cropCanvas.getContext('2d');
|
||||
cropCanvas.width = Math.max(1, srcW);
|
||||
cropCanvas.height = Math.max(1, srcH);
|
||||
cropCtx.drawImage(editState.image, srcX, srcY, srcW, srcH, 0, 0, cropCanvas.width, cropCanvas.height);
|
||||
loadEditorImage(cropCanvas.toDataURL('image/jpeg', 0.95));
|
||||
}
|
||||
|
||||
function openCaptureEditor(dataUrl, sourceType, sourceFilename) {
|
||||
editState.originalDataUrl = dataUrl;
|
||||
editState.currentDataUrl = dataUrl;
|
||||
editState.sourceType = sourceType || 'capture';
|
||||
editState.sourceFilename = sourceFilename || (editState.sourceType === 'upload' ? 'upload.jpg' : 'capture.jpg');
|
||||
editState.selection = null;
|
||||
loadEditorImage(dataUrl);
|
||||
if (editModal) {
|
||||
editModal.show();
|
||||
}
|
||||
}
|
||||
|
||||
function saveEditedCapture() {
|
||||
var blob = dataUrlToBlob(editState.currentDataUrl);
|
||||
if (!blob) {
|
||||
$editStatus.text('Unable to prepare edited image.');
|
||||
return;
|
||||
}
|
||||
if (editModal) {
|
||||
editModal.hide();
|
||||
}
|
||||
var formData = new window.FormData();
|
||||
var csrf = getCsrfPair();
|
||||
if (editState.sourceType === 'upload') {
|
||||
formData.append('userfile', blob, editState.sourceFilename || 'upload.jpg');
|
||||
} else {
|
||||
formData.append('webcam', blob, editState.sourceFilename || 'capture.jpg');
|
||||
}
|
||||
formData.append(csrf.name, csrf.value);
|
||||
submitScanFormData(formData);
|
||||
}
|
||||
|
||||
function isEditableImageFile(file) {
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
var type = (file.type || '').toLowerCase();
|
||||
if (type.indexOf('image/') === 0) {
|
||||
return true;
|
||||
}
|
||||
var name = (file.name || '').toLowerCase();
|
||||
return /\.(jpg|jpeg|png|webp|gif)$/.test(name);
|
||||
}
|
||||
|
||||
function readFileAsDataUrl(file, onDone) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
var reader = new window.FileReader();
|
||||
reader.onload = function(evt) {
|
||||
if (typeof onDone === 'function') {
|
||||
onDone((evt && evt.target) ? (evt.target.result || '') : '');
|
||||
}
|
||||
};
|
||||
reader.onerror = function() {
|
||||
$results.html('<div class="text-danger">Unable to read selected image.</div>');
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function saveSelectedDevice(deviceId) {
|
||||
selectedDeviceId = deviceId || '';
|
||||
try {
|
||||
if (selectedDeviceId) {
|
||||
window.localStorage.setItem(cameraStorageKey, selectedDeviceId);
|
||||
} else {
|
||||
window.localStorage.removeItem(cameraStorageKey);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
function getPreferredConstraints(deviceId) {
|
||||
var constraints = {
|
||||
width: videoConstraints.width,
|
||||
height: videoConstraints.height,
|
||||
frameRate: videoConstraints.frameRate
|
||||
};
|
||||
if (deviceId) {
|
||||
constraints.deviceId = { exact: deviceId };
|
||||
}
|
||||
return constraints;
|
||||
}
|
||||
|
||||
function refreshCameraList(preferredDeviceId) {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
|
||||
return $.Deferred ? $.Deferred().resolve().promise() : null;
|
||||
}
|
||||
|
||||
return navigator.mediaDevices.enumerateDevices().then(function(devices) {
|
||||
var videoDevices = [];
|
||||
devices.forEach(function(device) {
|
||||
if (device.kind === 'videoinput') {
|
||||
videoDevices.push(device);
|
||||
}
|
||||
});
|
||||
|
||||
var currentValue = preferredDeviceId || selectedDeviceId || $cameraSelect.val() || '';
|
||||
var optionsHtml = '<option value="">Default camera</option>';
|
||||
videoDevices.forEach(function(device, index) {
|
||||
var label = device.label || ('Camera ' + (index + 1));
|
||||
var value = device.deviceId || '';
|
||||
var selectedAttr = value && value === currentValue ? ' selected' : '';
|
||||
optionsHtml += '<option value="' + $('<div>').text(value).html() + '"' + selectedAttr + '>' + $('<div>').text(label).html() + '</option>';
|
||||
});
|
||||
$cameraSelect.html(optionsHtml);
|
||||
|
||||
if (currentValue && $cameraSelect.find('option[value="' + currentValue.replace(/"/g, '\\"') + '"]').length) {
|
||||
$cameraSelect.val(currentValue);
|
||||
} else {
|
||||
$cameraSelect.val('');
|
||||
if (preferredDeviceId || selectedDeviceId) {
|
||||
saveSelectedDevice('');
|
||||
}
|
||||
}
|
||||
}).catch(function() {
|
||||
});
|
||||
}
|
||||
|
||||
function startCamera(deviceId) {
|
||||
// Start webcam stream for quick bedside capture.
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
$results.html('<div class="text-danger">Camera not supported in this browser.</div>');
|
||||
$resolution.text('Camera: not supported in this browser');
|
||||
return;
|
||||
}
|
||||
|
||||
stopCamera();
|
||||
|
||||
function attachStream(stream) {
|
||||
streamRef = stream;
|
||||
var video = $video.get(0);
|
||||
video.srcObject = stream;
|
||||
video.onloadedmetadata = function() {
|
||||
video.play().catch(function() {});
|
||||
updateResolutionLabel();
|
||||
};
|
||||
setTimeout(updateResolutionLabel, 300);
|
||||
setTimeout(updateResolutionLabel, 1000);
|
||||
}
|
||||
|
||||
var preferredDeviceId = typeof deviceId === 'string' ? deviceId : ($cameraSelect.val() || selectedDeviceId || '');
|
||||
|
||||
navigator.mediaDevices.getUserMedia({ video: getPreferredConstraints(preferredDeviceId), audio: false })
|
||||
.then(function(stream) {
|
||||
streamRef = stream;
|
||||
var video = $video.get(0);
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
saveSelectedDevice(preferredDeviceId);
|
||||
attachStream(stream);
|
||||
refreshCameraList(preferredDeviceId);
|
||||
})
|
||||
.catch(function() {
|
||||
$results.html('<div class="text-danger">Unable to access webcam. Please allow camera permission.</div>');
|
||||
// Fallback to default camera settings if HD constraints fail.
|
||||
navigator.mediaDevices.getUserMedia({ video: true, audio: false })
|
||||
.then(function(stream) {
|
||||
saveSelectedDevice('');
|
||||
attachStream(stream);
|
||||
refreshCameraList('');
|
||||
})
|
||||
.catch(function() {
|
||||
$results.html('<div class="text-danger">Unable to access webcam. Please allow camera permission.</div>');
|
||||
$resolution.text('Camera: permission denied or unavailable');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateResolutionLabel() {
|
||||
var video = $video.get(0);
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
|
||||
if (streamRef && streamRef.getVideoTracks) {
|
||||
var track = streamRef.getVideoTracks()[0];
|
||||
if (track && track.getSettings) {
|
||||
var settings = track.getSettings();
|
||||
width = parseInt(settings.width || 0, 10) || 0;
|
||||
height = parseInt(settings.height || 0, 10) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
if ((!width || !height) && video) {
|
||||
width = parseInt(video.videoWidth || 0, 10) || width;
|
||||
height = parseInt(video.videoHeight || 0, 10) || height;
|
||||
}
|
||||
|
||||
if (width > 0 && height > 0) {
|
||||
$resolution.text('Camera: ' + width + ' x ' + height);
|
||||
} else {
|
||||
$resolution.text('Camera: detecting resolution...');
|
||||
}
|
||||
}
|
||||
|
||||
function stopCamera() {
|
||||
// Stop all active tracks to release camera hardware.
|
||||
if (streamRef) {
|
||||
streamRef.getTracks().forEach(function(track) { track.stop(); });
|
||||
streamRef = null;
|
||||
}
|
||||
$resolution.text('Camera: stopped');
|
||||
}
|
||||
|
||||
function loadLastList() {
|
||||
|
|
@ -213,7 +598,7 @@
|
|||
}
|
||||
|
||||
function captureAndUpload() {
|
||||
// Capture current webcam frame and send it as multipart form data.
|
||||
// Capture current webcam frame, allow edit, then upload it.
|
||||
if (opdid <= 0) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -228,24 +613,39 @@
|
|||
$textBox.html('<div class="text-muted small">Capturing and uploading file...</div>');
|
||||
|
||||
var canvas = $canvas.get(0);
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
canvas.toBlob(function(blob) {
|
||||
if (!blob) {
|
||||
$captureBtn.prop('disabled', false);
|
||||
$results.html('<div class="text-danger">Capture failed.</div>');
|
||||
return;
|
||||
var width = video.videoWidth;
|
||||
var height = video.videoHeight;
|
||||
if (streamRef && streamRef.getVideoTracks) {
|
||||
var track = streamRef.getVideoTracks()[0];
|
||||
if (track && track.getSettings) {
|
||||
var settings = track.getSettings();
|
||||
width = parseInt(settings.width || width || 0, 10) || width;
|
||||
height = parseInt(settings.height || height || 0, 10) || height;
|
||||
}
|
||||
|
||||
var formData = new window.FormData();
|
||||
var csrf = getCsrfPair();
|
||||
formData.append('webcam', blob, 'capture.jpg');
|
||||
formData.append(csrf.name, csrf.value);
|
||||
submitScanFormData(formData);
|
||||
}, 'image/jpeg', 0.9);
|
||||
}
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
updateResolutionLabel();
|
||||
$captureBtn.prop('disabled', false);
|
||||
if (!allowImagePreuploadEdit) {
|
||||
canvas.toBlob(function(blob) {
|
||||
if (!blob) {
|
||||
$results.html('<div class="text-danger">Capture failed.</div>');
|
||||
return;
|
||||
}
|
||||
var formData = new window.FormData();
|
||||
var csrf = getCsrfPair();
|
||||
formData.append('webcam', blob, 'capture.jpg');
|
||||
formData.append(csrf.name, csrf.value);
|
||||
submitScanFormData(formData);
|
||||
}, 'image/jpeg', 0.95);
|
||||
return;
|
||||
}
|
||||
openCaptureEditor(canvas.toDataURL('image/jpeg', 0.95), 'capture', 'capture.jpg');
|
||||
}
|
||||
|
||||
function uploadSelectedFile() {
|
||||
|
|
@ -260,6 +660,18 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (allowImagePreuploadEdit && isEditableImageFile(file)) {
|
||||
$textBox.html('<div class="text-muted small">Preparing image editor...</div>');
|
||||
readFileAsDataUrl(file, function(dataUrl) {
|
||||
if (!dataUrl) {
|
||||
$results.html('<div class="text-danger">Unable to read selected image.</div>');
|
||||
return;
|
||||
}
|
||||
openCaptureEditor(dataUrl, 'upload', file.name || 'upload.jpg');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$uploadBtn.prop('disabled', true).text('Uploading...');
|
||||
$textBox.html('<div class="text-muted small">Uploading file...</div>');
|
||||
|
||||
|
|
@ -274,6 +686,64 @@
|
|||
$stopBtn.off('click.opdscan').on('click.opdscan', stopCamera);
|
||||
$uploadBtn.off('click.opdscan').on('click.opdscan', uploadSelectedFile);
|
||||
$processAiBtn.off('click.opdscan').on('click.opdscan', processLatestAi);
|
||||
$cameraSelect.off('change.opdscan').on('change.opdscan', function() {
|
||||
var deviceId = $(this).val() || '';
|
||||
saveSelectedDevice(deviceId);
|
||||
startCamera(deviceId);
|
||||
});
|
||||
$cameraRefresh.off('click.opdscan').on('click.opdscan', function() {
|
||||
refreshCameraList($cameraSelect.val() || selectedDeviceId || '');
|
||||
startCamera($cameraSelect.val() || selectedDeviceId || '');
|
||||
});
|
||||
$('#opd_scan_rotate_left').off('click.opdscan').on('click.opdscan', function() {
|
||||
rotateEditorImage('left');
|
||||
});
|
||||
$('#opd_scan_rotate_right').off('click.opdscan').on('click.opdscan', function() {
|
||||
rotateEditorImage('right');
|
||||
});
|
||||
$('#opd_scan_crop_btn').off('click.opdscan').on('click.opdscan', function() {
|
||||
cropEditorImage();
|
||||
});
|
||||
$('#opd_scan_reset_edit').off('click.opdscan').on('click.opdscan', function() {
|
||||
loadEditorImage(editState.originalDataUrl);
|
||||
});
|
||||
$('#opd_scan_save_edit').off('click.opdscan').on('click.opdscan', function() {
|
||||
saveEditedCapture();
|
||||
});
|
||||
|
||||
if (editCanvas) {
|
||||
editCanvas.addEventListener('mousedown', function(evt) {
|
||||
if (!editState.imageRect) {
|
||||
return;
|
||||
}
|
||||
editState.dragging = true;
|
||||
editState.dragStart = getCanvasPoint(evt);
|
||||
editState.selection = normalizeSelection(editState.dragStart, editState.dragStart, editState.imageRect);
|
||||
drawEditorCanvas();
|
||||
});
|
||||
editCanvas.addEventListener('mousemove', function(evt) {
|
||||
if (!editState.dragging || !editState.dragStart || !editState.imageRect) {
|
||||
return;
|
||||
}
|
||||
editState.selection = normalizeSelection(editState.dragStart, getCanvasPoint(evt), editState.imageRect);
|
||||
drawEditorCanvas();
|
||||
});
|
||||
editCanvas.addEventListener('mouseup', function(evt) {
|
||||
if (!editState.dragging || !editState.dragStart || !editState.imageRect) {
|
||||
return;
|
||||
}
|
||||
editState.selection = normalizeSelection(editState.dragStart, getCanvasPoint(evt), editState.imageRect);
|
||||
editState.dragging = false;
|
||||
editState.dragStart = null;
|
||||
drawEditorCanvas();
|
||||
});
|
||||
editCanvas.addEventListener('mouseleave', function() {
|
||||
if (editState.dragging) {
|
||||
editState.dragging = false;
|
||||
editState.dragStart = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.removeOpdScanImage = function(fileId) {
|
||||
if (!fileId) {
|
||||
|
|
@ -300,12 +770,22 @@
|
|||
});
|
||||
};
|
||||
|
||||
startCamera();
|
||||
refreshCameraList(selectedDeviceId);
|
||||
startCamera(selectedDeviceId);
|
||||
loadLastList();
|
||||
|
||||
$('#tallModal').off('hidden.bs.modal.opdscan').on('hidden.bs.modal.opdscan', function() {
|
||||
stopCamera();
|
||||
$('#opd_scan_results').html('Captured image will appear here.');
|
||||
$resolution.text('Camera: closed');
|
||||
});
|
||||
if (editModalEl) {
|
||||
editModalEl.addEventListener('hidden.bs.modal', function() {
|
||||
editState.selection = null;
|
||||
editState.dragging = false;
|
||||
editState.dragStart = null;
|
||||
$editStatus.text('No crop selected.');
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user