config = model(Configuration::class); $this->log = model(LateSlipLog::class); $this->user = model(User::class); $this->schoolYear = (string) ($this->config->getConfig('school_year') ?? ''); $this->semester = (string) ($this->config->getConfig('semester') ?? ''); } /** * POST /api/v1/slips/print * Generate and return a PDF late slip */ public function print(): SymfonyResponse|JsonResponse { $data = $this->payloadData(); $slipData = [ 'school_year' => trim((string) ($data['school_year'] ?? $this->schoolYear ?: '')), 'student_name' => trim((string) ($data['student_name'] ?? '')), 'date' => trim((string) ($data['date'] ?? date('m/d/Y'))), 'time_in' => trim((string) ($data['time_in'] ?? date('h:i A'))), 'grade' => trim((string) ($data['grade'] ?? '')), 'reason' => trim((string) ($data['reason'] ?? '')), 'admin_name' => trim((string) ($data['admin_name'] ?? '')), ]; // Always prefer current logged-in user's full name from DB $adminFromDb = $this->currentAdminName(); if ($adminFromDb !== '') { $slipData['admin_name'] = $adminFromDb; } // Validate minimal fields if ($slipData['student_name'] === '') { return $this->error('Student name is required.', SymfonyResponse::HTTP_UNPROCESSABLE_ENTITY); } try { // Persist a log (best-effort) before generating PDF try { $printedBy = $this->getCurrentUserId(); $logData = [ 'school_year' => $slipData['school_year'], 'semester' => $this->semester, 'student_name' => $slipData['student_name'], 'slip_date' => $this->toDbDate($slipData['date']), 'time_in' => $this->toDbTime($slipData['time_in']), 'grade' => $slipData['grade'], 'reason' => $slipData['reason'], 'admin_name' => $slipData['admin_name'], ]; $this->log->logSlip($logData, $printedBy); } catch (\Throwable $e) { Log::error('Late slip log insert failed: ' . $e->getMessage()); } // Generate and return PDF return $this->renderSlipPdfResponse($slipData); } catch (\Throwable $e) { Log::error('Late slip PDF generation failed: ' . $e->getMessage()); return $this->error('Printing failed.', SymfonyResponse::HTTP_INTERNAL_SERVER_ERROR, [ 'details' => $e->getMessage(), ]); } } /** * GET/POST /api/v1/slips/preview * Show preview of late slips (GET returns list, POST returns JSON preview) */ public function preview(): JsonResponse { try { // Resolve current school year $currentYear = trim((string) ($this->schoolYear ?? '')); if ($currentYear === '') { $latest = $this->log->newQuery() ->select('school_year') ->orderBy('id', 'DESC') ->first(); $currentYear = $latest['school_year'] ?? (date('Y') . '-' . (date('Y') + 1)); } $data = $this->payloadData(); $selectedYear = trim((string) ($data['school_year'] ?? $currentYear)); $selectedSemester = strtolower(trim((string) ($data['semester'] ?? ''))); $previewData = [ 'school_year' => $selectedYear, 'student_name' => trim((string) ($data['student_name'] ?? '')), 'date' => trim((string) ($data['date'] ?? date('m/d/Y'))), 'time_in' => trim((string) ($data['time_in'] ?? date('h:i A'))), 'grade' => trim((string) ($data['grade'] ?? '')), 'reason' => trim((string) ($data['reason'] ?? '')), 'admin_name' => trim((string) ($data['admin_name'] ?? '')), ]; // Prefer logged-in admin name $adminFromDb = $this->currentAdminName(); if ($adminFromDb !== '') { $previewData['admin_name'] = $adminFromDb; } // Handle GET (return list of recent slips) if (strtolower($this->laravelRequest->method()) === 'get') { $builder = $this->log->newQuery(); // 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->where(function ($q) { $q->where('semester', 'fall') ->orWhere('semester', '1') ->orWhere('semester', 1); }); } elseif ($selectedSemester === 'spring') { $builder->where(function ($q) { $q->where('semester', 'spring') ->orWhere('semester', '2') ->orWhere('semester', 2); }); } } // Execute query $logs = $builder->orderBy('id', 'DESC')->limit(50)->get()->toArray(); // Only use fallback when no filters are selected if (empty($logs) && !$hasFilter && $selectedYear === '') { $logs = $this->log->newQuery()->orderBy('id', 'DESC')->limit(50)->get()->toArray(); } // Format rows $rows = []; foreach ($logs as $r) { $dispDate = ''; if (!empty($r['slip_date'])) { $ts = strtotime($r['slip_date']); $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[] = [ 'id' => (int) ($r['id'] ?? 0), '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'] ?? ''), ]; } return $this->success([ 'rows' => $rows, 'school_year' => $selectedYear, 'semester' => $selectedSemester, ], 'Late slip logs retrieved'); } // 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: {$previewData['school_year']}\n\n"; $body = implode("\n", $this->buildSlipLines($previewData, $lineWidth)); $footer = str_repeat("\n", $feedLines); $full = $header . $body . $footer; return $this->success([ 'ok' => true, 'text' => $full, 'width' => $lineWidth, ], 'Preview generated'); } catch (\Throwable $e) { Log::error('Late slip preview failed: ' . $e->getMessage()); return $this->error('Preview failed.', SymfonyResponse::HTTP_INTERNAL_SERVER_ERROR, [ 'details' => $e->getMessage(), ]); } } /** * POST /api/v1/slips/reprint/{id} * Reprint a late slip from a saved log row */ public function reprint($id = null): SymfonyResponse|JsonResponse { $id = (int) ($id ?? 0); if ($id <= 0) { return $this->error('Invalid slip id.', SymfonyResponse::HTTP_BAD_REQUEST); } try { $row = $this->log->find($id); } catch (\Throwable $e) { $row = null; } if (!$row) { return $this->error('Slip not found.', SymfonyResponse::HTTP_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) { $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 { // Log again (best-effort) for audit trail try { $printedBy = $this->getCurrentUserId(); $logData = [ 'school_year' => $data['school_year'], 'semester' => $this->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'], ]; $this->log->logSlip($logData, $printedBy); } catch (\Throwable $e) { Log::error('Late slip reprint log insert failed: ' . $e->getMessage()); } // Generate and return PDF return $this->renderSlipPdfResponse($data); } catch (\Throwable $e) { Log::error('Reprint failed: ' . $e->getMessage()); return $this->error('Reprint failed: ' . $e->getMessage(), SymfonyResponse::HTTP_INTERNAL_SERVER_ERROR); } } /** * Build monospaced slip lines for receipt printing */ private function buildSlipLines(array $data, ?int $lineWidth = null): array { $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 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 WindowsPrintConnector($name); return new Printer($connector); } Log::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 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 UsbPrintConnector($vid, $pid); return new Printer($connector); } // Default: network $host = (string) ($cfg['host'] ?? '192.168.1.100'); $port = (int) ($cfg['port'] ?? 9100); $connector = new NetworkPrintConnector($host, $port); return new Printer($connector); } /** * Ensure any line is exactly $width characters */ 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)); } /** * Convert hex string to int */ 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; } /** * Get printer configuration from environment */ 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; } $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; } /** * Get current admin full name using session user_id */ private function currentAdminName(): string { try { $uid = $this->getCurrentUserId(); if ($uid && $uid > 0) { $user = $this->user->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 return it as a response */ private function renderSlipPdfResponse(array $data): SymfonyResponse { // Determine paper size (mm) $paper = strtolower((string) ($this->laravelRequest->input('paper') ?? env('SLIP_PAPER', 'card'))); // Estimate dynamic height $fontPt = (float) ($this->laravelRequest->input('font_pt') ?? env('SLIP_FONT_PT', 13.0)); $lineH = (float) ($this->laravelRequest->input('line_h') ?? env('SLIP_LINE_H', 2.0)); $linesCount = (int) ($this->laravelRequest->input('rows') ?? 8); if ($linesCount < 1) { $linesCount = 8; } $ptPerLine = $fontPt * $lineH; $mmPerPt = 25.4 / 72.0; $contentMm = $ptPerLine * $linesCount * $mmPerPt; $pageMarginsMm = 3.0 * 2; $fudgeMm = (float) ($this->laravelRequest->input('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); $wChars = 30; break; case 'receipt80': $wMm = 72.0; $hMm = 90.0; $wChars = 30; break; case 'receipt58': $wMm = 58.0; $hMm = 80.0; $wChars = 24; break; case 'card': default: $wMm = 85.60; $hMm = max($dynHeightMm, 40.0); $wChars = 40; } // Optional manual override $hOverride = $this->laravelRequest->input('height_mm') ?? $this->laravelRequest->input('h'); if (is_numeric($hOverride)) { $hMm = max(30.0, min(200.0, (float) $hOverride)); } // Build lines $w = $wChars; $lines = []; $lines[] = $this->fitLine('Al Rahma School at ISGL ' . ($data['school_year'] ?? ''), $w); $lines[] = $this->fitLine('Student Name: ' . (string) ($data['student_name'] ?? ''), $w); $lines[] = $this->fitLine('Date: ' . (string) ($data['date'] ?? ''), $w); $lines[] = $this->fitLine('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); // Generate HTML for PDF $html = $this->generateSlipHtml($data, $lines, ['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'); if ($hMm < $dynHeightMm) { $hMm = $dynHeightMm; } // Set paper size $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 response($dompdf->output(), 200, [ 'Content-Type' => 'application/pdf', 'Content-Disposition' => 'inline; filename="' . $filename . '"', ]); } /** * Generate HTML for the slip PDF */ private function generateSlipHtml(array $data, array $lines, array $page): string { $wMm = $page['w_mm']; $hMm = $page['h_mm']; $html = ' '; foreach ($lines as $line) { $html .= '
' . htmlspecialchars($line, ENT_QUOTES, 'UTF-8') . '
'; } $html .= ' '; return $html; } }