=3)', `QR_PrintOutRetryCount` int(11) NOT NULL DEFAULT 0 COMMENT 'Berapa kali sudah dicoba upload, maksimal 3', `QR_PrintOutLastRetryAt` datetime DEFAULT NULL COMMENT 'Waktu percobaan upload terakhir yang gagal', `QR_PrintOutUploadedAt` datetime DEFAULT NULL COMMENT 'Waktu upload PDF ke dedicated server berhasil', `QR_PrintOutCreatedAt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Waktu token QR dibuat', `QR_PrintOutCreatedByUserID` int(11) NOT NULL DEFAULT 0 COMMENT 'UserID yang mencetak laporan', `QR_PrintOutIsActive` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1=aktif, 0=dinonaktifkan (mis. hasil direvisi)', PRIMARY KEY (`QR_PrintOutID`), UNIQUE KEY `uq_qr_uuid` (`QR_PrintOutUUID`), KEY `idx_order_header` (`QR_PrintOutT_OrderHeaderID`), KEY `idx_upload_status` (`QR_PrintOutUploadStatus`), KEY `idx_retry` (`QR_PrintOutUploadStatus`, `QR_PrintOutRetryCount`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COMMENT='Token QR Code untuk verifikasi keaslian laporan hasil laboratorium'; */ /** * Generateqrreport Library * * Library untuk generate QR Code pada laporan hasil laboratorium. * QR Code berisi URL langsung ke file PDF di dedicated server. * Saat QR di-scan, browser langsung membuka file PDF tersebut. * * Arsitektur: * * ┌─ PHP App (ini) ──────────────────────────────────────────────┐ * │ 1. saveQRPrintout(verifyBaseURL='https://ds.com/files') │ * │ → UUID + verifyURL = https://ds.com/files/{uuid}.pdf │ * │ verifyURL = URL final PDF = yang di-encode ke QR Code │ * │ 2. generateQRImageBase64(verifyURL) → embed QR ke PDF cetak │ * │ 3. saveTempPDF() → simpan PDF sementara di /tmp/ │ * └──────────────────────────────────────────────────────────────┘ * │ * ▼ * ┌─ Golang Upload Tool ─────────────────────────────────────────┐ * │ - Poll PHP API: getPendingUploads() │ * │ - Ambil TempFilePath dari row │ * │ - Upload PDF ke dedicated server pada path sesuai verifyURL │ * │ - Berhasil → callback confirmUpload($uuid) │ * │ - Gagal → callback incrementRetry($uuid) │ * └──────────────────────────────────────────────────────────────┘ * * ┌─ Dedicated Server ───────────────────────────────────────────┐ * │ - Serve file PDF statis │ * │ - URL: https://ds.com/files/{uuid}.pdf │ * │ - Saat QR di-scan → browser langsung buka PDF ini │ * └──────────────────────────────────────────────────────────────┘ * * Tabel yang digunakan: qr_printout * (lihat: sql/qr_printout_updated.sql untuk DDL lengkap) */ class Generateqrreport { /** @var CI_DB_driver */ protected $db_smartone; /** @var CI_DB_driver */ protected $db_onedev; function __construct() { $CI = & get_instance(); $this->db_smartone = $CI->load->database("default", true); $this->db_onedev = $CI->load->database("default", true); $this->_loadQRLib(); } // ========================================================================= // PRIVATE HELPERS // ========================================================================= /** * Load phpqrcode library jika belum di-include. */ private function _loadQRLib() { if (!class_exists('QRcode', false)) { $libPath = APPPATH . 'libraries/qrcode/'; if (!defined('QR_CACHEABLE')) define('QR_CACHEABLE', false); if (!defined('QR_CACHE_DIR')) define('QR_CACHE_DIR', APPPATH . 'cache/'); if (!defined('QR_LOG_DIR')) define('QR_LOG_DIR', APPPATH . 'logs/'); if (!defined('QR_FIND_BEST_MASK')) define('QR_FIND_BEST_MASK', true); if (!defined('QR_FIND_FROM_RANDOM')) define('QR_FIND_FROM_RANDOM', false); if (!defined('QR_PNG_MAXIMUM_SIZE')) define('QR_PNG_MAXIMUM_SIZE', 1024); include_once $libPath . 'phpqrcode.php'; } } /** * Generate UUID v4. * * @param bool $withHyphens true → 36 char 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' * false → 32 char hex tanpa tanda hubung * @return string */ private function _generateUUID($withHyphens = true) { $data = random_bytes(16); $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // version 4 $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // variant bits $uuid = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); return $withHyphens ? $uuid : str_replace('-', '', $uuid); } /** * Normalisasi UUID: konversi 32-char (tanpa hyphen) ke format 36-char standar. * * @param string $uuid * @return string lowercase 36-char UUID */ private function _normalizeUUID($uuid) { $uuid = trim($uuid); if (strlen($uuid) === 32 && strpos($uuid, '-') === false) { $uuid = substr($uuid, 0, 8) . '-' . substr($uuid, 8, 4) . '-' . substr($uuid, 12, 4) . '-' . substr($uuid, 16, 4) . '-' . substr($uuid, 20); } return strtolower($uuid); } /** * Validasi format UUID v4 (8-4-4-4-12). * * @param string $uuid * @return bool */ private function _validateUUIDFormat($uuid) { return (bool)preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', strtolower($uuid) ); } // ========================================================================= // PUBLIC METHODS — QR TOKEN // ========================================================================= /** * Buat record QR baru di tabel qr_printout. * * verifyURL = URL LANGSUNG ke file PDF di dedicated server. * URL ini yang di-encode ke gambar QR Code. * Golang tool akan upload PDF ke path/URL ini. * * Contoh: * verifyBaseURL = 'https://files.lab.rs.com/reports' * verifyURL = 'https://files.lab.rs.com/reports/{uuid}.pdf' * * @param array $params Wajib: * - orderHeaderID (int) FK ke t_orderheader * - groupResultName (string) Label group, mis: 'HEMATOLOGI' * - verifyBaseURL (string) Base URL folder PDF di dedicated server, * contoh: 'https://files.lab.rs.com/reports' * Hasilnya: {verifyBaseURL}/{uuid}.pdf * Opsional: * - groupResultID (int) default 0 * - testID (int) default 0 * - createdByUserID (int) default 0 * @return array [ * 'success' => bool, * 'uuid' => string|null, // 36-char dengan hyphen * 'uuid_short' => string|null, // 32-char tanpa hyphen * 'verifyURL' => string|null, // URL PDF final = yang di-encode ke QR Code * 'qr_printout_id' => int|null, * 'message' => string * ] */ public function saveQRPrintout(array $params) { $required = ['orderHeaderID', 'groupResultName', 'verifyBaseURL', 'QR_PrintOutReportURL']; foreach ($required as $field) { if (empty($params[$field])) { return [ 'success' => false, 'uuid' => null, 'uuid_short' => null, 'verifyURL' => null, 'qr_printout_id' => null, 'message' => "Parameter '{$field}' wajib diisi.", ]; } } $uuid = $this->_generateUUID(true); $uuidShort = str_replace('-', '', $uuid); // verifyURL = URL langsung ke PDF: {base}/{uuid}.pdf // Golang akan upload PDF ke path ini di dedicated server $verifyURL = rtrim($params['verifyBaseURL'], '/') . '/' . str_replace('-', '', $uuid) . '.pdf'; $data = [ 'QR_PrintOutT_OrderHeaderID' => (int)$params['orderHeaderID'], 'QR_PrintOutGroup_ResultID' => (int)($params['groupResultID'] ?? 0), 'QR_PrintOutT_TestID' => (int)($params['testID'] ?? 0), 'QR_PrintOutGroup_ResultName' => $params['groupResultName'], 'QR_PrintOutUUID' => $uuid, 'QR_PrintOutVerifyURL' => $verifyURL, // URL final PDF = yang di-encode ke QR Code 'QR_PrintOutReportURL' => $params['QR_PrintOutReportURL'], // URL sumber PDF di PHP server (Golang fetch dari sini) 'QR_PrintOutTempFilePath' => '', 'QR_PrintOutUploadStatus' => 'pending', 'QR_PrintOutRetryCount' => 0, 'QR_PrintOutLastRetryAt' => null, 'QR_PrintOutUploadedAt' => null, 'QR_PrintOutCreatedAt' => date('Y-m-d H:i:s'), 'QR_PrintOutCreatedByUserID' => (int)($params['createdByUserID'] ?? 0), 'QR_PrintOutIsActive' => 1, ]; $this->db_smartone->insert('qr_printout', $data); if ($this->db_smartone->affected_rows() === 0) { return [ 'success' => false, 'uuid' => null, 'uuid_short' => null, 'verifyURL' => null, 'qr_printout_id' => null, 'message' => 'Gagal menyimpan data QR Printout ke database.', ]; } return [ 'success' => true, 'uuid' => $uuid, 'uuid_short' => $uuidShort, 'verifyURL' => $verifyURL, // ini yang di-encode ke gambar QR Code 'qr_printout_id' => $this->db_smartone->insert_id(), 'message' => 'OK', ]; } /** * Nonaktifkan QR Code (misal karena hasil direvisi dan QR lama tidak boleh diakses). * * @param string $uuid * @return bool */ public function deactivateQR($uuid) { $uuid = $this->_normalizeUUID($uuid); $this->db_smartone->where('QR_PrintOutUUID', $uuid) ->update('qr_printout', ['QR_PrintOutIsActive' => 0]); return $this->db_smartone->affected_rows() > 0; } // ========================================================================= // PUBLIC METHODS — QR IMAGE // ========================================================================= /** * Generate QR Code sebagai Base64 PNG string. * Hasil bisa langsung di-embed ke HTML/PDF: * * @param string $url URL yang akan di-encode (verifyURL dari saveQRPrintout) * @param int $pixelSize Ukuran pixel per modul (1–10), default 5 * @param string $errorLevel Koreksi error: L, M, Q, H (default H) * @return string "data:image/png;base64,..." atau '' jika gagal */ public function generateQRImageBase64($url, $pixelSize = 5, $errorLevel = 'H') { $errorLevel = in_array($errorLevel, ['L', 'M', 'Q', 'H']) ? $errorLevel : 'H'; $pixelSize = min(max((int)$pixelSize, 1), 10); ob_start(); QRcode::png($url, false, $errorLevel, $pixelSize, 2); $imgData = ob_get_clean(); return empty($imgData) ? '' : 'data:image/png;base64,' . base64_encode($imgData); } /** * Generate QR Code dan simpan sebagai file PNG di disk. * * @param string $url URL yang akan di-encode * @param string $savePath Path lengkap tujuan, misal: FCPATH.'qrcodes/abc.png' * @param int $pixelSize Ukuran pixel per modul (1–10), default 5 * @param string $errorLevel Koreksi error: L, M, Q, H (default H) * @return bool true jika berhasil disimpan */ public function generateQRImageFile($url, $savePath, $pixelSize = 5, $errorLevel = 'H') { $errorLevel = in_array($errorLevel, ['L', 'M', 'Q', 'H']) ? $errorLevel : 'H'; $pixelSize = min(max((int)$pixelSize, 1), 10); $dir = dirname($savePath); if (!is_dir($dir)) { mkdir($dir, 0755, true); } QRcode::png($url, $savePath, $errorLevel, $pixelSize, 2); return file_exists($savePath); } // ========================================================================= // PUBLIC METHODS — PDF TEMP & GOLANG CALLBACK // ========================================================================= // ========================================================================= // PUBLIC METHODS — GOLANG UPLOAD TOOL SUPPORT // ========================================================================= /** * Ambil daftar record yang BELUM diupload ke dedicated server. * Dipakai oleh Golang upload tool sebagai polling. * * Hanya mengambil record dengan: * - UploadStatus = 'pending' ATAU 'failed' (masih bisa retry) * - RetryCount < 3 * - IsActive = 1 * - QR_PrintOutReportURL tidak kosong (PDF sudah tersedia di PHP server) * * Field penting untuk Golang dari setiap row: * - QR_PrintOutUUID : identifier * - QR_PrintOutVerifyURL : URL TUJUAN upload di dedicated server * - QR_PrintOutReportURL : URL SUMBER PDF di PHP server (Golang fetch dari sini) * - QR_PrintOutTempFilePath: path lokal file PDF (opsional, jika Golang akses lokal) * * @param int $limit Jumlah maksimal record (default 10) * @return array */ public function getPendingUploads($limit = 10) { return $this->db_smartone ->where_in('QR_PrintOutUploadStatus', ['pending', 'failed']) ->where('QR_PrintOutRetryCount <', 3) ->where('QR_PrintOutIsActive', 1) ->where('QR_PrintOutReportURL !=', '') // pastikan PDF sudah siap ->order_by('QR_PrintOutCreatedAt', 'ASC') ->limit((int)$limit) ->get('qr_printout') ->result_array(); } /** * Catat bahwa satu percobaan upload gagal dan tambah retry count. * * - Jika RetryCount setelah increment < 3 → status tetap 'failed' (akan dicoba lagi) * - Jika RetryCount setelah increment >= 3 → status menjadi 'failed_permanent' * (tidak akan diambil oleh getPendingUploads() lagi) * * Dipanggil oleh Golang tool saat upload ke dedicated server gagal. * * @param string $uuid * @return array [ * 'success' => bool, * 'retry_count' => int, // retry count setelah increment * 'is_permanent' => bool, // true = sudah melebihi batas, tidak akan di-retry lagi * 'message' => string * ] */ public function incrementRetry($uuid) { $uuid = $this->_normalizeUUID($uuid); if (!$this->_validateUUIDFormat($uuid)) { return ['success' => false, 'retry_count' => 0, 'is_permanent' => false, 'message' => 'Format UUID tidak valid.']; } $qrRow = $this->db_smartone ->select('QR_PrintOutRetryCount, QR_PrintOutUploadStatus') ->where('QR_PrintOutUUID', $uuid) ->get('qr_printout') ->row_array(); if (!$qrRow) { return ['success' => false, 'retry_count' => 0, 'is_permanent' => false, 'message' => 'UUID tidak ditemukan.']; } $newRetryCount = (int)$qrRow['QR_PrintOutRetryCount'] + 1; $isPermanent = $newRetryCount >= 3; $newStatus = $isPermanent ? 'failed_permanent' : 'failed'; $this->db_smartone->where('QR_PrintOutUUID', $uuid) ->update('qr_printout', [ 'QR_PrintOutRetryCount' => $newRetryCount, 'QR_PrintOutLastRetryAt' => date('Y-m-d H:i:s'), 'QR_PrintOutUploadStatus' => $newStatus, ]); return [ 'success' => true, 'retry_count' => $newRetryCount, 'is_permanent' => $isPermanent, 'message' => $isPermanent ? "Retry count mencapai {$newRetryCount}. Status menjadi 'failed_permanent'. Upload otomatis dihentikan." : "Retry count: {$newRetryCount}/3. Akan dicoba kembali.", ]; } /** * Minta upload ulang (re-upload) untuk satu record. * * Gunakan fungsi ini ketika: * (a) File PDF di dedicated server sudah dihapus / kadaluarsa, ATAU * (b) Record sebelumnya 'failed_permanent' dan ingin dicoba ulang secara manual * * Fungsi ini akan: * - Reset UploadStatus → 'pending' * - Reset RetryCount → 0 * - Kosongkan ReportURL (file lama di dedicated server sudah tidak valid) * - Wajib diikuti dengan saveTempPDF() untuk menyiapkan file PDF baru * * @param string $uuid * @return array ['success' => bool, 'message' => string] */ public function requestReUpload($uuid) { $uuid = $this->_normalizeUUID($uuid); if (!$this->_validateUUIDFormat($uuid)) { return ['success' => false, 'message' => 'Format UUID tidak valid.']; } $exists = $this->db_smartone ->where('QR_PrintOutUUID', $uuid) ->count_all_results('qr_printout'); if ($exists === 0) { return ['success' => false, 'message' => 'UUID tidak ditemukan.']; } $this->db_smartone->where('QR_PrintOutUUID', $uuid) ->update('qr_printout', [ 'QR_PrintOutUploadStatus' => 'pending', 'QR_PrintOutRetryCount' => 0, 'QR_PrintOutLastRetryAt' => null, 'QR_PrintOutUploadedAt' => null, 'QR_PrintOutTempFilePath' => '', // harus diisi ulang via saveTempPDF() ]); if ($this->db_smartone->affected_rows() === 0) { return ['success' => false, 'message' => 'Gagal update database.']; } return [ 'success' => true, 'message' => 'Record di-reset ke pending. Panggil saveTempPDF() untuk menyiapkan PDF baru.', ]; } /** * Konfirmasi bahwa Golang berhasil upload PDF ke dedicated server. * * URL QR_PrintOutVerifyURL tidak berubah (sudah fix sejak saveQRPrintout). * Fungsi ini hanya mengubah status → 'uploaded'. * * Alur Golang: * 1. Poll getPendingUploads() → dapat row dengan: * QR_PrintOutReportURL (fetch PDF dari PHP server via HTTP) * QR_PrintOutVerifyURL (upload PDF ke dedicated server di path ini) * 2. Download PDF dari QR_PrintOutReportURL * 3. Upload ke dedicated server sesuai QR_PrintOutVerifyURL * 4. Callback PHP: confirmUpload($uuid) * * @param string $uuid UUID record qr_printout * @return array ['success' => bool, 'message' => string] */ public function confirmUpload($uuid) { $uuid = $this->_normalizeUUID($uuid); if (!$this->_validateUUIDFormat($uuid)) { return ['success' => false, 'message' => 'Format UUID tidak valid.']; } $exists = $this->db_smartone ->where('QR_PrintOutUUID', $uuid) ->count_all_results('qr_printout'); if ($exists === 0) { return ['success' => false, 'message' => 'UUID tidak ditemukan di database.']; } $this->db_smartone->where('QR_PrintOutUUID', $uuid) ->update('qr_printout', [ 'QR_PrintOutUploadStatus' => 'uploaded', 'QR_PrintOutUploadedAt' => date('Y-m-d H:i:s'), ]); if ($this->db_smartone->affected_rows() === 0) { return ['success' => false, 'message' => 'Gagal update database.']; } return ['success' => true, 'message' => 'OK']; } /** * Tandai upload gagal permanen tanpa menambah retry count. * Gunakan ini hanya untuk override manual (mis. admin force-fail). * Untuk kasus normal, gunakan incrementRetry() agar retry count terlacak. * * @param string $uuid * @return bool */ public function markUploadFailed($uuid) { $uuid = $this->_normalizeUUID($uuid); $this->db_smartone->where('QR_PrintOutUUID', $uuid) ->update('qr_printout', [ 'QR_PrintOutUploadStatus' => 'failed_permanent', 'QR_PrintOutRetryCount' => 3, ]); return $this->db_smartone->affected_rows() > 0; } /** * Reset URL report (untuk upload ulang jika laporan direvisi). * @deprecated Gunakan requestReUpload() yang lebih lengkap. * * @param string $uuid * @return array ['success' => bool, 'message' => string] */ public function resetReportURL($uuid) { return $this->requestReUpload($uuid); } // ========================================================================= // PUBLIC METHODS — VERIFIKASI (dipakai dedicated server / controller publik) // ========================================================================= /** * Ambil URL PDF report berdasarkan UUID. * * Dipakai jika PHP app perlu tahu URL PDF (mis. untuk redirect). * Scan tracking dihandle oleh dedicated server. * * @param string $uuid UUID (36 atau 32 char) * @return array [ * 'success' => bool, * 'report_url' => string|null, * 'qr_info' => array|null, * 'message' => string * ] */ public function getReportURL($uuid) { $uuid = $this->_normalizeUUID($uuid); if (!$this->_validateUUIDFormat($uuid)) { return ['success' => false, 'report_url' => null, 'qr_info' => null, 'message' => 'Format UUID tidak valid.']; } $qrRow = $this->db_smartone ->where('QR_PrintOutUUID', $uuid) ->where('QR_PrintOutIsActive', 1) ->get('qr_printout') ->row_array(); if (!$qrRow) { return ['success' => false, 'report_url' => null, 'qr_info' => null, 'message' => 'QR Code tidak ditemukan atau sudah tidak aktif.']; } // verifyURL = URL langsung ke PDF, hanya valid jika sudah ter-upload if ($qrRow['QR_PrintOutUploadStatus'] !== 'uploaded') { $status = $qrRow['QR_PrintOutUploadStatus']; $msg = $status === 'failed_permanent' ? 'Upload PDF gagal permanen. Hubungi admin laboratorium.' : 'Report PDF sedang diproses. Coba beberapa saat lagi.'; return ['success' => false, 'report_url' => null, 'qr_info' => $qrRow, 'message' => $msg]; } return [ 'success' => true, 'report_url' => $qrRow['QR_PrintOutVerifyURL'], // URL langsung ke PDF 'qr_info' => $qrRow, 'message' => 'OK', ]; } // ========================================================================= // LEGACY / UTILITY // ========================================================================= function clean_mysqli_connection($dbc) { while (mysqli_more_results($dbc)) { if (mysqli_next_result($dbc)) { $result = mysqli_use_result($dbc); if (get_class($result) == 'mysqli_stmt') { mysqli_stmt_free_result($result); } else { unset($result); } } } } }