add more controller
This commit is contained in:
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.');
|
||||
}
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
@@ -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 (~24–32 chars). Fall back to env config.
|
||||
$cfgWidth = (int) ($this->printerConfig()['chars_per_line'] ?? 0);
|
||||
$w = $lineWidth ?? ($cfgWidth > 0 ? $cfgWidth : 32);
|
||||
|
||||
$lines = [];
|
||||
$lines[] = $this->fitLine('Student Name: ' . (string)($data['student_name'] ?? ''), $w);
|
||||
$lines[] = $this->fitLine('Date: ' . (string)($data['date'] ?? '') . ' ' . 'Time In: ' . (string)($data['time_in'] ?? ''), $w);
|
||||
$lines[] = $this->fitLine('Grade: ' . (string)($data['grade'] ?? ''), $w);
|
||||
$lines[] = $this->fitLine('Reason: ' . (string)($data['reason'] ?? ''), $w);
|
||||
$lines[] = $this->fitLine('Admin: ' . (string)($data['admin_name'] ?? ''), $w);
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a configured Mike42\Escpos\Printer instance.
|
||||
*/
|
||||
private function makePrinter(): Printer
|
||||
{
|
||||
$cfg = $this->printerConfig();
|
||||
$mode = strtolower((string) ($cfg['mode'] ?? 'network'));
|
||||
|
||||
if ($mode === 'windows') {
|
||||
if (stripos(PHP_OS_FAMILY, 'Windows') !== false) {
|
||||
$name = (string) ($cfg['windows_name'] ?? 'POS-80');
|
||||
$connector = new \Mike42\Escpos\PrintConnectors\WindowsPrintConnector($name);
|
||||
return new Printer($connector);
|
||||
}
|
||||
log_message('warning', 'PRINTER_MODE=windows on non-Windows OS; falling back to network.');
|
||||
$mode = 'network';
|
||||
}
|
||||
|
||||
if ($mode === 'usb') {
|
||||
if (stripos(PHP_OS_FAMILY, 'Windows') !== false) {
|
||||
$name = (string) ($cfg['windows_name'] ?? 'POS-80');
|
||||
$connector = new \Mike42\Escpos\PrintConnectors\WindowsPrintConnector($name);
|
||||
return new Printer($connector);
|
||||
}
|
||||
$vidStr = (string) ($cfg['usb_vid'] ?? '');
|
||||
$pidStr = (string) ($cfg['usb_pid'] ?? '');
|
||||
if ($vidStr === '' || $pidStr === '') {
|
||||
throw new \RuntimeException('USB mode requires PRINTER_USB_VID and PRINTER_USB_PID');
|
||||
}
|
||||
$vid = $this->hexToInt($vidStr);
|
||||
$pid = $this->hexToInt($pidStr);
|
||||
$connector = new \Mike42\Escpos\PrintConnectors\UsbPrintConnector($vid, $pid);
|
||||
return new Printer($connector);
|
||||
}
|
||||
|
||||
// Default: network
|
||||
$host = (string) ($cfg['host'] ?? '192.168.1.100');
|
||||
$port = (int) ($cfg['port'] ?? 9100);
|
||||
$connector = new \Mike42\Escpos\PrintConnectors\NetworkPrintConnector($host, $port);
|
||||
return new Printer($connector);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Build a "Label: ________VALUE" style line, using underscores to fill remaining space.
|
||||
* If $value is empty, just render a long blank line after the label.
|
||||
*/
|
||||
private function labelWithLine(string $label, string $value, int $width, ?int $fillCount = null): string
|
||||
{
|
||||
$label = rtrim($label, ':') . ': ';
|
||||
$maxFill = $fillCount ?? max(0, $width - strlen($label));
|
||||
$filled = $value !== ''
|
||||
? $this->fixedField($value, $maxFill, '_')
|
||||
: str_repeat('_', $maxFill);
|
||||
|
||||
$line = $label . $filled;
|
||||
|
||||
// Ensure total width, pad or trim
|
||||
return $this->fitLine($line, $width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single field like "Date: ________" (or with value).
|
||||
*/
|
||||
private function fieldWithBlanks(string $label, string $value, int $fieldWidth): string
|
||||
{
|
||||
$label = rtrim($label, ':') . ': ';
|
||||
$fill = max(0, $fieldWidth - strlen($label));
|
||||
$content = $value !== '' ? $this->fixedField($value, $fill, '_') : str_repeat('_', $fill);
|
||||
return $this->fitLine($label . $content, $fieldWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Join two fixed-width fields into one line (e.g., Date field + Time In field).
|
||||
*/
|
||||
private function joinTwoFields(string $left, string $right, int $totalWidth): string
|
||||
{
|
||||
$gap = ' ';
|
||||
$line = $left . $gap . $right;
|
||||
return $this->fitLine($line, $totalWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pad/truncate a value to an exact width using a fill character (for underlines).
|
||||
*/
|
||||
private function fixedField(string $val, int $width, string $fillChar = '_'): string
|
||||
{
|
||||
$val = (string) $val;
|
||||
if (strlen($val) > $width) {
|
||||
return substr($val, 0, $width);
|
||||
}
|
||||
return $val . str_repeat($fillChar, max(0, $width - strlen($val)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure any line is exactly $width characters (trim or pad spaces).
|
||||
*/
|
||||
private function fitLine(string $line, int $width): string
|
||||
{
|
||||
$line = (string) $line;
|
||||
if (strlen($line) > $width) {
|
||||
return substr($line, 0, $width);
|
||||
}
|
||||
return $line . str_repeat(' ', $width - strlen($line));
|
||||
}
|
||||
|
||||
/**
|
||||
* "0x1a86" -> 0x1a86 (int). Accepts plain decimal too.
|
||||
*/
|
||||
private function hexToInt(string $hexOrDec): int
|
||||
{
|
||||
$hexOrDec = trim($hexOrDec);
|
||||
if ($hexOrDec === '') return 0;
|
||||
if (stripos($hexOrDec, '0x') === 0) {
|
||||
return intval($hexOrDec, 16);
|
||||
}
|
||||
return (int) $hexOrDec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consolidated printer configuration. Supports both dotted keys (printer.*)
|
||||
* and uppercase PRINTER_* keys from .env.
|
||||
*/
|
||||
private function printerConfig(): array
|
||||
{
|
||||
$get = function ($keys, $default = null) {
|
||||
$keys = is_array($keys) ? $keys : [$keys];
|
||||
foreach ($keys as $k) {
|
||||
$v = env($k);
|
||||
if ($v !== null && $v !== '') return $v;
|
||||
}
|
||||
return $default;
|
||||
};
|
||||
|
||||
return [
|
||||
'mode' => strtolower((string) $get(['printer.mode', 'PRINTER_MODE'], 'network')),
|
||||
'host' => (string) $get(['printer.host', 'PRINTER_HOST'], '192.168.1.100'),
|
||||
'port' => (int) $get(['printer.port', 'PRINTER_PORT'], 9100),
|
||||
'usb_vid' => (string) $get(['printer.usb.vid', 'PRINTER_USB_VID'], ''),
|
||||
'usb_pid' => (string) $get(['printer.usb.pid', 'PRINTER_USB_PID'], ''),
|
||||
'windows_name' => (string) $get(['printer.windows.name', 'PRINTER_WINDOWS_NAME'], 'POS-80'),
|
||||
'chars_per_line' => (int) $get(['printer.chars_per_line', 'PRINTER_CHARS_PER_LINE'], 48),
|
||||
'feed_lines' => (int) $get(['printer.feed_lines', 'PRINTER_FEED_LINES'], 3),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert user-provided date/time strings into DB formats.
|
||||
*/
|
||||
private function toDbDate(?string $s): ?string
|
||||
{
|
||||
$s = trim((string)$s);
|
||||
if ($s === '') return null;
|
||||
// Explicitly parse common US formats to avoid day/month swaps.
|
||||
if (preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $s)) {
|
||||
return $s;
|
||||
}
|
||||
if (preg_match('/^\\d{1,2}\\/\\d{1,2}\\/\\d{4}$/', $s)) {
|
||||
$dt = \DateTime::createFromFormat('!m/d/Y', $s);
|
||||
return $dt ? $dt->format('Y-m-d') : null;
|
||||
}
|
||||
if (preg_match('/^\\d{1,2}-\\d{1,2}-\\d{4}$/', $s)) {
|
||||
$dt = \DateTime::createFromFormat('!m-d-Y', $s);
|
||||
return $dt ? $dt->format('Y-m-d') : null;
|
||||
}
|
||||
$ts = strtotime($s);
|
||||
return $ts ? date('Y-m-d', $ts) : null;
|
||||
}
|
||||
|
||||
private function toDbTime(?string $s): ?string
|
||||
{
|
||||
$s = trim((string)$s);
|
||||
if ($s === '') return null;
|
||||
$ts = strtotime($s);
|
||||
return $ts ? date('H:i:s', $ts) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute current admin full name using session user_id and users table.
|
||||
* Falls back to session firstname/lastname, then user_name, else ''.
|
||||
*/
|
||||
private function currentAdminName(): string
|
||||
{
|
||||
try {
|
||||
$uid = (int) (session()->get('user_id') ?? 0);
|
||||
if ($uid > 0) {
|
||||
$user = (new UserModel())
|
||||
->select('firstname, lastname')
|
||||
->find($uid);
|
||||
if ($user) {
|
||||
$full = trim((string)($user['firstname'] ?? '') . ' ' . (string)($user['lastname'] ?? ''));
|
||||
if ($full !== '') return $full;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore and fall back
|
||||
}
|
||||
|
||||
$first = trim((string)(session()->get('firstname') ?? ''));
|
||||
$last = trim((string)(session()->get('lastname') ?? ''));
|
||||
$full = trim($first . ' ' . $last);
|
||||
if ($full !== '') return $full;
|
||||
return (string)(session()->get('user_name') ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the slip as a PDF and stream it inline to the browser.
|
||||
*/
|
||||
private function renderSlipPdfResponse(array $data): ResponseInterface
|
||||
{
|
||||
// Build HTML from a dedicated view
|
||||
// Determine paper size (mm)
|
||||
$paper = strtolower((string) ($this->request->getVar('paper') ?: env('SLIP_PAPER', 'card')));
|
||||
// Estimate dynamic height based on the PDF view font and line-height
|
||||
// Title + seven content lines = 8 rows total
|
||||
// Defaults match the current view: 13pt font, 2.0 line-height
|
||||
$fontPt = (float) ($this->request->getVar('font_pt') ?? env('SLIP_FONT_PT', 13.0));
|
||||
$lineH = (float) ($this->request->getVar('line_h') ?? env('SLIP_LINE_H', 2.0));
|
||||
$linesCount = (int) ($this->request->getVar('rows') ?? 8);
|
||||
if ($linesCount < 1) {
|
||||
$linesCount = 8;
|
||||
}
|
||||
$ptPerLine = $fontPt * $lineH;
|
||||
$mmPerPt = 25.4 / 72.0; // 1pt in mm
|
||||
$contentMm = $ptPerLine * $linesCount * $mmPerPt; // content only
|
||||
$pageMarginsMm = 3.0 * 2; // top+bottom from CSS
|
||||
// Allow caller to increase safety buffer if their viewer/driver adds spacing
|
||||
// Slightly bigger safety buffer to avoid second-page spillovers from drivers
|
||||
$fudgeMm = (float) ($this->request->getVar('fudge_mm') ?? env('SLIP_FUDGE_MM', 12.0));
|
||||
$dynHeightMm = $contentMm + $pageMarginsMm + $fudgeMm;
|
||||
|
||||
switch ($paper) {
|
||||
case 'cardp':
|
||||
case 'card-portrait':
|
||||
$wMm = 53.98;
|
||||
$hMm = max($dynHeightMm, 40.0); // portrait width, dynamic height
|
||||
$wChars = 30;
|
||||
break;
|
||||
case 'receipt80':
|
||||
$wMm = 72.0;
|
||||
$hMm = 90.0; // 80mm roll (printable ~72mm) — taller to fit larger font
|
||||
$wChars = 30;
|
||||
break;
|
||||
case 'receipt58':
|
||||
$wMm = 58.0;
|
||||
$hMm = 80.0; // 58mm roll — taller to fit larger font
|
||||
$wChars = 24;
|
||||
break;
|
||||
case 'card':
|
||||
default:
|
||||
$wMm = 85.60;
|
||||
$hMm = max($dynHeightMm, 40.0); // credit card width, dynamic height
|
||||
$wChars = 40;
|
||||
}
|
||||
|
||||
// Optional manual override via query: height_mm (or h)
|
||||
$hOverride = $this->request->getVar('height_mm') ?? $this->request->getVar('h');
|
||||
if (is_numeric($hOverride)) {
|
||||
$hMm = max(30.0, min(200.0, (float) $hOverride));
|
||||
}
|
||||
|
||||
// Build exactly six lines in monospace formatting
|
||||
$w = $wChars; // characters per line for layout
|
||||
$lines = [];
|
||||
$lines[] = $this->fitLine('Al Rahma School at ISGL ' . ($data['school_year'] ?? ''), $w);
|
||||
// Helper to compose "Label: value_____" without truncating value first
|
||||
$compose = function (string $label, string $value, int $padUnderscore = 0) use ($w) {
|
||||
$prefix = rtrim($label, ':') . ': ';
|
||||
$line = $prefix . $value;
|
||||
$len = strlen($line);
|
||||
if ($padUnderscore > 0 && $len < $w) {
|
||||
$line .= str_repeat('_', min($padUnderscore, $w - $len));
|
||||
}
|
||||
return $this->fitLine($line, $w);
|
||||
};
|
||||
// Student Name (no trailing underscores)
|
||||
$lines[] = $compose('Student Name', (string)($data['student_name'] ?? ''), 0);
|
||||
// Date and Time In on separate lines
|
||||
$lines[] = $this->fitLine('Date: ' . (string)($data['date'] ?? ''), $w);
|
||||
$lines[] = $this->fitLine('Time In: ' . (string)($data['time_in'] ?? ''), $w);
|
||||
// Grade and Reason (no trailing underscores)
|
||||
$lines[] = $compose('Grade', (string)($data['grade'] ?? ''), 0);
|
||||
$lines[] = $compose('Reason', (string)($data['reason'] ?? ''), 0);
|
||||
// Admin: do not underline, just value
|
||||
$lines[] = $this->fitLine('Admin: ' . (string)($data['admin_name'] ?? ''), $w);
|
||||
|
||||
$html = view('slips/slip_pdf', ['entry' => $data, 'lines' => $lines, 'page' => ['w_mm' => $wMm, 'h_mm' => $hMm]]);
|
||||
|
||||
$options = new Options();
|
||||
$options->set('isRemoteEnabled', true);
|
||||
$options->set('defaultFont', 'Courier');
|
||||
|
||||
$dompdf = new Dompdf($options);
|
||||
$dompdf->loadHtml($html, 'UTF-8');
|
||||
// Ensure minimum height is enough for current content calculation
|
||||
if ($hMm < $dynHeightMm) {
|
||||
$hMm = $dynHeightMm;
|
||||
}
|
||||
|
||||
// Set paper size from selected mm values -> points
|
||||
$mmToPt = 72 / 25.4;
|
||||
$wPt = $wMm * $mmToPt;
|
||||
$hPt = $hMm * $mmToPt;
|
||||
$dompdf->setPaper([0, 0, $wPt, $hPt]);
|
||||
$dompdf->render();
|
||||
|
||||
$filename = 'LateSlip_' . preg_replace('/[^A-Za-z0-9]+/', '_', (string)($data['student_name'] ?? 'slip')) . '_' . date('Ymd_His') . '.pdf';
|
||||
|
||||
return $this->response
|
||||
->setHeader('Content-Type', 'application/pdf')
|
||||
->setHeader('Content-Disposition', 'inline; filename="' . $filename . '"')
|
||||
->setBody($dompdf->output());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
Reference in New Issue
Block a user