configModel = new ConfigurationModel(); $this->semester = $this->configModel->getConfig('semester'); $this->schoolYear = $this->configModel->getConfig('school_year'); } /** * POST /slips/print * Expected fields (POST): school_year, student_name, date, time_in, grade, reason, admin_name * You can wire this to your own form/data source; all fields are strings. */ public function print(): ResponseInterface { $req = $this->request; $data = [ 'school_year' => trim((string) $req->getVar('school_year') ?: ''), 'student_name' => trim((string) $req->getVar('student_name') ?: ''), 'date' => trim((string) $req->getVar('date') ?: date('m/d/Y')), 'time_in' => trim((string) $req->getVar('time_in') ?: date('h:i A')), 'grade' => trim((string) $req->getVar('grade') ?: ''), 'reason' => trim((string) $req->getVar('reason') ?: ''), 'admin_name' => trim((string) $req->getVar('admin_name') ?: ''), ]; // Always prefer current logged-in user's full name from DB $adminFromDb = $this->currentAdminName(); if ($adminFromDb !== '') { $data['admin_name'] = $adminFromDb; } // Validate minimal fields if ($data['student_name'] === '') { return $this->response->setStatusCode(422)->setJSON(['error' => 'Student name is required.']); } try { // Persist a log (best-effort) before generating PDF try { $printedBy = (int) (session()->get('user_id') ?? 0) ?: null; $cfg = new ConfigurationModel(); $semester = (string)($cfg->getConfig('semester') ?? ''); $logData = [ 'school_year' => $data['school_year'], 'semester' => $semester, 'student_name' => $data['student_name'], 'slip_date' => $this->toDbDate($data['date']), 'time_in' => $this->toDbTime($data['time_in']), 'grade' => $data['grade'], 'reason' => $data['reason'], 'admin_name' => $data['admin_name'], ]; (new LateSlipLogModel())->logSlip($logData, $printedBy); } catch (\Throwable $e) { log_message('error', 'Late slip log insert failed: ' . $e->getMessage()); } // Generate and stream a PDF instead of direct printer output return $this->renderSlipPdfResponse($data); } catch (\Throwable $e) { log_message('error', 'Late slip PDF generation failed: ' . $e->getMessage()); return $this->response->setStatusCode(500)->setJSON([ 'error' => 'Printing failed.', 'details' => $e->getMessage(), ]); } } /** * Show or print preview of late slips. */ public function preview(): ResponseInterface { $req = $this->request; try { // ---- Step 1: Resolve current school year ---- $currentYear = trim((string) ($this->schoolYear ?? '')); if ($currentYear === '') { $latest = (new LateSlipLogModel()) ->select('school_year') ->orderBy('id', 'DESC') ->first(); $currentYear = $latest['school_year'] ?? (date('Y') . '-' . (date('Y') + 1)); } $selectedYear = trim((string) $req->getVar('school_year') ?: $currentYear); $selectedSemester = strtolower(trim((string) $req->getVar('semester') ?: '')); $data = [ 'school_year' => $selectedYear, 'student_name' => trim((string) $req->getVar('student_name') ?: ''), 'date' => trim((string) $req->getVar('date') ?: date('m/d/Y')), 'time_in' => trim((string) $req->getVar('time_in') ?: date('h:i A')), 'grade' => trim((string) $req->getVar('grade') ?: ''), 'reason' => trim((string) $req->getVar('reason') ?: ''), 'admin_name' => trim((string) $req->getVar('admin_name') ?: ''), ]; // Prefer logged-in admin name $adminFromDb = $this->currentAdminName(); if ($adminFromDb !== '') { $data['admin_name'] = $adminFromDb; } // ---- Handle GET (HTML preview) ---- if (strtolower($req->getMethod()) === 'get') { $rows = []; $logModel = new LateSlipLogModel(); $builder = $logModel; // Filter by school year if not empty if ($selectedYear !== '') { $builder->where('school_year', $selectedYear); } // Normalize and apply semester filter $hasFilter = false; if ($selectedSemester !== '') { $hasFilter = true; if ($selectedSemester === 'fall') { $builder->groupStart() ->where('semester', 'fall') ->orWhere('semester', '1') ->orWhere('semester', 1) ->groupEnd(); } elseif ($selectedSemester === 'spring') { $builder->groupStart() ->where('semester', 'spring') ->orWhere('semester', '2') ->orWhere('semester', 2) ->groupEnd(); } } // ---- Execute query ---- $logs = $builder->orderBy('id', 'DESC')->findAll(50); // 🟢 Only use fallback when *no filters are selected* if (empty($logs) && !$hasFilter && $selectedYear === '') { $logs = (new LateSlipLogModel())->orderBy('id', 'DESC')->findAll(50); } // ---- Format rows ---- foreach ($logs as $r) { $dispDate = ''; $slipDateRaw = trim((string)($r['slip_date'] ?? '')); if ($slipDateRaw !== '' && $slipDateRaw !== '0000-00-00') { $ts = strtotime($slipDateRaw); $dispDate = $ts ? date('m/d/Y', $ts) : ''; } if ($dispDate === '') { $printedAt = trim((string)($r['printed_at'] ?? '')); if ($printedAt !== '') { try { $dispDate = local_date($printedAt, 'm/d/Y'); } catch (\Throwable $e) { $ts = strtotime($printedAt); $dispDate = $ts ? date('m/d/Y', $ts) : ''; } } } $dispTime = ''; if (!empty($r['time_in'])) { $t = strtotime($r['time_in']) ?: strtotime('today ' . $r['time_in']); $dispTime = $t ? date('h:i A', $t) : ''; } $rows[] = [ 'school_year' => (string)($r['school_year'] ?? ''), 'semester' => (string)($r['semester'] ?? ''), 'student_name' => (string)($r['student_name'] ?? ''), 'date' => $dispDate, 'time_in' => $dispTime, 'grade' => (string)($r['grade'] ?? ''), 'reason' => (string)($r['reason'] ?? ''), 'admin_name' => (string)($r['admin_name'] ?? ''), ]; } // ---- Render the view ---- $html = view('slips/preview_list', [ 'rows' => $rows, 'school_year' => $selectedYear, 'semester' => $selectedSemester, ]); return $this->response ->setStatusCode(200) ->setHeader('Content-Type', 'text/html; charset=UTF-8') ->setBody($html); } // ---- POST mode (JSON preview for printer) ---- $cfg = $this->printerConfig(); $lineWidth = (int) ($cfg['chars_per_line'] ?? 48); $feedLines = (int) ($cfg['feed_lines'] ?? 3); $header = "Al Rahma School at ISGL\nSchool Year: {$data['school_year']}\n\n"; $body = implode("\n", $this->buildSlipLines($data, $lineWidth)); $footer = str_repeat("\n", $feedLines); $full = $header . $body . $footer; return $this->response->setJSON([ 'ok' => true, 'text' => $full, 'width' => $lineWidth, ]); } catch (\Throwable $e) { log_message('error', 'Late slip preview failed: ' . $e->getMessage()); return $this->response->setStatusCode(500)->setJSON([ 'ok' => false, 'error' => 'Preview failed.', 'details' => $e->getMessage(), ]); } } /** * POST /administrator/late_slips/reprint/{id} * Reprint a late slip from a saved log row, then redirect back with status. */ public function reprint($id = null): ResponseInterface { $id = (int) ($id ?? 0); if ($id <= 0) { return redirect()->back()->with('error', 'Invalid slip id.'); } $row = null; try { $row = (new LateSlipLogModel())->find($id); } catch (\Throwable $e) { $row = null; } if (!$row) { return redirect()->back()->with('error', 'Slip not found.'); } // Map DB row to print payload $dateDisplay = ''; if (!empty($row['slip_date'])) { $ts = strtotime($row['slip_date']); $dateDisplay = $ts ? date('m/d/Y', $ts) : ''; } if ($dateDisplay === '') $dateDisplay = date('m/d/Y'); $timeDisplay = ''; if (!empty($row['time_in'])) { $ts = strtotime($row['time_in']); if ($ts === false) { // Try combining with today $ts = strtotime('today ' . $row['time_in']); } $timeDisplay = $ts ? date('h:i A', $ts) : ''; } if ($timeDisplay === '') $timeDisplay = date('h:i A'); $data = [ 'school_year' => (string) ($row['school_year'] ?? ''), 'student_name' => (string) ($row['student_name'] ?? ''), 'date' => $dateDisplay, 'time_in' => $timeDisplay, 'grade' => (string) ($row['grade'] ?? ''), 'reason' => (string) ($row['reason'] ?? ''), 'admin_name' => (string) ($row['admin_name'] ?? ''), ]; // Always prefer current logged-in user's full name from DB $adminFromDb = $this->currentAdminName(); if ($adminFromDb !== '') { $data['admin_name'] = $adminFromDb; } try { // Format lines and print $lines = $this->buildSlipLines($data); $printer = $this->makePrinter(); $printer->setJustification(Printer::JUSTIFY_CENTER); $printer->setEmphasis(true); $printer->text("Al Rahma School at ISGL\n"); $printer->setEmphasis(false); $printer->text("School Year: {$data['school_year']}\n"); $printer->feed(); $printer->setJustification(Printer::JUSTIFY_LEFT); foreach ($lines as $ln) { $printer->text($ln . "\n"); } $printer->feed((int) ($this->printerConfig()['feed_lines'] ?? 3)); $printer->cut(); $printer->close(); // Log again (best-effort) for audit trail try { $printedBy = (int) (session()->get('user_id') ?? 0) ?: null; $cfg = new ConfigurationModel(); $semester = (string)($cfg->getConfig('semester') ?? ''); $logData = [ 'school_year' => $data['school_year'], 'semester' => $semester, 'student_name' => $data['student_name'], 'slip_date' => $this->toDbDate($data['date']), 'time_in' => $this->toDbTime($data['time_in']), 'grade' => $data['grade'], 'reason' => $data['reason'], 'admin_name' => $data['admin_name'], ]; (new LateSlipLogModel())->logSlip($logData, $printedBy); } catch (\Throwable $e) { log_message('error', 'Late slip reprint log insert failed: ' . $e->getMessage()); } return redirect()->back()->with('success', 'Late slip sent to printer.'); } catch (\Throwable $e) { return redirect()->back()->with('error', 'Reprint failed: ' . $e->getMessage()); } } /** * Build monospaced slip lines for an 80mm (typically 48 chars) receipt. * * Layout required: * Al Rahma School at ISGL "SchoolYear" * Student Name ___________________________ * Date ______________ Time In ______________ * Grade _________________________________ * Reason _________________________________ * _______________________________________ * Admin Name ____________________________ */ private function buildSlipLines(array $data, ?int $lineWidth = null): array { // Choose a sane default for 58mm rolls (~24–32 chars). Fall back to env config. $cfgWidth = (int) ($this->printerConfig()['chars_per_line'] ?? 0); $w = $lineWidth ?? ($cfgWidth > 0 ? $cfgWidth : 32); $lines = []; $lines[] = $this->fitLine('Student Name: ' . (string)($data['student_name'] ?? ''), $w); $lines[] = $this->fitLine('Date: ' . (string)($data['date'] ?? '') . ' ' . 'Time In: ' . (string)($data['time_in'] ?? ''), $w); $lines[] = $this->fitLine('Grade: ' . (string)($data['grade'] ?? ''), $w); $lines[] = $this->fitLine('Reason: ' . (string)($data['reason'] ?? ''), $w); $lines[] = $this->fitLine('Admin: ' . (string)($data['admin_name'] ?? ''), $w); return $lines; } /** * Create and return a configured Mike42\Escpos\Printer instance. */ private function makePrinter(): Printer { $cfg = $this->printerConfig(); $mode = strtolower((string) ($cfg['mode'] ?? 'network')); if ($mode === 'windows') { if (stripos(PHP_OS_FAMILY, 'Windows') !== false) { $name = (string) ($cfg['windows_name'] ?? 'POS-80'); $connector = new \Mike42\Escpos\PrintConnectors\WindowsPrintConnector($name); return new Printer($connector); } log_message('warning', 'PRINTER_MODE=windows on non-Windows OS; falling back to network.'); $mode = 'network'; } if ($mode === 'usb') { if (stripos(PHP_OS_FAMILY, 'Windows') !== false) { $name = (string) ($cfg['windows_name'] ?? 'POS-80'); $connector = new \Mike42\Escpos\PrintConnectors\WindowsPrintConnector($name); return new Printer($connector); } $vidStr = (string) ($cfg['usb_vid'] ?? ''); $pidStr = (string) ($cfg['usb_pid'] ?? ''); if ($vidStr === '' || $pidStr === '') { throw new \RuntimeException('USB mode requires PRINTER_USB_VID and PRINTER_USB_PID'); } $vid = $this->hexToInt($vidStr); $pid = $this->hexToInt($pidStr); $connector = new \Mike42\Escpos\PrintConnectors\UsbPrintConnector($vid, $pid); return new Printer($connector); } // Default: network $host = (string) ($cfg['host'] ?? '192.168.1.100'); $port = (int) ($cfg['port'] ?? 9100); $connector = new \Mike42\Escpos\PrintConnectors\NetworkPrintConnector($host, $port); return new Printer($connector); } /** * Build a "Label: ________VALUE" style line, using underscores to fill remaining space. * If $value is empty, just render a long blank line after the label. */ private function labelWithLine(string $label, string $value, int $width, ?int $fillCount = null): string { $label = rtrim($label, ':') . ': '; $maxFill = $fillCount ?? max(0, $width - strlen($label)); $filled = $value !== '' ? $this->fixedField($value, $maxFill, '_') : str_repeat('_', $maxFill); $line = $label . $filled; // Ensure total width, pad or trim return $this->fitLine($line, $width); } /** * Create a single field like "Date: ________" (or with value). */ private function fieldWithBlanks(string $label, string $value, int $fieldWidth): string { $label = rtrim($label, ':') . ': '; $fill = max(0, $fieldWidth - strlen($label)); $content = $value !== '' ? $this->fixedField($value, $fill, '_') : str_repeat('_', $fill); return $this->fitLine($label . $content, $fieldWidth); } /** * Join two fixed-width fields into one line (e.g., Date field + Time In field). */ private function joinTwoFields(string $left, string $right, int $totalWidth): string { $gap = ' '; $line = $left . $gap . $right; return $this->fitLine($line, $totalWidth); } /** * Pad/truncate a value to an exact width using a fill character (for underlines). */ private function fixedField(string $val, int $width, string $fillChar = '_'): string { $val = (string) $val; if (strlen($val) > $width) { return substr($val, 0, $width); } return $val . str_repeat($fillChar, max(0, $width - strlen($val))); } /** * Ensure any line is exactly $width characters (trim or pad spaces). */ private function fitLine(string $line, int $width): string { $line = (string) $line; if (strlen($line) > $width) { return substr($line, 0, $width); } return $line . str_repeat(' ', $width - strlen($line)); } /** * "0x1a86" -> 0x1a86 (int). Accepts plain decimal too. */ private function hexToInt(string $hexOrDec): int { $hexOrDec = trim($hexOrDec); if ($hexOrDec === '') return 0; if (stripos($hexOrDec, '0x') === 0) { return intval($hexOrDec, 16); } return (int) $hexOrDec; } /** * Consolidated printer configuration. Supports both dotted keys (printer.*) * and uppercase PRINTER_* keys from .env. */ private function printerConfig(): array { $get = function ($keys, $default = null) { $keys = is_array($keys) ? $keys : [$keys]; foreach ($keys as $k) { $v = env($k); if ($v !== null && $v !== '') return $v; } return $default; }; return [ 'mode' => strtolower((string) $get(['printer.mode', 'PRINTER_MODE'], 'network')), 'host' => (string) $get(['printer.host', 'PRINTER_HOST'], '192.168.1.100'), 'port' => (int) $get(['printer.port', 'PRINTER_PORT'], 9100), 'usb_vid' => (string) $get(['printer.usb.vid', 'PRINTER_USB_VID'], ''), 'usb_pid' => (string) $get(['printer.usb.pid', 'PRINTER_USB_PID'], ''), 'windows_name' => (string) $get(['printer.windows.name', 'PRINTER_WINDOWS_NAME'], 'POS-80'), 'chars_per_line' => (int) $get(['printer.chars_per_line', 'PRINTER_CHARS_PER_LINE'], 48), 'feed_lines' => (int) $get(['printer.feed_lines', 'PRINTER_FEED_LINES'], 3), ]; } /** * Convert user-provided date/time strings into DB formats. */ private function toDbDate(?string $s): ?string { $s = trim((string)$s); if ($s === '') return null; // Explicitly parse common US formats to avoid day/month swaps. if (preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $s)) { return $s; } if (preg_match('/^\\d{1,2}\\/\\d{1,2}\\/\\d{4}$/', $s)) { $dt = \DateTime::createFromFormat('!m/d/Y', $s); return $dt ? $dt->format('Y-m-d') : null; } if (preg_match('/^\\d{1,2}-\\d{1,2}-\\d{4}$/', $s)) { $dt = \DateTime::createFromFormat('!m-d-Y', $s); return $dt ? $dt->format('Y-m-d') : null; } $ts = strtotime($s); return $ts ? date('Y-m-d', $ts) : null; } private function toDbTime(?string $s): ?string { $s = trim((string)$s); if ($s === '') return null; $ts = strtotime($s); return $ts ? date('H:i:s', $ts) : null; } /** * Compute current admin full name using session user_id and users table. * Falls back to session firstname/lastname, then user_name, else ''. */ private function currentAdminName(): string { try { $uid = (int) (session()->get('user_id') ?? 0); if ($uid > 0) { $user = (new UserModel()) ->select('firstname, lastname') ->find($uid); if ($user) { $full = trim((string)($user['firstname'] ?? '') . ' ' . (string)($user['lastname'] ?? '')); if ($full !== '') return $full; } } } catch (\Throwable $e) { // ignore and fall back } $first = trim((string)(session()->get('firstname') ?? '')); $last = trim((string)(session()->get('lastname') ?? '')); $full = trim($first . ' ' . $last); if ($full !== '') return $full; return (string)(session()->get('user_name') ?? ''); } /** * Render the slip as a PDF and stream it inline to the browser. */ private function renderSlipPdfResponse(array $data): ResponseInterface { // Build HTML from a dedicated view // Determine paper size (mm) $paper = strtolower((string) ($this->request->getVar('paper') ?: env('SLIP_PAPER', 'card'))); // Estimate dynamic height based on the PDF view font and line-height // Title + seven content lines = 8 rows total // Defaults match the current view: 13pt font, 2.0 line-height $fontPt = (float) ($this->request->getVar('font_pt') ?? env('SLIP_FONT_PT', 13.0)); $lineH = (float) ($this->request->getVar('line_h') ?? env('SLIP_LINE_H', 2.0)); $linesCount = (int) ($this->request->getVar('rows') ?? 8); if ($linesCount < 1) { $linesCount = 8; } $ptPerLine = $fontPt * $lineH; $mmPerPt = 25.4 / 72.0; // 1pt in mm $contentMm = $ptPerLine * $linesCount * $mmPerPt; // content only $pageMarginsMm = 3.0 * 2; // top+bottom from CSS // Allow caller to increase safety buffer if their viewer/driver adds spacing // Slightly bigger safety buffer to avoid second-page spillovers from drivers $fudgeMm = (float) ($this->request->getVar('fudge_mm') ?? env('SLIP_FUDGE_MM', 12.0)); $dynHeightMm = $contentMm + $pageMarginsMm + $fudgeMm; switch ($paper) { case 'cardp': case 'card-portrait': $wMm = 53.98; $hMm = max($dynHeightMm, 40.0); // portrait width, dynamic height $wChars = 30; break; case 'receipt80': $wMm = 72.0; $hMm = 90.0; // 80mm roll (printable ~72mm) — taller to fit larger font $wChars = 30; break; case 'receipt58': $wMm = 58.0; $hMm = 80.0; // 58mm roll — taller to fit larger font $wChars = 24; break; case 'card': default: $wMm = 85.60; $hMm = max($dynHeightMm, 40.0); // credit card width, dynamic height $wChars = 40; } // Optional manual override via query: height_mm (or h) $hOverride = $this->request->getVar('height_mm') ?? $this->request->getVar('h'); if (is_numeric($hOverride)) { $hMm = max(30.0, min(200.0, (float) $hOverride)); } // Build exactly six lines in monospace formatting $w = $wChars; // characters per line for layout $lines = []; $lines[] = $this->fitLine('Al Rahma School at ISGL ' . ($data['school_year'] ?? ''), $w); // Helper to compose "Label: value_____" without truncating value first $compose = function (string $label, string $value, int $padUnderscore = 0) use ($w) { $prefix = rtrim($label, ':') . ': '; $line = $prefix . $value; $len = strlen($line); if ($padUnderscore > 0 && $len < $w) { $line .= str_repeat('_', min($padUnderscore, $w - $len)); } return $this->fitLine($line, $w); }; // Student Name (no trailing underscores) $lines[] = $compose('Student Name', (string)($data['student_name'] ?? ''), 0); // Date and Time In on separate lines $lines[] = $this->fitLine('Date: ' . (string)($data['date'] ?? ''), $w); $lines[] = $this->fitLine('Time In: ' . (string)($data['time_in'] ?? ''), $w); // Grade and Reason (no trailing underscores) $lines[] = $compose('Grade', (string)($data['grade'] ?? ''), 0); $lines[] = $compose('Reason', (string)($data['reason'] ?? ''), 0); // Admin: do not underline, just value $lines[] = $this->fitLine('Admin: ' . (string)($data['admin_name'] ?? ''), $w); $html = view('slips/slip_pdf', ['entry' => $data, 'lines' => $lines, 'page' => ['w_mm' => $wMm, 'h_mm' => $hMm]]); $options = new Options(); $options->set('isRemoteEnabled', true); $options->set('defaultFont', 'Courier'); $dompdf = new Dompdf($options); $dompdf->loadHtml($html, 'UTF-8'); // Ensure minimum height is enough for current content calculation if ($hMm < $dynHeightMm) { $hMm = $dynHeightMm; } // Set paper size from selected mm values -> points $mmToPt = 72 / 25.4; $wPt = $wMm * $mmToPt; $hPt = $hMm * $mmToPt; $dompdf->setPaper([0, 0, $wPt, $hPt]); $dompdf->render(); $filename = 'LateSlip_' . preg_replace('/[^A-Za-z0-9]+/', '_', (string)($data['student_name'] ?? 'slip')) . '_' . date('Ymd_His') . '.pdf'; return $this->response ->setHeader('Content-Type', 'application/pdf') ->setHeader('Content-Disposition', 'inline; filename="' . $filename . '"') ->setBody($dompdf->output()); } }