add more controller

This commit is contained in:
root
2026-03-10 00:48:32 -04:00
parent 5eeaec0257
commit 20ee70d153
151 changed files with 9481 additions and 8407 deletions
-96
View File
@@ -1,96 +0,0 @@
<?php
namespace App\Controllers\View;
use App\Models\ConfigurationModel;
use CodeIgniter\Controller;
class ConfigurationController extends Controller
{
protected $configModel;
public function __construct()
{
$this->configModel = new ConfigurationModel();
}
// Method to load configuration management page
public function index()
{
helper('url');
return view('configuration/configuration_view', [
'configEndpoint' => site_url('api/configuration'),
]);
}
public function addConfig()
{
// Retrieve POST data
$configKey = $this->request->getPost('config_key');
$configValue = $this->request->getPost('config_value');
// Validate inputs (optional)
if ($configKey && $configValue) {
// Save to the database
$this->configModel->save([
'config_key' => $configKey,
'config_value' => $configValue,
]);
// Redirect to the configuration view page
return redirect()->to('/configuration/configuration_view')->with('success', 'Configuration added.');
}
// If validation fails, reload the form with input
return redirect()->back()->withInput()->with('error', 'Please fill in all required fields.');
}
// Method to edit an existing configuration
public function editConfig($id)
{
$config = $this->configModel->find($id);
if (strtolower($this->request->getMethod()) === 'post') {
$key = trim((string) $this->request->getPost('config_key'));
$value = trim((string) $this->request->getPost('config_value'));
$this->configModel->update($id, [
'config_key' => $key,
'config_value' => $value
]);
return redirect()->to('/configuration/configuration_view')->with('success', 'Configuration updated.');
}
return view('configuration/configuration_edit', ['config' => $config]);
}
public function deleteConfig($id)
{
if ($this->configModel->find($id)) {
$this->configModel->delete($id);
return redirect()->to('/configuration/configuration_view')->with('success', 'Configuration deleted successfully.');
}
return redirect()->to('/configuration/configuration_view')->with('error', 'Configuration not found.');
}
public function listData()
{
$rows = $this->configModel
->orderBy('id', 'ASC')
->findAll();
$configs = array_map(static function ($row) {
if (!is_array($row)) {
return [];
}
return [
'id' => (int) ($row['id'] ?? 0),
'config_key' => (string) ($row['config_key'] ?? ''),
'config_value' => (string) ($row['config_value'] ?? ''),
];
}, $rows ?? []);
return $this->response->setJSON(['configs' => $configs]);
}
}
-135
View File
@@ -1,135 +0,0 @@
<?php
namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\EmergencyContactModel;
use App\Models\StudentModel;
use App\Models\UserModel;
use CodeIgniter\HTTP\ResponseInterface;
class EmergencyContactController extends BaseController
{
protected $contactModel;
protected $studentModel;
protected $userModel;
public function __construct()
{
$this->contactModel = new EmergencyContactModel();
$this->studentModel = new StudentModel();
$this->userModel = new UserModel(); // Add this
}
public function index()
{
$db = \Config\Database::connect();
$parentIds = $this->contactModel
->distinct()
->select('parent_id')
->findAll();
$data = [];
foreach ($parentIds as $row) {
$parentId = $row['parent_id'];
$parent = $this->userModel->find($parentId); // Get parent info
$parentName = $parent ? $parent['firstname'] . ' ' . $parent['lastname'] : 'Unknown Parent';
$parentPhone = is_array($parent) ? (string)($parent['cellphone'] ?? '') : '';
// Try to load second parent phone from parents table (if available)
$secondPhone = '';
try {
$row = $db->table('parents')
->select('secondparent_phone')
->where('firstparent_id', (int) $parentId)
->orderBy('updated_at', 'DESC')
->get()->getRowArray();
if ($row && !empty($row['secondparent_phone'])) {
$secondPhone = (string) $row['secondparent_phone'];
}
} catch (\Throwable $e) {
log_message('debug', 'EmergencyContactController: could not load second parent phone: ' . $e->getMessage());
}
$students = $this->studentModel
->where('parent_id', $parentId)
->findAll();
$contacts = $this->contactModel
->getEmergencyContactsByParentId($parentId);
$data[] = [
'parent_id' => $parentId,
'parent_name' => $parentName,
'students' => $students,
'contacts' => $contacts,
'parent_phones' => array_values(array_filter([$parentPhone, $secondPhone], static fn($v) => (string)$v !== '')),
];
}
return view('administrator/emergency_contact/index', ['groups' => $data]);
}
public function edit($id)
{
$contact = $this->contactModel->find($id);
return view('administrator/emergency_contact/edit', ['contact' => $contact]);
}
public function update($id)
{
dd("Update was called with ID: $id", $this->request->getPost());
}
public function delete($id)
{
$this->contactModel->delete($id);
return redirect()->to('/administrator/emergency_contact')->with('success', 'Contact deleted.');
}
// API: JSON payload for emergency contacts grouped by parent
public function data()
{
// Build groups similarly to index(), but return JSON
$parentRows = $this->contactModel
->distinct()
->select('parent_id')
->findAll();
$groups = [];
foreach ($parentRows as $row) {
$parentId = (int)($row['parent_id'] ?? 0);
if ($parentId <= 0) continue;
$parent = $this->userModel->find($parentId) ?: [];
$parentName = trim(($parent['firstname'] ?? '') . ' ' . ($parent['lastname'] ?? '')) ?: 'Unknown Parent';
$students = $this->studentModel
->select('id, firstname, lastname, school_id')
->where('parent_id', $parentId)
->findAll();
$contacts = $this->contactModel
->where('parent_id', $parentId)
->orderBy('updated_at', 'DESC')
->findAll();
$groups[] = [
'parent_id' => $parentId,
'parent_name' => $parentName,
'students' => $students,
'contacts' => $contacts,
];
}
return $this->response->setJSON([
'groups' => $groups,
'csrfHash' => csrf_hash(),
]);
}
}
-605
View File
@@ -1,605 +0,0 @@
<?php
namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\ClassSectionModel;
use App\Models\ConfigurationModel;
use App\Models\ExamDraftModel;
use App\Models\TeacherClassModel;
use App\Models\UserModel;
use CodeIgniter\HTTP\Files\UploadedFile;
use Config\Database;
class ExamDraftController extends BaseController
{
protected ExamDraftModel $examDraftModel;
protected TeacherClassModel $teacherClassModel;
protected ClassSectionModel $classSectionModel;
protected UserModel $userModel;
protected ConfigurationModel $configModel;
protected string $schoolYear;
protected string $semester;
protected bool $hasFinalPdfColumn = false;
protected bool $hasIsLegacyColumn = false;
// DB enum: draft, submitted, reviewed, finalized, rejected
// (string literals used to avoid excess constants)
protected const TEACHER_UPLOAD_DIR = 'exams/drafts';
protected const FINAL_UPLOAD_DIR = 'exams/finals';
protected const MAX_UPLOAD_BYTES = 12 * 1024 * 1024;
protected const ALLOWED_EXTENSIONS = ['doc', 'docx'];
protected const ADMIN_ALLOWED_EXTENSIONS = ['doc', 'docx', 'pdf'];
protected array $examTypes = [
'Final Exam',
'Midterm Exam',
'Quiz',
'Study Guide',
'Practice Exam',
'Other',
];
public function __construct()
{
$this->examDraftModel = new ExamDraftModel();
$this->teacherClassModel = new TeacherClassModel();
$this->classSectionModel = new ClassSectionModel();
$this->userModel = new UserModel();
$this->configModel = new ConfigurationModel();
$this->db = Database::connect();
$this->schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
$this->semester = (string) ($this->configModel->getConfig('semester') ?? '');
$this->hasFinalPdfColumn = $this->schemaHasColumn('exam_drafts', 'final_pdf_file');
$this->hasIsLegacyColumn = $this->schemaHasColumn('exam_drafts', 'is_legacy');
helper(['form', 'url', 'date']);
}
public function teacherIndex()
{
$teacherId = (int) (session()->get('user_id') ?? 0);
if ($teacherId <= 0) {
return redirect()->to('/login');
}
$assignments = $this->teacherClassModel->getClassAssignmentsByUserId($teacherId, $this->schoolYear);
$selectedClass = $this->resolveSelectedClassSection($assignments);
if ($selectedClass > 0) {
session()->set('class_section_id', $selectedClass);
}
$allDrafts = $this->examDraftModel
->where('teacher_id', $teacherId)
->orderBy('created_at', 'DESC')
->findAll();
foreach ($allDrafts as &$row) {
if (empty($row['final_pdf_file'])) {
$pdf = $this->ensurePdfExists($row['final_file'] ?? '', pathinfo($row['final_file'] ?? '', PATHINFO_EXTENSION));
if ($pdf !== null) {
$row['final_pdf_file'] = $pdf;
}
}
}
unset($row);
$classSectionIds = array_map(static fn($a) => (int) $a['class_section_id'], $assignments);
$legacyExams = [];
// Guard against missing schema column in older databases
if ($this->hasIsLegacyColumn) {
// Keep all submissions visible; legacy ones are also surfaced in a separate tab
$drafts = $allDrafts;
if (!empty($classSectionIds)) {
$legacyExams = $this->examDraftModel
->select('exam_drafts.*, cs.class_section_name')
->join('classSection cs', 'cs.class_section_id = exam_drafts.class_section_id', 'left')
->whereIn('exam_drafts.class_section_id', $classSectionIds)
->where('exam_drafts.is_legacy', 1)
->where('exam_drafts.final_file IS NOT NULL', null, false)
->orderBy('cs.class_section_name', 'ASC')
->orderBy('exam_drafts.created_at', 'DESC')
->findAll();
}
} else {
// Legacy column absent, show all drafts and skip legacy tab query
$drafts = $allDrafts;
}
$validation = session()->getFlashdata('validation') ?? $this->validator;
return view('teacher/exam_drafts', [
'assignments' => $assignments,
'selectedClassSection' => $selectedClass,
'drafts' => $drafts,
'legacyExams' => $legacyExams,
'examTypes' => $this->examTypes,
'statusBadges' => $this->statusBadgeMap(),
'schoolYear' => $this->schoolYear,
'semester' => $this->semester,
'maxUploadBytes' => self::MAX_UPLOAD_BYTES,
'validation' => $validation,
]);
}
public function teacherStore()
{
$teacherId = (int) (session()->get('user_id') ?? 0);
if ($teacherId <= 0) {
return redirect()->to('/login');
}
$classSectionId = (int) ($this->request->getPost('class_section_id') ?? session()->get('class_section_id') ?? 0);
$examType = trim((string) $this->request->getPost('exam_type'));
$description = trim((string) $this->request->getPost('description'));
if ($classSectionId <= 0) {
return redirect()->back()->withInput()->with('error', 'Select a class section before submitting.');
}
$assignment = $this->teacherClassModel
->where('teacher_id', $teacherId)
->where('class_section_id', $classSectionId)
->first();
if (empty($assignment)) {
return redirect()->back()->withInput()->with('error', 'You are not assigned to the selected class section.');
}
session()->set('class_section_id', $classSectionId);
if ($classSectionId <= 0) {
$classSectionId = $this->resolveSelectedClassSection($assignments);
if ($classSectionId <= 0) {
return redirect()->back()->withInput()->with('error', 'Select a class section before submitting.');
}
}
$file = $this->request->getFile('draft_file');
$teacherFile = null;
$teacherFilename = null;
if ($file && $file->isValid() && !$file->hasMoved()) {
$stored = $this->storeUploadedFile($file, self::TEACHER_UPLOAD_DIR);
if ($stored === null) {
return redirect()->back()->withInput()->with('error', 'Failed to store the uploaded file.');
}
$teacherFile = $stored;
$teacherFilename = $file->getClientName();
} elseif ($file && $file->getError() !== UPLOAD_ERR_NO_FILE) {
return redirect()->back()->withInput()->with('error', 'Upload failed. Please try again.');
}
$existingDraft = $this->examDraftModel
->where('teacher_id', $teacherId)
->where('class_section_id', $classSectionId)
->where('semester', $this->semester)
->where('school_year', $this->schoolYear)
->orderBy('version', 'DESC')
->first();
$nextVersion = 1;
$previousId = null;
if (!empty($existingDraft)) {
$nextVersion = ((int) ($existingDraft['version'] ?? 1)) + 1;
$previousId = (int) ($existingDraft['id'] ?? 0) ?: null;
}
$title = $examType ?: 'Exam Draft';
$payload = [
'teacher_id' => $teacherId,
'class_section_id' => $classSectionId,
'semester' => $this->semester,
'school_year' => $this->schoolYear,
'exam_type' => $examType ?: null,
'draft_title' => $title,
'description' => $description === '' ? null : $description,
'teacher_file' => $teacherFile,
'teacher_filename' => $teacherFilename,
'status' => 'submitted',
'version' => $nextVersion,
'previous_draft_id' => $previousId,
];
if ($this->examDraftModel->insert($payload)) {
return redirect()->to('/teacher/exam-drafts')->with('success', 'Exam draft submitted for review.');
}
return redirect()->back()->withInput()->with('error', 'Unable to save the exam draft.');
}
public function adminIndex()
{
$allDrafts = $this->examDraftModel
->select('exam_drafts.*, cs.class_section_name, u.firstname AS teacher_first, u.lastname AS teacher_last, a.firstname AS admin_first, a.lastname AS admin_last')
->join('classSection cs', 'cs.class_section_id = exam_drafts.class_section_id', 'left')
->join('users u', 'u.id = exam_drafts.teacher_id', 'left')
->join('users a', 'a.id = exam_drafts.admin_id', 'left')
->orderBy('exam_drafts.created_at', 'DESC')
->findAll();
foreach ($allDrafts as &$row) {
if (empty($row['final_pdf_file'])) {
$pdf = $this->ensurePdfExists($row['final_file'] ?? '', pathinfo($row['final_file'] ?? '', PATHINFO_EXTENSION));
if ($pdf !== null) {
$row['final_pdf_file'] = $pdf;
}
}
}
unset($row);
$classSections = $this->classSectionModel
->select('class_section_id, class_section_name')
->orderBy('class_section_name', 'ASC')
->findAll();
// Group legacy uploads (admin-uploaded finalized exams) by class_section for separate tab
$legacyByClass = [];
if ($this->hasIsLegacyColumn) {
// Keep all submissions visible; additionally surface legacy items in a separate tab
$drafts = $allDrafts;
foreach ($allDrafts as $d) {
$isLegacy = !empty($d['is_legacy']);
if (!$isLegacy) {
continue;
}
$cid = (int)($d['class_section_id'] ?? 0);
if (!isset($legacyByClass[$cid])) {
$legacyByClass[$cid] = [
'class_section_id' => $cid,
'class_section_name' => $d['class_section_name'] ?? 'Class ' . $cid,
'items' => [],
];
}
$legacyByClass[$cid]['items'][] = $d;
}
} else {
// Column missing: keep behavior simple and avoid legacy tab
$drafts = $allDrafts;
}
return view('administrator/exam_drafts', [
'drafts' => $drafts,
'statusBadges' => $this->statusBadgeMap(),
'statusOptions' => $this->statusOptions(),
'schoolYear' => $this->schoolYear,
'semester' => $this->semester,
'allowedExtensions' => self::ADMIN_ALLOWED_EXTENSIONS,
'maxUploadBytes' => self::MAX_UPLOAD_BYTES,
'examTypes' => $this->examTypes,
'classSections' => $classSections,
'legacyByClass' => $legacyByClass,
]);
}
public function adminUploadLegacy()
{
$adminId = (int) (session()->get('user_id') ?? 0);
if ($adminId <= 0) {
return redirect()->to('/login');
}
$classSectionId = (int) ($this->request->getPost('class_section_id') ?? 0);
$schoolYear = trim((string) ($this->request->getPost('school_year') ?? $this->schoolYear));
$semester = trim((string) ($this->request->getPost('semester') ?? $this->semester));
$examType = trim((string) $this->request->getPost('exam_type'));
if ($classSectionId <= 0) {
return redirect()->back()->withInput()->with('error', 'Select a class section.');
}
if ($schoolYear === '') {
return redirect()->back()->withInput()->with('error', 'School year is required.');
}
if ($semester === '') {
return redirect()->back()->withInput()->with('error', 'Semester is required.');
}
$file = $this->request->getFile('old_exam_file');
if (!$file || !$file->isValid()) {
return redirect()->back()->withInput()->with('error', 'A valid file is required.');
}
$stored = $this->storeUploadedFile($file, self::FINAL_UPLOAD_DIR, self::ADMIN_ALLOWED_EXTENSIONS);
if ($stored === null) {
return redirect()->back()->withInput()->with('error', 'File type not allowed or upload failed.');
}
$payload = [
'teacher_id' => $adminId, // store under admin user since legacy uploads are admin-only
'class_section_id' => $classSectionId,
'semester' => ucfirst(strtolower($semester)),
'school_year' => $schoolYear,
'exam_type' => $examType === '' ? null : $examType,
'draft_title' => $examType === '' ? 'Legacy Exam' : $examType,
'description' => null,
'final_file' => $stored,
'final_filename' => $file->getClientName(),
'status' => 'finalized',
'admin_id' => $adminId,
'reviewed_at' => utc_now(),
'version' => 1,
];
if ($this->hasIsLegacyColumn) {
$payload['is_legacy'] = 1;
}
$pdfName = null;
if (strtolower($file->getClientExtension()) === 'pdf') {
$pdfName = $stored;
} else {
$pdfName = $this->convertDocToPdf(
$this->fullUploadPath(self::FINAL_UPLOAD_DIR, $stored),
self::FINAL_UPLOAD_DIR
);
}
if ($pdfName !== null && $this->hasFinalPdfColumn) {
$payload['final_pdf_file'] = $pdfName;
}
if ($this->examDraftModel->insert($payload)) {
return redirect()->to('/administrator/exam-drafts')->with('success', 'Old exam uploaded successfully.');
}
return redirect()->back()->withInput()->with('error', 'Unable to save the old exam.');
}
public function adminReview()
{
$draftId = (int) ($this->request->getPost('draft_id') ?? 0);
if ($draftId <= 0) {
return redirect()->back()->with('error', 'Invalid submission selected.');
}
$draft = $this->examDraftModel->find($draftId);
if (empty($draft)) {
return redirect()->back()->with('error', 'Submission not found.');
}
$comments = trim((string) $this->request->getPost('admin_comments'));
$statusInput = trim((string) $this->request->getPost('review_status'));
$currentStatus = (string) ($draft['status'] ?? 'draft');
$status = $this->normalizeStatus(
$statusInput !== '' ? $statusInput : $currentStatus,
$currentStatus
);
$status = strtolower($status);
if (!in_array($status, $this->statusOptions(), true)) {
$status = 'reviewed';
}
$finalFile = null;
$finalFilename = null;
$file = $this->request->getFile('final_file');
if ($file && $file->isValid() && !$file->hasMoved()) {
$stored = $this->storeUploadedFile($file, self::FINAL_UPLOAD_DIR);
if ($stored === null) {
return redirect()->back()->with('error', 'Unable to store the final draft.')->withInput();
}
$finalFile = $stored;
$finalFilename = $file->getClientName();
// Only auto-finalize if the admin explicitly chose "finalized"
// (previously any uploaded file forced finalization)
if ($status === 'finalized') {
$status = 'finalized';
}
} elseif ($file && $file->getError() !== UPLOAD_ERR_NO_FILE) {
return redirect()->back()->with('error', 'Final file upload failed.');
}
$update = [
'status' => $status,
'admin_comments' => $comments === '' ? null : $comments,
'admin_id' => (int) (session()->get('user_id') ?? 0),
'reviewed_at' => utc_now(),
];
if ($finalFile !== null) {
$update['final_file'] = $finalFile;
$update['final_filename'] = $finalFilename;
$pdfName = $this->ensurePdfExists($finalFile, $file ? $file->getClientExtension() : null);
if ($pdfName !== null && $this->hasFinalPdfColumn) {
$update['final_pdf_file'] = $pdfName;
}
} elseif (strtolower($status) === 'finalized' && !empty($draft['teacher_file'])) {
// Auto-promote teacher file when admin finalizes without uploading a final
$copied = $this->copyDraftToFinal($draft['teacher_file']);
if ($copied !== null) {
$update['final_file'] = $copied;
$update['final_filename'] = $draft['teacher_filename'] ?? $draft['teacher_file'];
$pdfName = $this->ensurePdfExists($copied, pathinfo($copied, PATHINFO_EXTENSION));
if ($pdfName !== null && $this->hasFinalPdfColumn) {
$update['final_pdf_file'] = $pdfName;
}
}
}
if (strtolower($status) === 'finalized' && $this->hasIsLegacyColumn) {
$update['is_legacy'] = 1; // finalized exams move to Previous Exams
}
if ($this->hasIsLegacyColumn) {
// Keep existing legacy flag, do not set legacy automatically here
}
$updated = $this->examDraftModel
->set($update)
->where('id', $draftId)
->update();
if ($updated) {
return redirect()->back()->with('success', 'Review saved successfully.');
}
return redirect()->back()->with('error', 'Unable to save the review.');
}
private function resolveSelectedClassSection(array $assignments): int
{
$candidate = (int) ($this->request->getGet('class_section_id') ?? session()->get('class_section_id') ?? 0);
$validIds = array_map('intval', array_column($assignments, 'class_section_id'));
if ($candidate > 0 && in_array($candidate, $validIds, true)) {
return $candidate;
}
return $validIds[0] ?? 0;
}
private function statusBadgeMap(): array
{
return [
'draft' => [
'label' => 'Draft',
'class' => 'bg-secondary text-white',
],
'submitted' => [
'label' => 'Submitted',
'class' => 'bg-warning text-dark',
],
'reviewed' => [
'label' => 'Reviewed',
'class' => 'bg-info text-dark',
],
'finalized' => [
'label' => 'Finalized',
'class' => 'bg-success text-white',
],
'rejected' => [
'label' => 'Rejected',
'class' => 'bg-danger text-white',
],
];
}
private function statusOptions(): array
{
return ['draft', 'submitted', 'reviewed', 'finalized', 'rejected'];
}
private function normalizeStatus(string $input, string $default): string
{
$input = strtolower(trim($input));
$aliases = [
'pending' => 'submitted', // legacy value
'final' => 'finalized',
'approved' => 'finalized', // legacy value
];
if (isset($aliases[$input])) {
$input = $aliases[$input];
}
return in_array($input, $this->statusOptions(), true) ? $input : $default;
}
private function storeUploadedFile(UploadedFile $file, string $subdir, array $allowedExtensions = self::ALLOWED_EXTENSIONS): ?string
{
$ext = strtolower($file->getClientExtension());
if (!in_array($ext, $allowedExtensions, true)) {
return null;
}
if ($file->getSize() > self::MAX_UPLOAD_BYTES) {
return null;
}
$destination = WRITEPATH . 'uploads/' . trim($subdir, '/');
if (!is_dir($destination)) {
mkdir($destination, 0755, true);
}
$filename = $file->getRandomName();
try {
$file->move($destination, $filename);
return $filename;
} catch (\Throwable $e) {
log_message('error', 'ExamDraftController::storeUploadedFile error: ' . $e->getMessage());
return null;
}
}
private function convertDocToPdf(string $sourcePath, string $targetSubdir): ?string
{
// Only attempt conversion for doc/docx
$ext = strtolower(pathinfo($sourcePath, PATHINFO_EXTENSION));
if (!in_array($ext, ['doc', 'docx'], true)) {
return null;
}
$targetDir = WRITEPATH . 'uploads/' . trim($targetSubdir, '/');
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
$base = pathinfo($sourcePath, PATHINFO_FILENAME);
$targetPath = $targetDir . '/' . $base . '.pdf';
// Attempt conversion via LibreOffice if available
$cmd = 'soffice --headless --convert-to pdf --outdir ' . escapeshellarg($targetDir) . ' ' . escapeshellarg($sourcePath) . ' 2>/dev/null';
@exec($cmd);
return is_file($targetPath) ? basename($targetPath) : null;
}
private function schemaHasColumn(string $table, string $column): bool
{
try {
$fields = $this->db->getFieldNames($table);
return in_array($column, $fields, true);
} catch (\Throwable $e) {
log_message('error', "Schema check failed for {$table}.{$column}: " . $e->getMessage());
return false;
}
}
private function fullUploadPath(string $subdir, string $filename): string
{
return WRITEPATH . 'uploads/' . trim($subdir, '/') . '/' . $filename;
}
private function neighborPdfIfExists(?string $filename, string $subdir): ?string
{
if (empty($filename)) return null;
$path = $this->fullUploadPath($subdir, $filename);
$base = pathinfo($path, PATHINFO_FILENAME);
$dir = pathinfo($path, PATHINFO_DIRNAME);
$pdfPath = $dir . '/' . $base . '.pdf';
return is_file($pdfPath) ? basename($pdfPath) : null;
}
private function ensurePdfExists(string $finalFilename, ?string $originalExt): ?string
{
$pdfNeighbor = $this->neighborPdfIfExists($finalFilename, self::FINAL_UPLOAD_DIR);
if ($pdfNeighbor !== null) {
return $pdfNeighbor;
}
$ext = strtolower((string)$originalExt);
if ($ext === 'pdf') {
// final file itself is already pdf
$path = $this->fullUploadPath(self::FINAL_UPLOAD_DIR, $finalFilename);
return is_file($path) ? $finalFilename : null;
}
return $this->convertDocToPdf(
$this->fullUploadPath(self::FINAL_UPLOAD_DIR, $finalFilename),
self::FINAL_UPLOAD_DIR
);
}
private function copyDraftToFinal(string $draftFilename): ?string
{
$source = $this->fullUploadPath(self::TEACHER_UPLOAD_DIR, $draftFilename);
if (!is_file($source)) {
return null;
}
$destinationDir = WRITEPATH . 'uploads/' . trim(self::FINAL_UPLOAD_DIR, '/');
if (!is_dir($destinationDir)) {
mkdir($destinationDir, 0755, true);
}
$ext = pathinfo($draftFilename, PATHINFO_EXTENSION);
$destName = uniqid('final_', true) . '.' . $ext;
$destPath = $destinationDir . '/' . $destName;
if (!@copy($source, $destPath)) {
return null;
}
return $destName;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-227
View File
@@ -1,227 +0,0 @@
<?php
namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\PurchaseOrderModel;
use App\Models\PurchaseOrderItemModel;
use App\Models\SupplierModel;
use App\Models\SupplyModel;
use App\Models\SupplyTransactionModel;
class PurchaseOrderController extends BaseController
{
protected $poModel;
protected $itemModel;
protected $supplierModel;
protected $supplyModel;
protected $txnModel;
protected $db;
public function __construct()
{
$this->poModel = new PurchaseOrderModel();
$this->itemModel = new PurchaseOrderItemModel();
$this->supplierModel = new SupplierModel();
$this->supplyModel = new SupplyModel();
$this->txnModel = new SupplyTransactionModel();
$this->db = \Config\Database::connect();
}
public function index()
{
$q = trim($this->request->getGet('q') ?? '');
$builder = $this->poModel->select('purchase_orders.*, suppliers.name AS supplier_name')
->join('suppliers', 'suppliers.id = purchase_orders.supplier_id', 'left');
if ($q !== '') {
$builder->groupStart()
->like('po_number', $q)
->orLike('suppliers.name', $q)
->groupEnd();
}
$orders = $builder->orderBy('purchase_orders.created_at', 'DESC')->paginate(20);
return view('inventory/po_index', [
'orders' => $orders,
'pager' => $this->poModel->pager,
'q' => $q,
]);
}
public function create()
{
return view('inventory/po_form', [
'title' => 'Create Purchase Order',
'action' => site_url('inventory/po/store'),
'suppliers' => $this->supplierModel->orderBy('name')->findAll(),
'supplies' => $this->supplyModel->orderBy('name')->findAll(),
]);
}
public function store()
{
$po = $this->request->getPost([
'po_number','supplier_id','order_date','expected_date','notes'
]);
$status = $this->request->getPost('status') ?? 'ordered';
$supply_ids = $this->request->getPost('item_supply_id') ?? [];
$descs = $this->request->getPost('item_description') ?? [];
$qtys = $this->request->getPost('item_quantity') ?? [];
$unit_costs = $this->request->getPost('item_unit_cost') ?? [];
if (empty($supply_ids)) {
return redirect()->back()->withInput()->with('error', 'Add at least one line item.');
}
// compute totals
$subtotal = 0.0;
$items = [];
foreach ($supply_ids as $i => $sid) {
$q = max(0, (int)($qtys[$i] ?? 0));
$uc = (float)($unit_costs[$i] ?? 0);
if ($sid && $q > 0) {
$line = $q * $uc;
$subtotal += $line;
$items[] = [
'supply_id' => (int)$sid,
'description' => trim($descs[$i] ?? ''),
'quantity' => $q,
'unit_cost' => $uc,
'received_qty'=> 0,
];
}
}
if (!$items) {
return redirect()->back()->withInput()->with('error', 'Valid line items required.');
}
$tax = 0.00;
$total = $subtotal + $tax;
$this->db->transStart();
$po['status'] = in_array($status, ['draft','ordered'], true) ? $status : 'ordered';
$po['subtotal'] = $subtotal;
$po['tax'] = $tax;
$po['total'] = $total;
if (!$this->poModel->save($po)) {
$this->db->transRollback();
return redirect()->back()->withInput()->with('error', implode("\n", $this->poModel->errors()));
}
$poId = $this->poModel->getInsertID();
foreach ($items as $it) {
$it['purchase_order_id'] = $poId;
if (!$this->itemModel->insert($it)) {
$this->db->transRollback();
return redirect()->back()->withInput()->with('error', 'Failed to save line items.');
}
}
$this->db->transComplete();
if ($this->db->transStatus() === false) {
return redirect()->back()->withInput()->with('error', 'Failed to save purchase order.');
}
return redirect()->to(site_url('inventory/po/show/'.$poId))->with('success', 'PO created.');
}
public function show($id)
{
$po = $this->poModel->select('purchase_orders.*, suppliers.name AS supplier_name')
->join('suppliers', 'suppliers.id = purchase_orders.supplier_id', 'left')
->find($id);
if (!$po) return redirect()->to('inventory/po')->with('error', 'PO not found.');
$items = $this->itemModel->select('purchase_order_items.*, supplies.name AS supply_name, supplies.unit as supply_unit')
->join('supplies', 'supplies.id = purchase_order_items.supply_id', 'left')
->where('purchase_order_id', $id)->findAll();
return view('inventory/po_show', [
'po' => $po,
'items' => $items,
]);
}
/**
* Receive items (partial or full)
* POST body: received[item_id] = qty_to_receive
*/
public function receive($id)
{
$po = $this->poModel->find($id);
if (!$po || in_array($po['status'], ['canceled','received'], true)) {
return redirect()->to('inventory/po/show/'.$id)->with('error', 'PO not receivable.');
}
$received = $this->request->getPost('received') ?? []; // [itemId => qty]
if (!$received) {
return redirect()->to('inventory/po/show/'.$id)->with('error', 'No items to receive.');
}
$issuedBy = (string) (session('user.email') ?? session('user.username') ?? 'system');
$this->db->transStart();
$completed = true;
foreach ($received as $itemId => $qty) {
$qty = (int)$qty;
if ($qty <= 0) continue;
$item = $this->itemModel->where('purchase_order_id', $id)->find($itemId);
if (!$item) { $completed = false; continue; }
$remaining = (int)$item['quantity'] - (int)$item['received_qty'];
$toReceive = min($remaining, $qty);
if ($toReceive <= 0) continue;
// Update item received qty
$this->itemModel->update($itemId, [
'received_qty' => (int)$item['received_qty'] + $toReceive
]);
// Update supply on hand
$supply = $this->supplyModel->find($item['supply_id']);
if (!$supply) { $completed = false; continue; }
$newQty = (int)$supply['qty_on_hand'] + $toReceive;
$this->supplyModel->update($supply['id'], ['qty_on_hand' => $newQty]);
// Log transaction IN
$this->txnModel->insert([
'supply_id' => $supply['id'],
'type' => 'in',
'quantity' => $toReceive,
'ref' => 'PO ' . $po['po_number'],
'issued_to' => 'Inventory',
'issued_by' => $issuedBy,
'notes' => 'Received against PO',
]);
if (($item['received_qty'] + $toReceive) < $item['quantity']) {
$completed = false;
}
}
// Set PO status
$this->poModel->update($id, ['status' => $completed ? 'received' : 'ordered']);
$this->db->transComplete();
if ($this->db->transStatus() === false) {
return redirect()->to('inventory/po/show/'.$id)->with('error', 'Failed to receive items.');
}
return redirect()->to('inventory/po/show/'.$id)->with('success', $completed ? 'PO fully received.' : 'PO partially received.');
}
public function cancel($id)
{
$po = $this->poModel->find($id);
if (!$po || $po['status'] === 'received') {
return redirect()->to('inventory/po/show/'.$id)->with('error', 'Cannot cancel this PO.');
}
$this->poModel->update($id, ['status' => 'canceled']);
return redirect()->to('inventory/po/show/'.$id)->with('success', 'PO canceled.');
}
}
-74
View File
@@ -1,74 +0,0 @@
<?php
namespace App\Services;
use App\Models\UserModel;
use CodeIgniter\Database\BaseConnection;
class SchoolIdService
{
protected $db;
protected $userModel;
public function __construct()
{
$this->db = \Config\Database::connect();
$this->userModel = new UserModel();
}
public function assignSchoolIdToUser($userId)
{
$user = $this->userModel->find($userId);
if (!$user || $user['school_id']) {
return $user['school_id'] ?? null;
}
$schoolId = $this->generateUserSchoolId();
$this->userModel->update($userId, ['school_id' => $schoolId]);
return $schoolId;
}
public function generateStudentSchoolId()
{
$year = date('y');
$maxId = 0;
// Match the real prefix: STU + year
$studentMax = $this->db->table('students')
->select('MAX(school_id) AS max_id')
->like('school_id', "STU{$year}", 'after')
->get()
->getRowArray();
if (!empty($studentMax['max_id'])) {
// Skip "STUyy" (5 chars)
$numericPart = substr($studentMax['max_id'], 5);
$maxId = (int) $numericPart;
}
$nextId = str_pad($maxId + 1, 5, '0', STR_PAD_LEFT);
return "STU{$year}{$nextId}";
}
public function generateUserSchoolId()
{
$year = date('y');
$maxId = 0;
// Get maximum user ID for current year
$userMax = $this->db->table('users')->select('MAX(school_id) AS max_id')
->notLike('school_id', 'S%') // Exclude student IDs
->like('school_id', $year, 'after')->get()->getRowArray();
if (!empty($userMax['max_id'])) {
$numericPart = substr($userMax['max_id'], 2); // Skip 2-digit year
$maxId = max($maxId, (int)$numericPart);
}
$nextId = str_pad($maxId + 1, 5, '0', STR_PAD_LEFT);
return "{$year}{$nextId}";
}
}
-691
View File
@@ -1,691 +0,0 @@
<?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());
}
}
File diff suppressed because it is too large Load Diff
-143
View File
@@ -1,143 +0,0 @@
<?php
namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\ClassModel;
use App\Models\SubjectCurriculumModel;
use CodeIgniter\Exceptions\PageNotFoundException;
class SubjectCurriculumController extends BaseController
{
private const SUBJECT_OPTIONS = [
'islamic' => 'Islamic Studies',
'quran' => 'Quran/Arabic',
];
protected SubjectCurriculumModel $curriculumModel;
protected ClassModel $classModel;
public function __construct()
{
helper(['form']);
$this->curriculumModel = new SubjectCurriculumModel();
$this->classModel = new ClassModel();
}
public function index()
{
$data = $this->buildViewData();
$data['editEntry'] = null;
return view('administrator/subject_curriculum', $data);
}
public function store()
{
if (! $this->validate($this->getValidationRules())) {
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
}
$data = $this->buildEntryDataFromInput();
$insertedId = $this->curriculumModel->insert($data);
if ($insertedId === false) {
return redirect()->back()->withInput()->with('error', 'Unable to save the curriculum entry. Please try again.');
}
return redirect()->to('administrator/subject-curriculum')->with('success', 'Curriculum entry added.');
}
public function edit(int $id)
{
$entry = $this->curriculumModel->find($id);
if (! $entry) {
throw new PageNotFoundException('Curriculum entry not found.');
}
$data = $this->buildViewData();
$data['editEntry'] = $entry;
return view('administrator/subject_curriculum', $data);
}
public function update(int $id)
{
$entry = $this->curriculumModel->find($id);
if (! $entry) {
throw new PageNotFoundException('Curriculum entry not found.');
}
$redirectBack = redirect()->to('administrator/subject-curriculum/edit/' . $id)->withInput();
if (! $this->validate($this->getValidationRules())) {
return $redirectBack->with('errors', $this->validator->getErrors());
}
$data = $this->buildEntryDataFromInput();
if ($this->curriculumModel->update($id, $data) === false) {
return $redirectBack->with('error', 'Unable to update the curriculum entry.');
}
return redirect()->to('administrator/subject-curriculum')->with('success', 'Curriculum entry updated.');
}
public function delete(int $id)
{
$entry = $this->curriculumModel->find($id);
if (! $entry) {
throw new PageNotFoundException('Curriculum entry not found.');
}
$this->curriculumModel->delete($id);
return redirect()->to('administrator/subject-curriculum')->with('success', 'Curriculum entry deleted.');
}
private function buildViewData(): array
{
$classes = $this->classModel->orderBy('class_name', 'ASC')->findAll();
$entries = $this->curriculumModel
->builder()
->select('subject_curriculum_items.*, classes.class_name')
->join('classes', 'classes.id = subject_curriculum_items.class_id', 'left')
->orderBy('classes.class_name', 'ASC')
->orderBy('subject', 'ASC')
->orderBy('unit_number', 'ASC')
->orderBy('chapter_name', 'ASC')
->get()
->getResultArray();
return [
'classes' => $classes,
'entries' => $entries,
'subjectLabels' => self::SUBJECT_OPTIONS,
];
}
private function buildEntryDataFromInput(): array
{
$unitNumber = $this->request->getPost('unit_number');
$unitNumber = is_numeric($unitNumber) ? (int) $unitNumber : null;
if ($unitNumber === 0) {
$unitNumber = null;
}
$unitTitle = trim((string) $this->request->getPost('unit_title'));
$chapterName = trim((string) $this->request->getPost('chapter_name'));
return [
'class_id' => (int) $this->request->getPost('class_id'),
'subject' => (string) $this->request->getPost('subject'),
'unit_number' => $unitNumber,
'unit_title' => $unitTitle !== '' ? $unitTitle : null,
'chapter_name' => $chapterName,
];
}
private function getValidationRules(): array
{
return [
'class_id' => 'required|is_natural_no_zero',
'subject' => 'required|in_list[islamic,quran]',
'chapter_name' => 'required|string|max_length[255]',
'unit_number' => 'permit_empty|integer',
'unit_title' => 'permit_empty|string|max_length[255]',
];
}
}
File diff suppressed because it is too large Load Diff