Files
2026-05-16 13:44:12 -04:00

572 lines
22 KiB
PHP
Executable File

<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use App\Models\PrintRequestModel;
use App\Models\UserModel;
use App\Models\ClassModel;
use App\Models\ClassSectionModel;
use App\Models\ConfigurationModel;
use App\Models\TeacherClassModel;
use App\Models\AdminNotificationSubjectModel;
use App\Models\NotificationModel;
use App\Models\UserNotificationModel;
use CodeIgniter\Exceptions\PageNotFoundException;
class PrintRequests extends BaseController
{
protected $printRequestModel;
protected $userModel;
protected $classModel;
protected $teacherClassModel;
protected $configModel;
protected $classSectionModel;
protected $adminNotificationSubjectModel;
protected $notificationModel;
protected $userNotificationModel;
// Define the upload path as a constant or property for easy maintenance
protected $uploadPath = WRITEPATH . 'uploads/print_requests/';
public function __construct()
{
$this->printRequestModel = new PrintRequestModel();
$this->userModel = new UserModel();
$this->classModel = new ClassModel();
$this->teacherClassModel = new TeacherClassModel();
$this->configModel = new ConfigurationModel();
$this->classSectionModel = new ClassSectionModel();
$this->adminNotificationSubjectModel = new AdminNotificationSubjectModel();
$this->notificationModel = new NotificationModel();
$this->userNotificationModel = new UserNotificationModel();
helper(['form', 'url']);
}
public function teacher_index()
{
$teacher_id = session()->get('user_id');
$data['print_requests'] = $this->printRequestModel
->select('print_requests.*, admins.firstname as admin_firstname, admins.lastname as admin_lastname')
->join('users as admins', 'admins.id = print_requests.admin_id', 'left')
->where('print_requests.teacher_id', $teacher_id)
->findAll();
$teacher_classes = $this->teacherClassModel->getClassByTeacherId($teacher_id);
$data['class_id'] = !empty($teacher_classes) ? $teacher_classes[0]['class_section_id'] : null;
$dateOptions = $this->buildRequiredByOptions();
$data['sundays'] = $dateOptions['sundays'];
$data['times'] = $dateOptions['times'];
return view('print_requests/teacher_index', $data);
}
public function admin_index()
{
$db = \Config\Database::connect();
$query = $db->query("
SELECT
pr.*,
u.firstname,
u.lastname,
cs.class_section_name,
admins.firstname AS admin_firstname,
admins.lastname AS admin_lastname
FROM print_requests pr
LEFT JOIN users u ON u.id = pr.teacher_id
LEFT JOIN classSection cs ON cs.class_section_id = pr.class_id
LEFT JOIN users admins ON admins.id = pr.admin_id
ORDER BY
CASE
WHEN pr.status = 'not_assigned' THEN 1
WHEN pr.status = 'assigned' THEN 2
WHEN pr.status = 'done' THEN 3
WHEN pr.status = 'delivered' THEN 4
ELSE 5
END ASC,
pr.required_by ASC
");
$data['print_requests'] = $query->getResultArray();
return view('print_requests/admin_index', $data);
}
public function create()
{
$validationRules = [
'file' => 'uploaded[file]|max_size[file,2048]|ext_in[file,pdf,jpg,png,jpeg,doc,docx,txt]',
'page_selection' => 'permit_empty|regex_match[/^\\s*\\d+(?:\\s*-\\s*\\d+)?(?:\\s*,\\s*\\d+(?:\\s*-\\s*\\d+)?)*\\s*$/]',
'num_copies' => 'required|integer|greater_than[0]',
'required_by' => 'required|valid_date',
'pickup_method' => 'required'
];
if (!$this->validate($validationRules)) {
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
}
$teacher_id = session()->get('user_id');
$file = $this->request->getFile('file');
$newName = '';
if ($file->isValid() && !$file->hasMoved()) {
$newName = $file->getRandomName();
// Moved to WRITEPATH
$file->move($this->uploadPath, $newName);
}
$data = [
'teacher_id' => $teacher_id,
'class_id' => $this->request->getPost('class_id'),
'file_path' => $newName,
'page_selection' => trim((string) $this->request->getPost('page_selection')),
'num_copies' => $this->request->getPost('num_copies'),
'required_by' => $this->request->getPost('required_by'),
'pickup_method' => $this->request->getPost('pickup_method'),
'status' => 'not_assigned',
];
$printRequestId = (int) $this->printRequestModel->insert($data, true);
if ($printRequestId > 0) {
$this->notifyAdminsForPrintRequest($printRequestId, $data, 'created');
}
return redirect()->to('teacher/print-requests')->with('success', 'Print request created successfully.');
}
public function update($id)
{
$request = $this->printRequestModel->find($id);
if (!$request) {
return redirect()->back()->with('error', 'Print request not found.');
}
// Case 1: Admin status update
if ($this->request->getPost('status')) {
$user_id = session()->get('user_id');
$current_status = $request['status'];
$new_status = $this->request->getPost('status');
$allowed_transitions = [
'not_assigned' => ['assigned'],
'assigned' => ['not_assigned', 'done'],
'done' => ['delivered'],
'delivered' => []
];
if (!isset($allowed_transitions[$current_status]) || !in_array($new_status, $allowed_transitions[$current_status])) {
if ($current_status == $new_status) {
return redirect()->to('admin/print-requests');
}
return redirect()->to('admin/print-requests')->with('error', 'Invalid status transition.');
}
$data = ['status' => $new_status];
if ($new_status == 'assigned') {
$data['admin_id'] = $user_id;
} elseif ($new_status == 'not_assigned') {
$data['admin_id'] = null;
}
$this->printRequestModel->update($id, $data);
return redirect()->to('admin/print-requests')->with('success', 'Status updated successfully.');
}
// Case 2: Teacher edit
if ($this->request->getPost('num_copies')) {
$user_id = session()->get('user_id');
if ($request['teacher_id'] != $user_id) {
return redirect()->to('teacher/print-requests')->with('error', 'You are not authorized to edit this request.');
}
if (!in_array($request['status'], ['not_assigned', 'assigned'])) {
return redirect()->to('teacher/print-requests')->with('error', 'Cannot edit a request that is already being processed.');
}
$validationRules = [
'num_copies' => 'required|integer|greater_than[0]',
'required_by' => 'required|valid_date',
'pickup_method' => 'required|in_list[Self Pickup,Delivered to class]'
];
$pageSelectionRule = 'regex_match[/^\\s*\\d+(?:\\s*-\\s*\\d+)?(?:\\s*,\\s*\\d+(?:\\s*-\\s*\\d+)?)*\\s*$/]';
$validationRules['page_selection'] = 'permit_empty|' . $pageSelectionRule;
$hasNewFile = $this->request->getFile('file') && $this->request->getFile('file')->isValid();
if ($hasNewFile) {
$validationRules['file'] = 'uploaded[file]|max_size[file,2048]|ext_in[file,pdf,jpg,png,jpeg,doc,docx,txt]';
}
if (!$this->validate($validationRules)) {
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
}
$data = [
'num_copies' => $this->request->getPost('num_copies'),
'required_by' => $this->request->getPost('required_by'),
'pickup_method' => $this->request->getPost('pickup_method'),
'page_selection' => trim((string) $this->request->getPost('page_selection')),
];
$file = $this->request->getFile('file');
if ($file && $file->isValid() && !$file->hasMoved()) {
// Remove old file from WRITEPATH
if ($request['file_path'] && file_exists($this->uploadPath . $request['file_path'])) {
@unlink($this->uploadPath . $request['file_path']);
}
$newName = $file->getRandomName();
$file->move($this->uploadPath, $newName);
$data['file_path'] = $newName;
}
$this->printRequestModel->update($id, $data);
$notifyPayload = array_merge($request, $data, ['id' => $id]);
$this->notifyAdminsForPrintRequest($id, $notifyPayload, 'updated');
return redirect()->to('teacher/print-requests')->with('success', 'Print request updated successfully.');
}
return redirect()->back()->with('error', 'Invalid request data.');
}
public function delete($id)
{
$teacher_id = session()->get('user_id');
$request = $this->printRequestModel->find($id);
if (!$request) {
return redirect()->to('teacher/print-requests')->with('error', 'Print request not found.');
}
if ($request['teacher_id'] != $teacher_id) {
return redirect()->to('teacher/print-requests')->with('error', 'You are not authorized to delete this request.');
}
if (!in_array($request['status'], ['not_assigned', 'assigned'])) {
return redirect()->to('teacher/print-requests')->with('error', 'Cannot delete a request that is already being processed.');
}
$this->notifyAdminsForPrintRequest($id, $request, 'deleted');
// Delete file from WRITEPATH
if ($request['file_path'] && file_exists($this->uploadPath . $request['file_path'])) {
@unlink($this->uploadPath . $request['file_path']);
}
$this->printRequestModel->delete($id);
return redirect()->to('teacher/print-requests')->with('success', 'Print request deleted successfully.');
}
public function copy($id)
{
$teacher_id = session()->get('user_id');
$request = $this->printRequestModel->find($id);
if (!$request) {
return redirect()->to('teacher/print-requests')->with('error', 'Print request not found.');
}
if ($request['teacher_id'] != $teacher_id) {
return redirect()->to('teacher/print-requests')->with('error', 'You are not authorized to copy this request.');
}
$filePath = trim((string) ($request['file_path'] ?? ''));
$candidates = [
$this->uploadPath . $filePath,
FCPATH . 'uploads/print_requests/' . $filePath,
];
$fileExists = false;
foreach ($candidates as $candidate) {
if ($filePath !== '' && file_exists($candidate)) {
$fileExists = true;
break;
}
}
if (!$fileExists) {
return redirect()->to('teacher/print-requests')->with('error', 'The original file is unavailable for copying.');
}
$data = [
'teacher_id' => $teacher_id,
'class_id' => $request['class_id'],
'file_path' => $filePath,
'page_selection' => $request['page_selection'] ?? null,
'num_copies' => $request['num_copies'],
'required_by' => $request['required_by'],
'pickup_method' => $request['pickup_method'],
'status' => 'not_assigned',
];
$copiedId = (int) $this->printRequestModel->insert($data, true);
if ($copiedId > 0) {
$this->notifyAdminsForPrintRequest($copiedId, $data, 'created');
}
return redirect()->to('teacher/print-requests')->with('success', 'Print request copied successfully.');
}
public function createCopy()
{
$validationRules = [
'num_copies' => 'required|integer|greater_than[0]',
'required_by' => 'required|valid_date',
'pickup_method' => 'required'
];
if (!$this->validate($validationRules)) {
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
}
$teacher_id = session()->get('user_id');
$data = [
'teacher_id' => $teacher_id,
'class_id' => $this->request->getPost('class_id'),
'file_path' => '',
'page_selection' => null,
'num_copies' => $this->request->getPost('num_copies'),
'required_by' => $this->request->getPost('required_by'),
'pickup_method' => $this->request->getPost('pickup_method'),
'status' => 'not_assigned',
];
$printRequestId = (int) $this->printRequestModel->insert($data, true);
if ($printRequestId > 0) {
$this->notifyAdminsForPrintRequest($printRequestId, $data, 'created');
}
return redirect()->to('teacher/print-requests')->with('success', 'Copy request submitted. Please hand the original document to the copy center.');
}
private function buildRequiredByOptions(): array
{
$tz = new \DateTimeZone('America/Chicago');
$currentDate = new \DateTime('now', $tz);
$currentYear = (int)$currentDate->format('Y');
$endYear = ($currentDate->format('n') > 6) ? $currentYear + 1 : $currentYear;
$endDate = new \DateTime("first Sunday of June {$endYear}", $tz);
$endDate->modify('+1 week');
$sundays = [];
$date = new \DateTime('now', $tz);
$date->setTime(0, 0, 0);
if ($date->format('w') !== '0') {
$date->modify('next Sunday');
}
while ($date < $endDate) {
$sundays[] = $date->format('Y-m-d');
$date->modify('+1 week');
}
$times = [];
$start = new \DateTime('10:00', $tz);
$end = new \DateTime('13:00', $tz);
$interval = new \DateInterval('PT15M');
$period = new \DatePeriod($start, $interval, $end);
foreach ($period as $time) {
$times[] = $time->format('H:i');
}
return [
'sundays' => $sundays,
'times' => $times,
];
}
public function serveFile(string $filename, string $mode = 'inline')
{
$safeName = basename(trim($filename));
if ($safeName === '') {
throw new PageNotFoundException('File not specified.');
}
$candidates = [
WRITEPATH . 'uploads/print_requests/' . $safeName,
FCPATH . 'uploads/print_requests/' . $safeName,
];
$path = null;
foreach ($candidates as $candidate) {
if (is_file($candidate)) {
$path = $candidate;
break;
}
}
if (!$path) {
throw new PageNotFoundException('File not found.');
}
$mime = mime_content_type($path) ?: 'application/octet-stream';
$disposition = strtolower(trim($mode)) === 'download' ? 'attachment' : 'inline';
return $this->response
->download($path, null)
->setFileName($safeName)
->setHeader('Content-Type', $mime)
->setHeader('Content-Disposition', sprintf('%s; filename="%s"', $disposition, $safeName));
}
private function notifyAdminsForPrintRequest(int $printRequestId, array $requestData, string $event = 'created'): void
{
try {
$db = \Config\Database::connect();
} catch (\Throwable $e) {
return;
}
if (
!$db->tableExists('admin_notification_subjects') ||
!$db->tableExists('notifications') ||
!$db->tableExists('user_notifications')
) {
log_message('error', 'Print request notifications skipped: required tables missing.');
return;
}
$rows = $this->adminNotificationSubjectModel
->select('admin_id')
->where('subject', 'print_requests')
->findAll();
$adminIds = array_values(array_unique(array_map('intval', array_column($rows, 'admin_id'))));
if (empty($adminIds)) {
log_message('info', 'Print request notifications skipped: no admins subscribed to print_requests.');
return;
}
$teacherId = (int) ($requestData['teacher_id'] ?? 0);
$teacherName = '';
if ($teacherId > 0) {
$teacher = $this->userModel->find($teacherId);
$teacherName = trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? ''));
}
if ($teacherName === '') {
$teacherName = $teacherId > 0 ? ('Teacher #' . $teacherId) : 'Teacher';
}
$className = '';
$classId = $requestData['class_id'] ?? null;
if ($classId !== null && $classId !== '') {
$className = (string) ($this->classSectionModel->getClassSectionNameBySectionId($classId) ?? '');
}
$event = strtolower(trim($event));
$eventMap = [
'created' => [
'title' => 'New Print Request',
'intro' => 'A new print request has been submitted.',
'lead' => 'New print request from ',
],
'updated' => [
'title' => 'Print Request Updated',
'intro' => 'A print request has been updated.',
'lead' => 'Updated print request from ',
],
'deleted' => [
'title' => 'Print Request Removed',
'intro' => 'A print request has been deleted.',
'lead' => 'Print request removed for ',
],
];
$eventConfig = $eventMap[$event] ?? $eventMap['created'];
$filePath = trim((string) ($requestData['file_path'] ?? ''));
$isCopyRequest = $filePath === '';
$messageParts = [$eventConfig['lead'] . $teacherName];
if ($className !== '') {
$messageParts[] = 'Class: ' . $className;
}
if (!empty($requestData['num_copies'])) {
$messageParts[] = 'Copies: ' . (int) $requestData['num_copies'];
}
if (!empty($requestData['required_by'])) {
$messageParts[] = 'Needed by: ' . $requestData['required_by'];
}
if (!empty($requestData['page_selection'])) {
$messageParts[] = 'Pages: ' . $requestData['page_selection'];
}
if (!empty($requestData['pickup_method'])) {
$messageParts[] = 'Pickup: ' . $requestData['pickup_method'];
}
$message = implode(' | ', $messageParts);
$actionUrl = site_url('admin/print-requests');
$payload = [
'title' => $eventConfig['title'],
'message' => $message,
'target_group' => 'admin',
'delivery_channels' => 'in_app,email',
'priority' => 'normal',
'status' => 'pending',
'action_url' => $isCopyRequest ? '' : $actionUrl,
'scheduled_at' => utc_now(),
'school_year' => $this->configModel->getConfig('school_year'),
'semester' => $this->configModel->getConfig('semester'),
];
$notificationFields = $db->getFieldNames('notifications');
if (is_array($notificationFields) && !empty($notificationFields)) {
$payload = array_intersect_key($payload, array_flip($notificationFields));
}
$notificationId = (int) $this->notificationModel->insert($payload, true);
if ($notificationId <= 0) {
return;
}
$userNotificationFields = $db->getFieldNames('user_notifications');
$userFieldMap = is_array($userNotificationFields) ? array_flip($userNotificationFields) : [];
$batch = [];
foreach ($adminIds as $adminId) {
if ($adminId <= 0) {
continue;
}
$row = [
'notification_id' => $notificationId,
'user_id' => $adminId,
'is_read' => 0,
'delivered' => 0,
];
if (!empty($userFieldMap)) {
$row = array_intersect_key($row, $userFieldMap);
}
$batch[] = $row;
}
if (!empty($batch)) {
$this->userNotificationModel->insertBatch($batch);
}
$adminRows = $this->userModel
->select('id, firstname, lastname, email')
->whereIn('id', $adminIds)
->findAll();
if (empty($adminRows)) {
return;
}
$mailer = new \App\Controllers\View\EmailController();
$subject = $eventConfig['title'];
$body = '<p>' . esc($eventConfig['intro']) . '</p>'
. '<p><strong>From:</strong> ' . esc($teacherName) . '</p>'
. ($className !== '' ? '<p><strong>Class:</strong> ' . esc($className) . '</p>' : '')
. (!empty($requestData['num_copies']) ? '<p><strong>Copies:</strong> ' . (int) $requestData['num_copies'] . '</p>' : '')
. (!empty($requestData['required_by']) ? '<p><strong>Needed by:</strong> ' . esc($requestData['required_by']) . '</p>' : '')
. (!empty($requestData['page_selection']) ? '<p><strong>Pages:</strong> ' . esc($requestData['page_selection']) . '</p>' : '')
. (!empty($requestData['pickup_method']) ? '<p><strong>Pickup Method:</strong> ' . esc($requestData['pickup_method']) . '</p>' : '')
. ($isCopyRequest ? '' : '<p><a href="' . esc($actionUrl) . '">View print requests</a></p>');
foreach ($adminRows as $adminRow) {
$email = trim((string) ($adminRow['email'] ?? ''));
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
continue;
}
$mailer->sendEmail($email, $subject, $body, 'notifications');
}
}
}