add projet

This commit is contained in:
root
2026-03-05 12:29:37 -05:00
parent 8d1eef8ba8
commit 23b7db1107
9109 changed files with 1106501 additions and 73 deletions
+615
View File
@@ -0,0 +1,615 @@
<?php
namespace App\Http\Controllers\Api;
use App\Models\Configuration;
use App\Models\LateSlipLog;
use App\Models\User;
use Carbon\Carbon;
use Dompdf\Dompdf;
use Dompdf\Options;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Session;
use Mike42\Escpos\Printer;
use Mike42\Escpos\PrintConnectors\NetworkPrintConnector;
use Mike42\Escpos\PrintConnectors\UsbPrintConnector;
use Mike42\Escpos\PrintConnectors\WindowsPrintConnector;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
class SlipPrinterController extends BaseApiController
{
protected Configuration $config;
protected LateSlipLog $log;
protected User $user;
protected string $schoolYear;
protected string $semester;
public function __construct()
{
parent::__construct();
$this->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 = '<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@page {
margin: 0;
size: ' . $wMm . 'mm ' . $hMm . 'mm;
}
body {
margin: 3mm;
font-family: "Courier New", Courier, monospace;
font-size: 13pt;
line-height: 2.0;
}
.line {
white-space: pre;
}
</style>
</head>
<body>';
foreach ($lines as $line) {
$html .= '<div class="line">' . htmlspecialchars($line, ENT_QUOTES, 'UTF-8') . '</div>';
}
$html .= '</body>
</html>';
return $html;
}
}