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 = '

' . esc($eventConfig['intro']) . '

' . '

From: ' . esc($teacherName) . '

' . ($className !== '' ? '

Class: ' . esc($className) . '

' : '') . (!empty($requestData['num_copies']) ? '

Copies: ' . (int) $requestData['num_copies'] . '

' : '') . (!empty($requestData['required_by']) ? '

Needed by: ' . esc($requestData['required_by']) . '

' : '') . (!empty($requestData['page_selection']) ? '

Pages: ' . esc($requestData['page_selection']) . '

' : '') . (!empty($requestData['pickup_method']) ? '

Pickup Method: ' . esc($requestData['pickup_method']) . '

' : '') . ($isCopyRequest ? '' : '

View print requests

'); foreach ($adminRows as $adminRow) { $email = trim((string) ($adminRow['email'] ?? '')); if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { continue; } $mailer->sendEmail($email, $subject, $body, 'notifications'); } } }