diff --git a/AGENTS.md b/AGENTS.md index baea5a4a..56a04a63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,7 @@ - If a task changes live DB objects such as table schema, trigger, stored procedure, or function, always add a SQL record file under `sql/manual_changes/`. - Name the SQL record file with the pattern `YYYY-MM-DD-.sql`. - The SQL record file must include the actual SQL change that was applied, not just a note. +- Before every `commit` and `push`, always check first whether local branch needs to pull/rebase from remote. ## graphify diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..56a04a63 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,20 @@ +# Repo Working Notes + +- Verify the actual file, class name, and call site before changing a library/controller reference. +- If a class load error appears, check the existing library file and the current repo usage pattern first. +- Do not swap to a different library name on assumption alone. +- Keep fixes minimal and local unless the user asks for a broader refactor. +- If a task changes live DB objects such as table schema, trigger, stored procedure, or function, always add a SQL record file under `sql/manual_changes/`. +- Name the SQL record file with the pattern `YYYY-MM-DD-.sql`. +- The SQL record file must include the actual SQL change that was applied, not just a note. +- Before every `commit` and `push`, always check first whether local branch needs to pull/rebase from remote. + +## graphify + +This project has a graphify knowledge graph at graphify-out/. + +Rules: +- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure +- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files +- For cross-module "how does X relate to Y" questions, prefer `graphify query ""`, `graphify path "" ""`, or `graphify explain ""` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files +- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) diff --git a/application/controllers/tools/Inform_consent.php b/application/controllers/tools/Inform_consent.php new file mode 100644 index 00000000..b1d27ba6 --- /dev/null +++ b/application/controllers/tools/Inform_consent.php @@ -0,0 +1,706 @@ +db_onedev = $this->load->database('onedev', true); + } + + public function index() + { + $this->sys_ok([ + 'message' => 'Use /tools/inform_consent/pdf?T_OrderHeaderID=' + ]); + } + + public function pdf() + { + try { + $orderHeaderId = intval($this->input->get('T_OrderHeaderID', true)); + if ($orderHeaderId <= 0) { + $orderHeaderId = intval($this->input->get('PID', true)); + } + if ($orderHeaderId <= 0) { + $this->sys_error('T_OrderHeaderID mandatory'); + return; + } + + $patient = $this->get_patient_data($orderHeaderId); + if (!$patient) { + $this->sys_error('Order not found'); + return; + } + + $consent = $this->get_consent_content(); + $printBy = trim((string)$this->input->get('username', true)); + if ($printBy === '') { + $printBy = 'ADMIN'; + } + $printDate = date('M j, Y g:i A'); + + require_once APPPATH . 'third_party/fpdf/fpdf.php'; + $pdf = new FPDF('P', 'mm', 'A4'); + $pdf->SetAutoPageBreak(false); + $pdf->SetMargins(10, 10, 10); + + $remainingBlocks = $this->render_first_page($pdf, $patient, $consent, $printBy, $printDate); + $this->render_second_page($pdf, $patient, $remainingBlocks, $printBy, $printDate); + + header('Content-Type: application/pdf'); + header('Content-Disposition: inline; filename="inform_consent_' . $orderHeaderId . '.pdf"'); + echo $pdf->Output('S'); + } catch (Exception $e) { + $this->sys_error($e->getMessage()); + } + } + + private function get_patient_data($orderHeaderId) + { + $sql = "SELECT + h.T_OrderHeaderID, + h.T_OrderHeaderDate, + DATE_FORMAT(h.T_OrderHeaderDate, '%d-%m-%Y') as tanggal, + DATE_FORMAT(h.T_OrderHeaderDate, '%H:%i') as jam, + p.M_PatientID, + p.M_PatientNoReg, + CONCAT( + IF(TRIM(IFNULL(t.M_TitleName,''))='', '', CONCAT(TRIM(IFNULL(t.M_TitleName,'')), '. ')), + TRIM(IFNULL(p.M_PatientPrefix,'')), ' ', TRIM(IFNULL(p.M_PatientName,'')), ' ', TRIM(IFNULL(p.M_PatientSuffix,'')) + ) AS patient_name, + s.M_SexCode, + IFNULL(p.M_PatientPOB,'') as pob, + DATE_FORMAT(p.M_PatientDOB, '%d-%m-%Y') as dob, + IFNULL(pa.M_PatientAddressDescription,'') as alamat, + IFNULL(p.M_PatientHP,'') as phone, + IFNULL(c.M_CompanyName,'') as company_name, + ( + SELECT ps.Patient_SignatureUrl + FROM patient_signature ps + WHERE ps.Patient_SignatureM_PatientID = p.M_PatientID + AND ps.Patient_SignatureIsActive = 'Y' + ORDER BY ps.Patient_SignatureID DESC + LIMIT 1 + ) AS signature_url + FROM t_orderheader h + JOIN m_patient p ON p.M_PatientID = h.T_OrderHeaderM_PatientID + LEFT JOIN m_title t ON t.M_TitleID = p.M_PatientM_TitleID + LEFT JOIN m_sex s ON s.M_SexID = p.M_PatientM_SexID + LEFT JOIN m_company c ON c.M_CompanyID = h.T_OrderHeaderM_CompanyID + LEFT JOIN m_patientaddress pa ON pa.M_PatientAddressM_PatientID = p.M_PatientID + AND pa.M_PatientAddressNote = 'Utama' + AND pa.M_PatientAddressIsActive = 'Y' + WHERE h.T_OrderHeaderID = ? + LIMIT 1"; + $qry = $this->db_onedev->query($sql, [$orderHeaderId]); + if (!$qry) { + return false; + } + return $qry->row_array(); + } + + private function get_consent_content() + { + $sql = "SELECT M_InformConsentContent + FROM m_informconsent + WHERE M_InformConsentType = 'umum' AND M_InformConsentIsActive = 'Y' + LIMIT 1"; + $qry = $this->db_onedev->query($sql); + if (!$qry || !$qry->row_array()) { + return ''; + } + return (string)$qry->row_array()['M_InformConsentContent']; + } + + private function render_first_page($pdf, $patient, $consentHtml, $printBy, $printDate) + { + $pdf->AddPage(); + $this->render_header($pdf); + + $pdf->SetFont('Arial', 'B', 10); + $pdf->SetFillColor(220, 220, 220); + $pdf->SetXY(10, 42); + $pdf->Cell(190, 8, 'PENGKAJIAN DATA UMUM PASIEN', 1, 1, 'C', true); + + $pdf->SetFont('Arial', '', 8); + $pdf->SetX(10); + $pdf->Cell(145, 6, 'Tanggal : ' . $patient['tanggal'], 1, 0, 'L'); + $pdf->Cell(45, 6, 'Jam : ' . $patient['jam'], 1, 1, 'L'); + + $pdf->SetFont('Arial', 'B', 10); + $pdf->SetX(10); + $pdf->Cell(190, 8, 'IDENTITAS PASIEN', 1, 1, 'L'); + $pdf->SetFont('Arial', 'I', 8); + $pdf->SetX(10); + $pdf->Cell(190, 5, '(Diisi sesuai Kartu Tanda Pengenal Pasien yang masih berlaku : KTP/ KK/ SIM/ Kartu Pelajar/ dsb)', 1, 1, 'L'); + + $boxStart = $pdf->GetY(); + $pdf->SetFont('Arial', '', 8); + $this->kv($pdf, 'Nomor RM', $patient['M_PatientNoReg']); + $this->kv($pdf, 'Nama', trim(preg_replace('/\s+/', ' ', $patient['patient_name']))); + $this->kv($pdf, 'Jenis Kelamin', $patient['M_SexCode']); + $this->kv($pdf, 'Tempat/Tgl Lahir', trim($patient['pob']) . ' / ' . trim($patient['dob'])); + $this->kv($pdf, 'Alamat', $patient['alamat']); + $this->kv($pdf, 'No. Telepon', $patient['phone']); + $this->kv($pdf, 'Status Pembiayaan', 'Umum / BPJS / PT..................................................'); + $boxEnd = $pdf->GetY(); + $pdf->Rect(10, $boxStart, 190, max(8, $boxEnd - $boxStart)); + + $pdf->Ln(4); + $pdf->SetFont('Arial', 'B', 10); + $pdf->SetFillColor(220, 220, 220); + $pdf->SetX(10); + $pdf->Cell(190, 7, 'PERSETUJUAN UMUM/ GENERAL CONSENT', 1, 1, 'C', true); + $pdf->SetFont('Arial', 'BI', 8); + $pdf->SetX(10); + $pdf->Cell(190, 6, 'PASIEN/WALI PASIEN MEMBACA, MEMAHAMI DAN MENGISI INFORMASI BERIKUT', 1, 1, 'C'); + + $pdf->SetFont('Arial', '', 8); + $pdf->SetX(10); + $pdf->Cell(190, 6, 'Yang bertanda tangan di bawah ini, saya :', 1, 1, 'L'); + + $box2Start = $pdf->GetY(); + $this->kv($pdf, 'Nama', trim(preg_replace('/\s+/', ' ', $patient['patient_name']))); + $this->kv($pdf, 'Jenis Kelamin', $patient['M_SexCode']); + $this->kv($pdf, 'Tempat/Tgl Lahir', trim($patient['pob']) . ' / ' . trim($patient['dob'])); + $this->kv($pdf, 'Alamat', $patient['alamat']); + $this->kv($pdf, 'No.Telepon', $patient['phone']); + $this->kv($pdf, 'Bertindak atas', 'Diri Saya Sendiri/ Wali / Orang Tua / Anak / Saudara .*'); + $contentBoxStart = $box2Start; + + $pdf->Ln(3); + $pdf->SetFont('Arial', 'B', 8); + $pdf->SetX(18); + $pdf->Cell(182, 5, '1. PERSETUJUAN UNTUK PEMERIKSAAN DAN PERAWATAN', 0, 1, 'L'); + + $blocks = $this->html_to_blocks($consentHtml); + $lineH = 4.6; + $splitIndex = 0; + + $pdf->SetFont('Arial', '', 8); + for ($i = 0; $i < count($blocks); $i++) { + $block = $blocks[$i]; + $blockH = $this->measure_block_height($pdf, $block, 22, 176, $lineH); + if (($pdf->GetY() + $blockH) > $this->pageContentBottom) { + break; + } + $this->render_block($pdf, $block, 22, 176, $lineH); + $splitIndex = $i + 1; + } + + $contentBoxEnd = min($this->pageContentBottom, $pdf->GetY() + 2); + $pdf->Rect(10, $contentBoxStart, 190, max(10, $contentBoxEnd - $contentBoxStart)); + + $this->draw_print_footer($pdf, $printBy, $printDate, 1, 2); + return array_slice($blocks, $splitIndex); + } + + private function render_second_page($pdf, $patient, $remainingBlocks, $printBy, $printDate) + { + $pdf->AddPage(); + $this->render_header($pdf); + + $boxStartY = 42; + $pdf->SetY($boxStartY); + $pdf->SetFont('Arial', '', 8); + $lineH = 4.6; + foreach ($remainingBlocks as $block) { + $blockH = $this->measure_block_height($pdf, $block, 22, 176, $lineH); + if (($pdf->GetY() + $blockH) > 248) { + break; + } + $this->render_block($pdf, $block, 22, 176, $lineH); + } + + $top = $pdf->GetY() + 2; + $signTop = $top; + $signBoxHeight = 44; + $signBottom = $signTop + $signBoxHeight; + + $pdf->Rect(12, $signTop, 186, $signBoxHeight); + + $rightX = 132; + $rightW = 64; + $pdf->SetFont('Arial', '', 9); + $pdf->SetXY($rightX, $signTop + 2); + $pdf->Cell($rightW, 6, 'Semarang, ' . $patient['tanggal'], 0, 1, 'L'); + + $pdf->SetXY(14, $signBottom - 29); + $pdf->Cell(40, 6, 'Petugas', 0, 1, 'C'); + + $pdf->SetXY($rightX, $signBottom - 29); + $pdf->Cell($rightW, 6, 'Pasien/ Wali Pasien', 0, 1, 'L'); + + $pdf->SetXY(16, $signBottom - 9); + $pdf->Cell(62, 6, '........................................', 0, 1, 'L'); + + $signaturePath = $this->resolve_signature_path(isset($patient['signature_url']) ? $patient['signature_url'] : ''); + if ($signaturePath !== null) { + $pdf->Image($signaturePath, $rightX - 6, $signBottom - 23, 42, 12); + } + $this->draw_full_name_block($pdf, $rightX, $signBottom - 9, $rightW, trim(preg_replace('/\s+/', ' ', $patient['patient_name']))); + + $boxEndY = min($this->pageContentBottom, $signBottom + 2); + $pdf->Rect(10, $boxStartY, 190, max(10, $boxEndY - $boxStartY)); + + $this->draw_print_footer($pdf, $printBy, $printDate, 2, 2); + } + + private function draw_print_footer($pdf, $printBy, $printDate, $page, $total) + { + $y = $this->footerY; + $pdf->SetFont('Times', '', 7); + + $pdf->SetXY(14, $y); + $pdf->Cell(22, 4, 'Print Oleh :', 0, 0, 'L'); + $pdf->Cell(50, 4, $this->safe_text($printBy), 0, 0, 'L'); + $pdf->Cell(10, 4, (string)$page, 0, 0, 'C'); + $pdf->Cell(6, 4, '/', 0, 0, 'C'); + $pdf->Cell(10, 4, (string)$total, 0, 0, 'C'); + + $date = $this->safe_text($printDate); + $dateW = $pdf->GetStringWidth($date); + $rightMargin = 196; + $xDate = max(120, $rightMargin - $dateW); + $pdf->SetXY($xDate, $y); + $pdf->Cell($dateW + 1, 4, $date, 0, 1, 'L'); + } + + private function render_header($pdf) + { + $logoPath = APPPATH . '../assets/images/logo-ibl.png'; + if (is_file($logoPath)) { + $pdf->Image($logoPath, 14, 10, 56, 12); + } else { + $pdf->SetFont('Arial', 'B', 16); + $pdf->SetTextColor(220, 0, 0); + $pdf->SetXY(14, 12); + $pdf->Cell(80, 8, 'KLINIK IBL', 0, 1, 'L'); + $pdf->SetTextColor(0, 0, 0); + } + + $pdf->SetFont('Arial', '', 10); + $pdf->SetXY(170, 20); + $pdf->Cell(20, 6, 'RM. 01', 0, 1, 'L'); + } + + private function kv($pdf, $label, $value) + { + $x = 12; + $labelW = 35; + $colonW = 4; + $valueW = 149; + + $pdf->SetX($x); + $pdf->Cell($labelW, 5, $this->safe_text($label), 0, 0, 'L'); + $pdf->Cell($colonW, 5, ':', 0, 0, 'C'); + $pdf->MultiCell($valueW, 5, $this->safe_text($value), 0, 'L'); + } + + private function html_to_blocks($html) + { + $input = (string)$html; + $input = str_replace(['\\r\\n', '\\n', '\\r'], "\n", $input); + $input = str_replace(["\r\n", "\r"], "\n", $input); + + if (!class_exists('DOMDocument')) { + return $this->html_to_blocks_fallback($input); + } + + libxml_use_internal_errors(true); + $dom = new DOMDocument('1.0', 'UTF-8'); + $wrapped = '' . $input . ''; + $ok = $dom->loadHTML(mb_convert_encoding($wrapped, 'HTML-ENTITIES', 'UTF-8')); + libxml_clear_errors(); + + if (!$ok) { + return []; + } + + $blocks = []; + $ctx = ['indent' => 0, 'listType' => null, 'index' => 1]; + $body = $dom->getElementsByTagName('body')->item(0); + if (!$body) { + return []; + } + + foreach ($body->childNodes as $node) { + $this->walk_html_node($node, $blocks, $ctx); + } + + return $blocks; + } + + private function html_to_blocks_fallback($input) + { + $blocks = []; + $s = (string)$input; + $s = preg_replace('/\\\\r\\\\n|\\\\n|\\\\r/', "\n", $s); + $s = preg_replace('//i', "\n", $s); + $s = preg_replace('/<\\/(div|p|h[1-6]|li)>/i', "$0\n", $s); + + preg_match_all('/<(h[1-6]|p|li)[^>]*>(.*?)<\\/\\1>/is', $s, $matches, PREG_SET_ORDER); + foreach ($matches as $m) { + $tag = strtolower($m[1]); + $inner = (string)$m[2]; + + // remove nested list blocks inside current node to avoid duplicate render + $inner = preg_replace('/<(ul|ol)[^>]*>.*?<\\/\\1>/is', '', $inner); + $plain = trim(preg_replace('/\\s+/', ' ', html_entity_decode(strip_tags($inner), ENT_QUOTES | ENT_HTML5, 'UTF-8'))); + if ($plain === '' || $plain === ':') { + continue; + } + + $indent = 0; + $style = ''; + $text = $plain; + + if ($tag === 'h1' || $tag === 'h2' || $tag === 'h3' || $tag === 'h4' || $tag === 'h5' || $tag === 'h6') { + $style = 'B'; + $indent = 0; + } elseif ($tag === 'li') { + $indent = 8; + $text = '• ' . $plain; + } elseif ($tag === 'p') { + if (stripos($inner, ' $text, + 'indent' => $indent, + 'style' => $style + ]; + } + + if (empty($blocks)) { + $lines = preg_split('/\\n+/', trim(html_entity_decode(strip_tags($s), ENT_QUOTES | ENT_HTML5, 'UTF-8'))); + foreach ($lines as $line) { + $line = trim(preg_replace('/\\s+/', ' ', (string)$line)); + if ($line === '') { + continue; + } + $blocks[] = ['text' => $line, 'indent' => 0, 'style' => '']; + } + } + + return $blocks; + } + + private function walk_html_node($node, &$blocks, $ctx) + { + if (!$node) { + return; + } + + if ($node->nodeType === XML_TEXT_NODE) { + $text = trim(preg_replace('/\s+/', ' ', html_entity_decode($node->nodeValue, ENT_QUOTES | ENT_HTML5, 'UTF-8'))); + if ($text !== '') { + $blocks[] = [ + 'text' => $text, + 'indent' => $ctx['indent'], + 'style' => '' + ]; + } + return; + } + + if ($node->nodeType !== XML_ELEMENT_NODE) { + return; + } + + $tag = strtolower($node->nodeName); + if ($tag === 'br') { + return; + } + + if (in_array($tag, ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], true)) { + $text = $this->node_plain_text($node); + if ($text !== '') { + $blocks[] = [ + 'text' => $text, + 'indent' => max(0, $ctx['indent']), + 'style' => 'B' + ]; + } + return; + } + + if ($tag === 'p') { + $text = $this->node_plain_text($node); + if ($text !== '') { + $isBold = $this->node_has_tag($node, 'strong'); + $blocks[] = [ + 'text' => $text, + 'indent' => max(0, $ctx['indent']), + 'style' => $isBold ? 'B' : '' + ]; + } + return; + } + + if ($tag === 'ul' || $tag === 'ol') { + $childCtx = [ + 'indent' => $ctx['indent'] + 8, + 'listType' => $tag, + 'index' => 1 + ]; + foreach ($node->childNodes as $child) { + $this->walk_html_node($child, $blocks, $childCtx); + if ($tag === 'ol' && $child->nodeType === XML_ELEMENT_NODE && strtolower($child->nodeName) === 'li') { + $childCtx['index']++; + } + } + return; + } + + if ($tag === 'li') { + $prefix = '• '; + if ($ctx['listType'] === 'ol') { + $prefix = $ctx['index'] . '. '; + } + + $firstText = $this->node_direct_text($node); + if ($firstText !== '') { + $blocks[] = [ + 'text' => $prefix . $firstText, + 'indent' => max(0, $ctx['indent']), + 'style' => '' + ]; + } + + foreach ($node->childNodes as $child) { + if ($child->nodeType === XML_ELEMENT_NODE) { + $childTag = strtolower($child->nodeName); + if ($childTag === 'ul' || $childTag === 'ol' || $childTag === 'p') { + $nestedCtx = [ + 'indent' => $ctx['indent'] + 8, + 'listType' => $childTag === 'ol' ? 'ol' : ($childTag === 'ul' ? 'ul' : null), + 'index' => 1 + ]; + $this->walk_html_node($child, $blocks, $nestedCtx); + } + } + } + return; + } + + foreach ($node->childNodes as $child) { + $this->walk_html_node($child, $blocks, $ctx); + } + } + + private function node_plain_text($node) + { + $txt = html_entity_decode($node->textContent, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $txt = preg_replace('/\s+/', ' ', (string)$txt); + return trim($txt); + } + + private function node_direct_text($node) + { + $parts = []; + foreach ($node->childNodes as $child) { + if ($child->nodeType === XML_TEXT_NODE) { + $t = trim(preg_replace('/\s+/', ' ', html_entity_decode($child->nodeValue, ENT_QUOTES | ENT_HTML5, 'UTF-8'))); + if ($t !== '') { + $parts[] = $t; + } + } + if ($child->nodeType === XML_ELEMENT_NODE && strtolower($child->nodeName) === 'strong') { + $t = $this->node_plain_text($child); + if ($t !== '') { + $parts[] = $t; + } + } + } + return trim(implode(' ', $parts)); + } + + private function node_has_tag($node, $tagName) + { + if (!$node || !$node->hasChildNodes()) { + return false; + } + foreach ($node->childNodes as $child) { + if ($child->nodeType === XML_ELEMENT_NODE && strtolower($child->nodeName) === strtolower($tagName)) { + return true; + } + if ($this->node_has_tag($child, $tagName)) { + return true; + } + } + return false; + } + + private function measure_block_height($pdf, $block, $baseX, $baseW, $lineH) + { + $indent = isset($block['indent']) ? floatval($block['indent']) : 0; + $text = isset($block['text']) ? (string)$block['text'] : ''; + $w = max(20, $baseW - $indent); + $txt = $this->safe_text($text); + $lines = $this->nb_lines($pdf, $w, $txt); + if ($lines < 1) { + $lines = 1; + } + return $lines * $lineH; + } + + private function nb_lines($pdf, $w, $txt) + { + $text = (string)$txt; + if ($text === '') { + return 1; + } + $maxWidth = max(1.0, $w - 1.0); + $paragraphs = explode("\n", str_replace("\r", '', $text)); + $lines = 0; + + foreach ($paragraphs as $p) { + $p = trim($p); + if ($p === '') { + $lines++; + continue; + } + $words = preg_split('/\s+/', $p); + $line = ''; + foreach ($words as $word) { + $candidate = ($line === '') ? $word : ($line . ' ' . $word); + if ($pdf->GetStringWidth($candidate) <= $maxWidth) { + $line = $candidate; + } else { + if ($line !== '') { + $lines++; + $line = $word; + } else { + $lines++; + $line = ''; + } + } + } + if ($line !== '') { + $lines++; + } + } + return max(1, $lines); + } + + private function render_block($pdf, $block, $baseX, $baseW, $lineH) + { + $indent = isset($block['indent']) ? floatval($block['indent']) : 0; + $text = isset($block['text']) ? (string)$block['text'] : ''; + $style = isset($block['style']) ? (string)$block['style'] : ''; + $w = max(20, $baseW - $indent); + + $pdf->SetFont('Arial', $style, 8); + $pdf->SetX($baseX + $indent); + $pdf->MultiCell($w, $lineH, $this->safe_text($text), 0, 'L'); + } + + private function safe_text($text) + { + $t = (string)$text; + return iconv('UTF-8', 'windows-1252//TRANSLIT', $t); + } + + private function draw_full_name_block($pdf, $x, $yBottom, $w, $name) + { + $text = $this->safe_text($name); + $pdf->SetFont('Arial', '', 9); + $lines = $this->wrap_text_lines($pdf, $text, $w); + if (empty($lines)) { + $lines = ['-']; + } + + $lineH = 4.6; + $blockH = count($lines) * $lineH; + $topY = $yBottom - $blockH + 5.5; + if ($topY < 10) { + $topY = 10; + } + + $pdf->SetXY($x, $topY); + foreach ($lines as $i => $line) { + $pdf->Cell($w, $lineH, $line, 0, 2, 'L'); + } + } + + private function wrap_text_lines($pdf, $text, $w) + { + $words = preg_split('/\s+/', trim((string)$text)); + $lines = []; + $line = ''; + foreach ($words as $word) { + if ($word === '') { + continue; + } + $try = ($line === '') ? $word : ($line . ' ' . $word); + if ($pdf->GetStringWidth($try) <= $w) { + $line = $try; + } else { + if ($line !== '') { + $lines[] = $line; + } + $line = $word; + } + } + if ($line !== '') { + $lines[] = $line; + } + return $lines; + } + + private function resolve_signature_path($signatureUrl) + { + $url = trim((string)$signatureUrl); + if ($url === '') { + return null; + } + + if (preg_match('#^https?://#i', $url)) { + $tmp = sys_get_temp_dir() . '/sig_' . md5($url) . '.png'; + if (!is_file($tmp)) { + $ctx = stream_context_create([ + 'http' => ['timeout' => 3], + 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false] + ]); + $bin = @file_get_contents($url, false, $ctx); + if ($bin !== false && strlen($bin) > 0) { + @file_put_contents($tmp, $bin); + } + } + return is_file($tmp) ? $tmp : null; + } + + if (strpos($url, '/') === 0) { + if (strpos($url, '/one-media/') === 0) { + $absMedia = '/home/one/project' . $url; + if (is_file($absMedia)) { + return $absMedia; + } + $httpUrl = 'http://devone.aplikasi.web.id' . $url; + return $this->resolve_signature_path($httpUrl); + } + + $abs = FCPATH . ltrim($url, '/'); + if (is_file($abs)) { + return $abs; + } + } + + $abs2 = FCPATH . 'assets/uploads/signature/' . ltrim($url, '/'); + if (is_file($abs2)) { + return $abs2; + } + + return is_file($url) ? $url : null; + } +} diff --git a/application/controllers/tools/Inform_consent_cpmi.php b/application/controllers/tools/Inform_consent_cpmi.php new file mode 100644 index 00000000..15a4dfd1 --- /dev/null +++ b/application/controllers/tools/Inform_consent_cpmi.php @@ -0,0 +1,365 @@ +db_onedev = $this->load->database('onedev', true); + } + + public function index() + { + $this->sys_ok([ + 'message' => 'Use /tools/inform_consent_cpmi/pdf?T_OrderHeaderID=' + ]); + } + + public function pdf() + { + try { + $orderHeaderId = intval($this->input->get('T_OrderHeaderID', true)); + if ($orderHeaderId <= 0) { + $orderHeaderId = intval($this->input->get('PID', true)); + } + if ($orderHeaderId <= 0) { + $this->sys_error('T_OrderHeaderID mandatory'); + return; + } + + $patient = $this->get_patient_data($orderHeaderId); + if (!$patient) { + $this->sys_error('Order not found'); + return; + } + + $content = $this->get_consent_content(); + $printBy = trim((string)$this->input->get('username', true)); + if ($printBy === '') { + $printBy = 'ADMIN'; + } + $printDate = date('M j, Y g:i A'); + + require_once APPPATH . 'third_party/fpdf/fpdf.php'; + $pdf = new FPDF('P', 'mm', 'A4'); + $pdf->SetAutoPageBreak(false); + $pdf->SetMargins(10, 10, 10); + + $this->render_report($pdf, $patient, $content, $printBy, $printDate); + + header('Content-Type: application/pdf'); + header('Content-Disposition: inline; filename="inform_consent_cpmi_' . $orderHeaderId . '.pdf"'); + echo $pdf->Output('S'); + } catch (Exception $e) { + $this->sys_error($e->getMessage()); + } + } + + private function get_patient_data($orderHeaderId) + { + $sql = "SELECT + h.T_OrderHeaderID, + DATE_FORMAT(h.T_OrderHeaderDate, '%d-%m-%Y') as tanggal, + IFNULL(h.T_OrderHeaderM_PatientAge, '') as age, + p.M_PatientID, + CONCAT(TRIM(IFNULL(t.M_TitleName,'')), ' ', TRIM(IFNULL(p.M_PatientPrefix,'')), ' ', TRIM(IFNULL(p.M_PatientName,'')), ' ', TRIM(IFNULL(p.M_PatientSuffix,''))) AS patient_name, + IFNULL(s.M_SexCode, '') as sex_code, + DATE_FORMAT(p.M_PatientDOB, '%d-%m-%Y') as dob, + IFNULL(pa.M_PatientAddressDescription,'') as alamat, + ( + SELECT ps.Patient_SignatureUrl + FROM patient_signature ps + WHERE ps.Patient_SignatureM_PatientID = p.M_PatientID + AND ps.Patient_SignatureIsActive = 'Y' + ORDER BY ps.Patient_SignatureID DESC + LIMIT 1 + ) AS signature_url + FROM t_orderheader h + JOIN m_patient p ON p.M_PatientID = h.T_OrderHeaderM_PatientID + LEFT JOIN m_title t ON t.M_TitleID = p.M_PatientM_TitleID + LEFT JOIN m_sex s ON s.M_SexID = p.M_PatientM_SexID + LEFT JOIN m_patientaddress pa ON pa.M_PatientAddressM_PatientID = p.M_PatientID + AND pa.M_PatientAddressNote = 'Utama' + AND pa.M_PatientAddressIsActive = 'Y' + WHERE h.T_OrderHeaderID = ? + LIMIT 1"; + $qry = $this->db_onedev->query($sql, [$orderHeaderId]); + return $qry ? $qry->row_array() : null; + } + + private function get_consent_content() + { + $sql = "SELECT M_InformConsentContent + FROM m_informconsent + WHERE M_InformConsentType = 'cpmi' AND M_InformConsentIsActive = 'Y' + LIMIT 1"; + $qry = $this->db_onedev->query($sql); + if (!$qry || !$qry->row_array()) { + return ''; + } + return (string)$qry->row_array()['M_InformConsentContent']; + } + + private function render_report($pdf, $patient, $contentHtml, $printBy, $printDate) + { + $pdf->AddPage(); + + $logoPath = APPPATH . '../assets/images/logo-ibl-round.png'; + if (is_file($logoPath)) { + $pdf->Image($logoPath, 14, 21, 22); + } + + $pdf->SetFont('Times', 'B', 17); + $pdf->SetXY(40, 18); + $pdf->Cell(70, 8, 'KLINIK UTAMA', 0, 1, 'L'); + + $pdf->SetFont('Times', 'BI', 18); + $pdf->SetXY(40, 26); + $pdf->Cell(70, 8, 'IMAM', 0, 1, 'L'); + $pdf->SetXY(40, 34); + $pdf->Cell(70, 8, 'BONJOL', 0, 1, 'L'); + $pdf->SetFont('Arial', '', 9); + $pdf->SetXY(40, 42); + $pdf->Cell(70, 6, 'Jl. Imam Bonjol 173 Semarang', 0, 1, 'L'); + + $pdf->SetFont('Times', '', 10); + $pdf->SetXY(140, 12); + $pdf->Cell(55, 6, 'FR/IBL-IB/Lab/PM-4.2/02', 0, 1, 'R'); + $pdf->SetFont('Arial', '', 11); + $pdf->SetXY(98, 32); + $pdf->MultiCell(80, 6, "GENERAL CONSENT\nMEDICAL TEST CPMI", 0, 'L'); + + $y = 56; + $pdf->SetFont('Arial', '', 9); + $pdf->SetXY(14, $y); + $pdf->Cell(80, 5, 'Saya yang bertanda tangan dibawah ini :', 0, 1, 'L'); + $y += 8; + + $y = $this->line_form($pdf, $y, 'Nama lengkap', trim(preg_replace('/\s+/', ' ', $patient['patient_name']))); + $y = $this->line_form($pdf, $y, 'Bin / Binti', '...............................................................'); + $ttlUsia = trim($patient['dob']) . ' / ' . trim((string)$patient['age']); + $y = $this->line_form($pdf, $y, 'Tanggal lahir / Usia', $ttlUsia . ' tahun'); + $y = $this->line_form($pdf, $y, 'Status', 'Kawin / Tidak Kawin'); + $y = $this->line_form($pdf, $y, 'Alamat', $patient['alamat']); + + $y += 2; + $blocks = $this->html_to_blocks($contentHtml); + $pdf->SetFont('Arial', '', 8.2); + $lineH = 5.2; + foreach ($blocks as $b) { + $indent = (float)$b['indent']; + $style = (string)$b['style']; + $text = (string)$b['text']; + $w = max(20, 182 - $indent); + if ($y > 228) { + break; + } + $pdf->SetFont('Arial', $style, 8.2); + $pdf->SetXY(14 + $indent, $y); + $pdf->MultiCell($w, $lineH, $this->safe_text($text), 0, 'L'); + $y = $pdf->GetY(); + } + + $y += 3; + $pdf->SetFont('Arial', '', 8); + $pdf->SetXY(104, $y - 4); + $pdf->Cell(90, 5, 'Semarang, ' . $patient['tanggal'], 0, 1, 'R'); + $pdf->SetXY(14, $y); + $pdf->Cell(90, 5, 'Petugas yang memberi penjelasan', 0, 0, 'L'); + $pdf->Cell(90, 5, 'Yang membuat pernyataan', 0, 1, 'R'); + $y += 12; + $pdf->SetXY(14, $y); + $pdf->Cell(90, 5, '( ................................ )', 0, 0, 'L'); + $signaturePath = $this->resolve_signature_path(isset($patient['signature_url']) ? $patient['signature_url'] : ''); + if ($signaturePath !== null) { + $pdf->Image($signaturePath, 156, $y - 8, 34, 10); + } + $pdf->SetFont('Arial', 'U', 8.5); + $pdf->SetXY(149, $y); + $pdf->Cell(54, 5, $this->safe_text(trim(preg_replace('/\s+/', ' ', $patient['patient_name']))), 0, 1, 'C'); + + $y += 8; + $pdf->Line(14, $y, 196, $y); + $y += 4; + $pdf->SetFont('Arial', '', 10); + $pdf->SetXY(14, $y); + $pdf->Cell(182, 5, 'CHECK LIST PEMERIKSAAN', 0, 1, 'C'); + + $y += 6; + $labelsPage1 = ['Online Sidik Jari', 'Foto', 'Fisik', 'Darah + Urine', 'Feses', 'Sputum', 'Radiologi']; + $this->draw_checklist_table($pdf, 26, $y, $labelsPage1); + + $pdf->SetFont('Times', '', 7); + $pdf->SetXY(14, 286); + $pdf->Cell(22, 4, 'Print Oleh :', 0, 0, 'L'); + $pdf->Cell(50, 4, $this->safe_text($printBy), 0, 0, 'L'); + $pdf->Cell(12, 4, '1', 0, 0, 'C'); + $pdf->Cell(6, 4, '/', 0, 0, 'C'); + $pdf->Cell(12, 4, '2', 0, 0, 'C'); + $date = $this->safe_text($printDate); + $dateW = $pdf->GetStringWidth($date); + $xDate = max(120, 196 - $dateW); + $pdf->SetXY($xDate, 286); + $pdf->Cell($dateW + 1, 4, $date, 0, 1, 'L'); + + $this->render_second_checklist_page($pdf, $printBy, $printDate); + } + + private function render_second_checklist_page($pdf, $printBy, $printDate) + { + $pdf->AddPage(); + $pdf->SetFont('Arial', '', 10); + $pdf->SetXY(140, 12); + $pdf->Cell(55, 6, 'FR/IBL-IB/Lab/PM-4.2/02', 0, 1, 'R'); + + $labelsPage2 = ['EKG', 'Audiometri', 'Lain-Lain', 'Cetak Hasil', 'Cek I', 'Cek II', 'Revisi']; + $this->draw_checklist_table($pdf, 26, 30, $labelsPage2); + + $pdf->SetFont('Times', '', 7); + $pdf->SetXY(14, 286); + $pdf->Cell(22, 4, 'Print Oleh :', 0, 0, 'L'); + $pdf->Cell(50, 4, $this->safe_text($printBy), 0, 0, 'L'); + $pdf->Cell(12, 4, '2', 0, 0, 'C'); + $pdf->Cell(6, 4, '/', 0, 0, 'C'); + $pdf->Cell(12, 4, '2', 0, 0, 'C'); + $date = $this->safe_text($printDate); + $dateW = $pdf->GetStringWidth($date); + $xDate = max(120, 196 - $dateW); + $pdf->SetXY($xDate, 286); + $pdf->Cell($dateW + 1, 4, $date, 0, 1, 'L'); + } + + private function draw_checklist_table($pdf, $xStart, $yStart, $labels) + { + $cols = count($labels); + if ($cols < 1) { + return; + } + $tableW = 156; + $colW = $tableW / $cols; + $topH = 11; + $bottomH = 10; + + for ($i = 0; $i < $cols; $i++) { + $x = $xStart + ($i * $colW); + $pdf->Rect($x, $yStart, $colW, $topH); + $pdf->Rect($x, $yStart + $topH, $colW, $bottomH); + $pdf->Rect($x + 1, $yStart + 1, 4, 4); + + $label = $labels[$i]; + if ($label === 'Online Sidik Jari') { + $label = "Online\nSidik Jari"; + } elseif ($label === 'Darah + Urine') { + $label = "Darah +\nUrine"; + } + $pdf->SetFont('Arial', '', 8); + $pdf->SetXY($x + 5.5, $yStart + 1.5); + $pdf->MultiCell($colW - 6, 4, $this->safe_text($label), 0, 'C'); + } + } + + private function line_form($pdf, $y, $label, $value) + { + $pdf->SetFont('Arial', '', 9); + $pdf->SetXY(16, $y); + $pdf->Cell(28, 5, $this->safe_text($label), 0, 0, 'L'); + $pdf->Cell(3, 5, ':', 0, 0, 'C'); + $pdf->SetXY(47, $y); + $pdf->MultiCell(147, 5, $this->safe_text($value), 0, 'L'); + return $pdf->GetY() + 1; + } + + private function html_to_blocks($html) + { + $input = (string)$html; + $input = str_replace(['\\r\\n', '\\n', '\\r'], "\n", $input); + $input = str_replace(["\r\n", "\r"], "\n", $input); + + $blocks = []; + $s = preg_replace('//i', "\n", $input); + $s = preg_replace('/<\/(div|p|h[1-6]|li)>/i', "$0\n", $s); + preg_match_all('/<(h[1-6]|p|li)[^>]*>(.*?)<\/\1>/is', $s, $matches, PREG_SET_ORDER); + foreach ($matches as $m) { + $tag = strtolower($m[1]); + $inner = preg_replace('/<(ul|ol)[^>]*>.*?<\/\1>/is', '', (string)$m[2]); + $plain = trim(preg_replace('/\s+/', ' ', html_entity_decode(strip_tags($inner), ENT_QUOTES | ENT_HTML5, 'UTF-8'))); + if ($plain === '' || $plain === ':') { + continue; + } + $indent = 0; + $style = ''; + $text = $plain; + + if ($tag === 'h1' || $tag === 'h2' || $tag === 'h3' || $tag === 'h4' || $tag === 'h5' || $tag === 'h6') { + $style = 'B'; + } elseif ($tag === 'li') { + if (preg_match('/^[0-9]+\./', $plain) || preg_match('/^[IVX]+\./i', $plain)) { + $text = $plain; + $indent = 0; + } else { + $text = '• ' . $plain; + $indent = 5; + } + } elseif ($tag === 'p' && (stripos((string)$m[2], ' $text, + 'indent' => $indent, + 'style' => $style + ]; + } + return $blocks; + } + + private function safe_text($text) + { + return iconv('UTF-8', 'windows-1252//TRANSLIT', (string)$text); + } + + private function resolve_signature_path($signatureUrl) + { + $url = trim((string)$signatureUrl); + if ($url === '') { + return null; + } + + if (preg_match('#^https?://#i', $url)) { + $tmp = sys_get_temp_dir() . '/sig_' . md5($url) . '.png'; + if (!is_file($tmp)) { + $ctx = stream_context_create([ + 'http' => ['timeout' => 3], + 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false] + ]); + $bin = @file_get_contents($url, false, $ctx); + if ($bin !== false && strlen($bin) > 0) { + @file_put_contents($tmp, $bin); + } + } + return is_file($tmp) ? $tmp : null; + } + + if (strpos($url, '/one-media/') === 0) { + $absMedia = '/home/one/project' . $url; + if (is_file($absMedia)) { + return $absMedia; + } + return $this->resolve_signature_path('http://devone.aplikasi.web.id' . $url); + } + + if (strpos($url, '/') === 0) { + $abs = FCPATH . ltrim($url, '/'); + if (is_file($abs)) { + return $abs; + } + } + + $abs2 = FCPATH . 'assets/uploads/signature/' . ltrim($url, '/'); + if (is_file($abs2)) { + return $abs2; + } + + return is_file($url) ? $url : null; + } +} diff --git a/application/controllers/tools/Medical_checkup_report.php b/application/controllers/tools/Medical_checkup_report.php index 6b214cb5..587271ef 100644 --- a/application/controllers/tools/Medical_checkup_report.php +++ b/application/controllers/tools/Medical_checkup_report.php @@ -567,6 +567,8 @@ class Medical_checkup_report extends MY_Controller $available = $pdf->GetPageHeight() - $bottomMargin - $pdf->GetY(); if ($available < $neededHeight) { $pdf->AddPage(); + // Samakan jarak awal konten di halaman lanjutan. + $pdf->SetY(35); } } diff --git a/assets/images/logo-ibl-round.png b/assets/images/logo-ibl-round.png new file mode 100644 index 00000000..76bcd9b2 Binary files /dev/null and b/assets/images/logo-ibl-round.png differ diff --git a/assets/images/logo-ibl.png b/assets/images/logo-ibl.png new file mode 100644 index 00000000..ecbc7766 Binary files /dev/null and b/assets/images/logo-ibl.png differ