reconstruction of the project

This commit is contained in:
root
2026-03-08 16:33:24 -04:00
parent 23b7db1107
commit c8de5f7edc
9157 changed files with 77877 additions and 1073823 deletions
@@ -0,0 +1,691 @@
<?php
namespace App\Controllers\View;
use App\Controllers\BaseController;
use CodeIgniter\HTTP\ResponseInterface;
use App\Models\UserModel;
use App\Models\LateSlipLogModel;
use App\Models\ConfigurationModel;
use Mike42\Escpos\PrintConnectors\WindowsPrintConnector;
// ESC/POS
use Mike42\Escpos\Printer;
use Mike42\Escpos\EscposImage;
use Mike42\Escpos\PrintConnectors\NetworkPrintConnector;
use Mike42\Escpos\PrintConnectors\UsbPrintConnector;
use Dompdf\Dompdf;
use Dompdf\Options;
class SlipPrinterController extends BaseController
{
protected $configModel;
protected $semester;
protected $schoolYear;
public function __construct()
{
helper(['form', 'url']);
$this->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 (~2432 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());
}
}