fix logic and tests, update docker CI file
This commit is contained in:
@@ -3,10 +3,11 @@
|
||||
namespace App\Http\Controllers\Api\Administrator;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Administrator\SubmitAdministratorAbsenceRequest;
|
||||
use App\Services\Administrator\AdministratorAbsenceService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class AdministratorAbsenceController extends Controller
|
||||
{
|
||||
@@ -25,13 +26,42 @@ class AdministratorAbsenceController extends Controller
|
||||
return response()->json($this->service->getAbsenceFormData($userId));
|
||||
}
|
||||
|
||||
public function store(SubmitAdministratorAbsenceRequest $request): JsonResponse
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$userId = (int) Auth::id();
|
||||
if ($userId <= 0) {
|
||||
return response()->json(['message' => 'Please log in first.'], 401);
|
||||
}
|
||||
|
||||
$payload = $request->all();
|
||||
if (!array_key_exists('dates', $payload) || !array_key_exists('reason', $payload)) {
|
||||
$json = json_decode($request->getContent() ?: '', true);
|
||||
if (is_array($json)) {
|
||||
$payload = array_merge($payload, $json);
|
||||
}
|
||||
}
|
||||
|
||||
$validator = Validator::make($payload, [
|
||||
'dates' => ['required', 'array', 'min:1'],
|
||||
'dates.*' => ['required', 'date_format:Y-m-d'],
|
||||
'reason_type' => ['nullable', 'string', 'max:100'],
|
||||
'reason' => ['required', 'string', 'max:2000'],
|
||||
], [
|
||||
'dates.required' => 'At least one date is required.',
|
||||
'dates.array' => 'Dates must be submitted as an array.',
|
||||
'dates.min' => 'At least one date is required.',
|
||||
'dates.*.date_format' => 'Each date must be in Y-m-d format.',
|
||||
'reason.required' => 'Reason is required.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$request->merge($validator->validated());
|
||||
$result = $this->service->submit($request, $userId);
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
namespace App\Http\Controllers\Api\Administrator;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Administrator\UpdateEnrollmentStatusesRequest;
|
||||
use App\Services\Administrator\AdministratorEnrollmentService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class AdministratorEnrollmentController extends Controller
|
||||
{
|
||||
@@ -30,14 +31,53 @@ class AdministratorEnrollmentController extends Controller
|
||||
return response()->json($this->service->showNewStudentsData());
|
||||
}
|
||||
|
||||
public function updateStatuses(UpdateEnrollmentStatusesRequest $request): JsonResponse
|
||||
public function updateStatuses(Request $request): JsonResponse
|
||||
{
|
||||
$editorUserId = (int) Auth::id();
|
||||
if ($editorUserId <= 0) {
|
||||
return response()->json(['message' => 'Please log in first.'], 401);
|
||||
}
|
||||
|
||||
$data = $request->validated();
|
||||
$payload = $request->all();
|
||||
if (!array_key_exists('enrollment_status', $payload)) {
|
||||
$json = json_decode($request->getContent() ?: '', true);
|
||||
if (is_array($json) && array_key_exists('enrollment_status', $json)) {
|
||||
$payload['enrollment_status'] = $json['enrollment_status'];
|
||||
}
|
||||
}
|
||||
if (!array_key_exists('enrollment_status', $payload)) {
|
||||
$payload['enrollment_status'] = $request->query('enrollment_status');
|
||||
}
|
||||
|
||||
$allowedStatuses = [
|
||||
'admission under review',
|
||||
'payment pending',
|
||||
'enrolled',
|
||||
'withdraw under review',
|
||||
'refund pending',
|
||||
'withdrawn',
|
||||
'denied',
|
||||
'waitlist',
|
||||
];
|
||||
|
||||
$validator = Validator::make($payload, [
|
||||
'enrollment_status' => ['required', 'array', 'min:1'],
|
||||
'enrollment_status.*' => ['required', 'string', Rule::in($allowedStatuses)],
|
||||
], [
|
||||
'enrollment_status.required' => 'Enrollment statuses are required.',
|
||||
'enrollment_status.array' => 'Enrollment statuses must be an array.',
|
||||
'enrollment_status.min' => 'At least one enrollment status is required.',
|
||||
'enrollment_status.*.in' => 'One or more enrollment statuses are invalid.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
$result = $this->service->updateStatuses(
|
||||
(array) ($data['enrollment_status'] ?? []),
|
||||
$editorUserId
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace App\Http\Controllers\Api\Administrator;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Administrator\SaveAdminNotificationSubjectsRequest;
|
||||
use App\Http\Requests\Administrator\SavePrintRecipientsRequest;
|
||||
use App\Services\Administrator\AdministratorNotificationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class AdministratorNotificationController extends Controller
|
||||
{
|
||||
@@ -20,9 +20,31 @@ class AdministratorNotificationController extends Controller
|
||||
return response()->json($this->service->notificationsAlertsData());
|
||||
}
|
||||
|
||||
public function saveAlerts(SaveAdminNotificationSubjectsRequest $request): JsonResponse
|
||||
public function saveAlerts(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$payload = $request->all();
|
||||
if (!array_key_exists('subjects', $payload)) {
|
||||
$json = json_decode($request->getContent() ?: '', true);
|
||||
if (is_array($json)) {
|
||||
$payload = array_merge($payload, $json);
|
||||
}
|
||||
}
|
||||
|
||||
$validator = Validator::make($payload, [
|
||||
'subjects' => ['required', 'array'],
|
||||
], [
|
||||
'subjects.required' => 'Subjects payload is required.',
|
||||
'subjects.array' => 'Subjects payload must be an array.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
$result = $this->service->saveNotificationSubjects(
|
||||
(array) ($data['subjects'] ?? [])
|
||||
);
|
||||
@@ -35,9 +57,31 @@ class AdministratorNotificationController extends Controller
|
||||
return response()->json($this->service->printNotificationRecipientsData());
|
||||
}
|
||||
|
||||
public function savePrintRecipients(SavePrintRecipientsRequest $request): JsonResponse
|
||||
public function savePrintRecipients(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$payload = $request->all();
|
||||
if (!array_key_exists('notify', $payload)) {
|
||||
$json = json_decode($request->getContent() ?: '', true);
|
||||
if (is_array($json)) {
|
||||
$payload = array_merge($payload, $json);
|
||||
}
|
||||
}
|
||||
|
||||
$validator = Validator::make($payload, [
|
||||
'notify' => ['required', 'array'],
|
||||
], [
|
||||
'notify.required' => 'Notify payload is required.',
|
||||
'notify.array' => 'Notify payload must be an array.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
$result = $this->service->savePrintNotificationRecipients(
|
||||
(array) ($data['notify'] ?? [])
|
||||
);
|
||||
|
||||
+20
-2
@@ -3,10 +3,11 @@
|
||||
namespace App\Http\Controllers\Api\Administrator;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Administrator\SendTeacherSubmissionNotificationsRequest;
|
||||
use App\Services\Administrator\AdministratorTeacherSubmissionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class AdministratorTeacherSubmissionController extends Controller
|
||||
{
|
||||
@@ -20,13 +21,30 @@ class AdministratorTeacherSubmissionController extends Controller
|
||||
return response()->json($this->service->report());
|
||||
}
|
||||
|
||||
public function notify(SendTeacherSubmissionNotificationsRequest $request): JsonResponse
|
||||
public function notify(Request $request): JsonResponse
|
||||
{
|
||||
$adminId = (int) Auth::id();
|
||||
if ($adminId <= 0) {
|
||||
return response()->json(['message' => 'Please log in first.'], 401);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'notify' => ['required', 'array', 'min:1'],
|
||||
'missing_items' => ['nullable', 'array'],
|
||||
], [
|
||||
'notify.required' => 'Select at least one teacher to notify.',
|
||||
'notify.array' => 'Notify payload must be an array.',
|
||||
'notify.min' => 'Select at least one teacher to notify.',
|
||||
'missing_items.array' => 'Missing items payload must be an array.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$result = $this->service->sendNotifications($request, $adminId);
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
namespace App\Http\Controllers\Api\Assignment;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Assignment\StoreAssignmentRequest;
|
||||
use App\Http\Resources\Assignment\AssignmentOverviewResource;
|
||||
use App\Http\Resources\Assignment\AssignmentSectionResource;
|
||||
use App\Services\Assignment\AssignmentService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class AssignmentApiController extends Controller
|
||||
{
|
||||
@@ -29,10 +29,25 @@ class AssignmentApiController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreAssignmentRequest $request): JsonResponse
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'student_id' => ['required', 'integer', 'exists:students,id'],
|
||||
'class_section_id' => ['required', 'integer', 'exists:classSection,class_section_id'],
|
||||
'semester' => ['required', 'string', 'max:50'],
|
||||
'school_year' => ['required', 'string', 'max:50'],
|
||||
'description' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$assignment = $this->assignmentService->storeAssignment(
|
||||
data: $request->validated(),
|
||||
data: $validator->validated(),
|
||||
updatedBy: $request->user()?->id
|
||||
);
|
||||
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
namespace App\Http\Controllers\Api\Attendance;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\AttendanceCommentTemplate\StoreAttendanceCommentTemplateRequest;
|
||||
use App\Http\Requests\AttendanceCommentTemplate\UpdateAttendanceCommentTemplateRequest;
|
||||
use App\Services\AttendanceCommentTemplateService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class AttendanceCommentTemplateController extends Controller
|
||||
{
|
||||
@@ -49,9 +48,28 @@ class AttendanceCommentTemplateController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(StoreAttendanceCommentTemplateRequest $request): JsonResponse
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$created = $this->service->create($request->validated());
|
||||
$validator = Validator::make($request->all(), [
|
||||
'min_score' => ['required', 'integer', 'min:0', 'max:100'],
|
||||
'max_score' => ['required', 'integer', 'min:0', 'max:100', 'gte:min_score'],
|
||||
'template_text' => ['required', 'string', 'max:5000'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
if (!array_key_exists('is_active', $data)) {
|
||||
$data['is_active'] = true;
|
||||
}
|
||||
|
||||
$created = $this->service->create($data);
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
@@ -60,9 +78,31 @@ class AttendanceCommentTemplateController extends Controller
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function update(UpdateAttendanceCommentTemplateRequest $request, int $id): JsonResponse
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$updated = $this->service->update($id, $request->validated());
|
||||
$validator = Validator::make($request->all(), [
|
||||
'min_score' => ['sometimes', 'integer', 'min:0', 'max:100'],
|
||||
'max_score' => ['sometimes', 'integer', 'min:0', 'max:100'],
|
||||
'template_text' => ['sometimes', 'string', 'max:5000'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$validator->after(function ($validator) use ($request) {
|
||||
$min = $request->has('min_score') ? (int) $request->input('min_score') : null;
|
||||
$max = $request->has('max_score') ? (int) $request->input('max_score') : null;
|
||||
if ($min !== null && $max !== null && $max < $min) {
|
||||
$validator->errors()->add('max_score', 'The max_score field must be greater than or equal to min_score.');
|
||||
}
|
||||
});
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$updated = $this->service->update($id, $validator->validated());
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
|
||||
@@ -3,14 +3,10 @@
|
||||
namespace App\Http\Controllers\Api\AttendanceTracking;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\AttendanceTracking\ComposeAttendanceEmailRequest;
|
||||
use App\Http\Requests\AttendanceTracking\ParentsInfoRequest;
|
||||
use App\Http\Requests\AttendanceTracking\RecordAttendanceTrackingRequest;
|
||||
use App\Http\Requests\AttendanceTracking\SaveAttendanceNotificationNoteRequest;
|
||||
use App\Http\Requests\AttendanceTracking\SendAttendanceManualEmailRequest;
|
||||
use App\Services\AttendanceTracking\AttendanceTrackingService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class AttendanceTrackingController extends Controller
|
||||
{
|
||||
@@ -63,9 +59,26 @@ class AttendanceTrackingController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
public function record(RecordAttendanceTrackingRequest $request): JsonResponse
|
||||
public function record(Request $request): JsonResponse
|
||||
{
|
||||
$result = $this->service->record($request->validated());
|
||||
$validator = Validator::make($request->all(), [
|
||||
'student_id' => ['required', 'integer', 'min:1'],
|
||||
'date' => ['required', 'date_format:Y-m-d'],
|
||||
'parent_email' => ['nullable', 'email'],
|
||||
'parent_name' => ['nullable', 'string'],
|
||||
'subject_type' => ['nullable', 'string'],
|
||||
'semester' => ['nullable', 'string'],
|
||||
'school_year' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$result = $this->service->record($validator->validated());
|
||||
|
||||
return response()->json(
|
||||
$result,
|
||||
@@ -80,13 +93,28 @@ class AttendanceTrackingController extends Controller
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
public function compose(ComposeAttendanceEmailRequest $request): JsonResponse
|
||||
public function compose(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->query(), [
|
||||
'student_id' => ['required', 'integer', 'min:1'],
|
||||
'code' => ['nullable', 'string'],
|
||||
'variant' => ['nullable', 'string'],
|
||||
'incident_date' => ['nullable', 'date_format:Y-m-d'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
$result = $this->service->compose(
|
||||
(int) $request->validated('student_id'),
|
||||
(string) $request->validated('code', 'ABS_1'),
|
||||
(string) $request->validated('variant', 'default'),
|
||||
$request->validated('incident_date'),
|
||||
(int) ($data['student_id'] ?? 0),
|
||||
(string) ($data['code'] ?? 'ABS_1'),
|
||||
(string) ($data['variant'] ?? 'default'),
|
||||
$data['incident_date'] ?? null,
|
||||
);
|
||||
|
||||
return response()->json(
|
||||
@@ -95,9 +123,26 @@ class AttendanceTrackingController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
public function sendManualEmail(SendAttendanceManualEmailRequest $request): JsonResponse
|
||||
public function sendManualEmail(Request $request): JsonResponse
|
||||
{
|
||||
$result = $this->service->sendEmailManual($request->validated());
|
||||
$validator = Validator::make($request->all(), [
|
||||
'student_id' => ['required', 'integer', 'min:1'],
|
||||
'to' => ['required', 'email'],
|
||||
'subject' => ['required', 'string'],
|
||||
'body_html' => ['required', 'string'],
|
||||
'code' => ['required', 'string'],
|
||||
'variant' => ['nullable', 'string'],
|
||||
'incident_date' => ['nullable', 'date_format:Y-m-d'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$result = $this->service->sendEmailManual($validator->validated());
|
||||
|
||||
return response()->json(
|
||||
$result,
|
||||
@@ -105,9 +150,21 @@ class AttendanceTrackingController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
public function parentsInfo(ParentsInfoRequest $request): JsonResponse
|
||||
public function parentsInfo(Request $request): JsonResponse
|
||||
{
|
||||
$result = $this->service->parentsInfo((int) $request->validated('student_id'));
|
||||
$validator = Validator::make($request->query(), [
|
||||
'student_id' => ['required', 'integer', 'min:1'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
$result = $this->service->parentsInfo((int) ($data['student_id'] ?? 0));
|
||||
|
||||
return response()->json(
|
||||
$result,
|
||||
@@ -115,9 +172,26 @@ class AttendanceTrackingController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
public function saveNotificationNote(SaveAttendanceNotificationNoteRequest $request): JsonResponse
|
||||
public function saveNotificationNote(Request $request): JsonResponse
|
||||
{
|
||||
$result = $this->service->saveNotificationNote($request->validated());
|
||||
$validator = Validator::make($request->all(), [
|
||||
'student_id' => ['required', 'integer', 'min:1'],
|
||||
'code' => ['required', 'string'],
|
||||
'note' => ['required', 'string'],
|
||||
'incident_date' => ['nullable', 'date_format:Y-m-d'],
|
||||
'to' => ['nullable', 'email'],
|
||||
'subject' => ['nullable', 'string'],
|
||||
'variant' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$result = $this->service->saveNotificationNote($validator->validated());
|
||||
|
||||
return response()->json(
|
||||
$result,
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
namespace App\Http\Controllers\Api\Auth;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Auth\RegisterRequest;
|
||||
use App\Http\Resources\Auth\RegisterResource;
|
||||
use App\Services\Auth\RegistrationCaptchaService;
|
||||
use App\Services\Auth\RegistrationService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class RegisterController extends BaseApiController
|
||||
{
|
||||
@@ -28,7 +29,7 @@ class RegisterController extends BaseApiController
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(RegisterRequest $request): JsonResponse
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
if (app()->runningUnitTests() && $request->header('X-Debug-Request') === '1') {
|
||||
return response()->json([
|
||||
@@ -43,7 +44,15 @@ class RegisterController extends BaseApiController
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $this->service->register($request->validated());
|
||||
$validator = Validator::make($request->all(), \App\Http\Requests\Auth\RegisterRequest::ruleset());
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$result = $this->service->register($validator->validated());
|
||||
|
||||
if (empty($result['ok'])) {
|
||||
$code = $result['code'] ?? 'registration_failed';
|
||||
|
||||
@@ -3,13 +3,12 @@
|
||||
namespace App\Http\Controllers\Api\Badges;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Badges\BadgePrintStatusRequest;
|
||||
use App\Http\Requests\Badges\GenerateBadgePdfRequest;
|
||||
use App\Http\Requests\Badges\LogBadgePrintRequest;
|
||||
use App\Services\Badges\BadgeFormDataService;
|
||||
use App\Services\Badges\BadgePdfService;
|
||||
use App\Services\Badges\BadgePrintLogService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Throwable;
|
||||
|
||||
class BadgeController extends Controller
|
||||
@@ -21,24 +20,41 @@ class BadgeController extends Controller
|
||||
) {
|
||||
}
|
||||
|
||||
public function generatePdf(GenerateBadgePdfRequest $request)
|
||||
public function generatePdf(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'user_ids' => ['required', 'array', 'min:1'],
|
||||
'user_ids.*' => ['integer', 'min:1'],
|
||||
'school_year' => ['nullable', 'string', 'max:50'],
|
||||
'roles' => ['nullable', 'array'],
|
||||
'classes' => ['nullable', 'array'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
|
||||
return $this->badgePdfService->generate(
|
||||
userIds: $request->input('user_ids', []),
|
||||
schoolYear: $request->input('school_year'),
|
||||
rolesMap: $request->input('roles', []),
|
||||
classesMap: $request->input('classes', []),
|
||||
userIds: $data['user_ids'] ?? [],
|
||||
schoolYear: $data['school_year'] ?? null,
|
||||
rolesMap: $data['roles'] ?? [],
|
||||
classesMap: $data['classes'] ?? [],
|
||||
actorId: optional($request->user())->id
|
||||
);
|
||||
}
|
||||
|
||||
public function printStatus(BadgePrintStatusRequest $request): JsonResponse
|
||||
public function printStatus(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->badgePrintLogService->getStatus(
|
||||
$request->normalizedUserIds(),
|
||||
$this->parseUserIds($request),
|
||||
$request->input('school_year')
|
||||
),
|
||||
]);
|
||||
@@ -50,15 +66,32 @@ class BadgeController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function logPrint(LogBadgePrintRequest $request): JsonResponse
|
||||
public function logPrint(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validator = Validator::make($request->all(), [
|
||||
'user_ids' => ['required', 'array', 'min:1'],
|
||||
'user_ids.*' => ['integer', 'min:1'],
|
||||
'school_year' => ['nullable', 'string', 'max:50'],
|
||||
'roles' => ['nullable', 'array'],
|
||||
'classes' => ['nullable', 'array'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$data = $validator->validated();
|
||||
|
||||
$inserted = $this->badgePrintLogService->log(
|
||||
userIds: $request->input('user_ids', []),
|
||||
userIds: $data['user_ids'] ?? [],
|
||||
actorId: optional($request->user())->id,
|
||||
schoolYear: $request->input('school_year'),
|
||||
rolesMap: $request->input('roles', []),
|
||||
classesMap: $request->input('classes', [])
|
||||
schoolYear: $data['school_year'] ?? null,
|
||||
rolesMap: $data['roles'] ?? [],
|
||||
classesMap: $data['classes'] ?? []
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
@@ -73,15 +106,30 @@ class BadgeController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function formData(BadgePrintStatusRequest $request): JsonResponse
|
||||
public function formData(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->badgeFormDataService->build(
|
||||
schoolYear: $request->input('school_year'),
|
||||
selectedUserIds: $request->normalizedUserIds(),
|
||||
selectedUserIds: $this->parseUserIds($request),
|
||||
activeRole: $request->input('active_role')
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function parseUserIds(Request $request): array
|
||||
{
|
||||
$userIds = $request->input('user_ids', $request->query('user_ids', []));
|
||||
|
||||
if (is_string($userIds)) {
|
||||
$userIds = array_filter(array_map('trim', explode(',', $userIds)), 'strlen');
|
||||
} elseif (!is_array($userIds)) {
|
||||
$userIds = $userIds ? [$userIds] : [];
|
||||
}
|
||||
|
||||
$userIds = array_values(array_unique(array_map(static fn ($v) => (int) $v, $userIds)));
|
||||
|
||||
return array_values(array_filter($userIds, static fn ($v) => $v > 0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
namespace App\Http\Controllers\Api\Finance;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Fees\FeeRefundRequest;
|
||||
use App\Http\Requests\Fees\FeeTuitionTotalRequest;
|
||||
use App\Http\Resources\Fees\FeeRefundResource;
|
||||
use App\Http\Resources\Fees\FeeTuitionTotalResource;
|
||||
use App\Services\Fees\FeeRefundCalculatorService;
|
||||
use App\Services\Fees\FeeStudentFeeService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class FeeCalculationController extends BaseApiController
|
||||
{
|
||||
@@ -20,9 +20,26 @@ class FeeCalculationController extends BaseApiController
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function refund(FeeRefundRequest $request): JsonResponse
|
||||
public function refund(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$validator = Validator::make($request->all(), [
|
||||
'parent_id' => ['required', 'integer', 'min:1'],
|
||||
'students' => ['required', 'array'],
|
||||
'students.*.student_id' => ['nullable', 'integer'],
|
||||
'students.*.class_section_id' => ['required', 'integer', 'min:1'],
|
||||
'students.*.enrollment_status' => ['required', 'string', 'max:50'],
|
||||
'students.*.admission_status' => ['required', 'string', 'max:50'],
|
||||
'students.*.withdrawal_date' => ['nullable', 'date'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$result = $this->refunds->calculateRefund($payload['students'], (int) $payload['parent_id']);
|
||||
|
||||
return response()->json([
|
||||
@@ -31,9 +48,22 @@ class FeeCalculationController extends BaseApiController
|
||||
]);
|
||||
}
|
||||
|
||||
public function tuitionTotal(FeeTuitionTotalRequest $request): JsonResponse
|
||||
public function tuitionTotal(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$validator = Validator::make($request->all(), [
|
||||
'students' => ['required', 'array'],
|
||||
'students.*.class_section_id' => ['required', 'integer', 'min:1'],
|
||||
'students.*.grade' => ['nullable', 'string', 'max:20'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$total = $this->tuition->totalTuitionFee($payload['students']);
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Controllers\Api\Finance;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Invoices\InvoiceGenerateRequest;
|
||||
use App\Http\Requests\Invoices\InvoiceManagementRequest;
|
||||
use App\Http\Requests\Invoices\InvoiceParentRequest;
|
||||
use App\Http\Requests\Invoices\InvoiceStatusRequest;
|
||||
@@ -15,6 +14,8 @@ use App\Services\Invoices\InvoiceManagementService;
|
||||
use App\Services\Invoices\InvoicePaymentService;
|
||||
use App\Services\Invoices\InvoicePdfService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class InvoiceController extends BaseApiController
|
||||
@@ -44,9 +45,22 @@ class InvoiceController extends BaseApiController
|
||||
]);
|
||||
}
|
||||
|
||||
public function generate(InvoiceGenerateRequest $request): JsonResponse
|
||||
public function generate(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'parent_id' => ['required', 'integer', 'exists:users,id'],
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$result = $this->generation->generateInvoice((int) $payload['parent_id'], $payload['school_year'] ?? null);
|
||||
|
||||
if (empty($result['ok'])) {
|
||||
|
||||
@@ -4,13 +4,13 @@ namespace App\Http\Controllers\Api\Finance;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Payments\PaymentByParentRequest;
|
||||
use App\Http\Requests\Payments\PaymentCreateRequest;
|
||||
use App\Http\Requests\Payments\PaymentUpdateBalanceRequest;
|
||||
use App\Http\Resources\Payments\PaymentResource;
|
||||
use App\Services\Payments\PaymentBalanceService;
|
||||
use App\Services\Payments\PaymentLookupService;
|
||||
use App\Services\Payments\PaymentPlanService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class PaymentController extends BaseApiController
|
||||
{
|
||||
@@ -22,9 +22,30 @@ class PaymentController extends BaseApiController
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function store(PaymentCreateRequest $request): JsonResponse
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'parent_id' => ['required', 'integer', 'exists:users,id'],
|
||||
'invoice_id' => ['nullable', 'integer', 'exists:invoices,id'],
|
||||
'total_amount' => ['required', 'numeric', 'min:0.01'],
|
||||
'number_of_installments' => ['required', 'integer', 'min:1'],
|
||||
'payment_date' => ['nullable', 'date'],
|
||||
'payment_method' => ['nullable', 'string', 'max:50'],
|
||||
'payment_type' => ['nullable', 'string', 'max:50'],
|
||||
'status' => ['nullable', 'string', 'max:50'],
|
||||
'semester' => ['nullable', 'string', 'max:50'],
|
||||
'school_year' => ['nullable', 'string', 'max:50'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$payment = $this->planService->createPlan($payload);
|
||||
|
||||
return response()->json([
|
||||
@@ -44,9 +65,21 @@ class PaymentController extends BaseApiController
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateBalance(PaymentUpdateBalanceRequest $request, int $paymentId): JsonResponse
|
||||
public function updateBalance(Request $request, int $paymentId): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'paid_amount' => ['required', 'numeric', 'min:0.01'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$updated = $this->balanceService->updateBalance($paymentId, (float) $payload['paid_amount']);
|
||||
|
||||
if (!$updated) {
|
||||
|
||||
@@ -4,9 +4,10 @@ namespace App\Http\Controllers\Api\Finance;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Payments\PaymentEventChargesListRequest;
|
||||
use App\Http\Requests\Payments\PaymentEventChargesStoreRequest;
|
||||
use App\Services\Payments\PaymentEventChargesService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class PaymentEventChargesController extends BaseApiController
|
||||
{
|
||||
@@ -23,9 +24,29 @@ class PaymentEventChargesController extends BaseApiController
|
||||
return response()->json(['ok' => true] + $data);
|
||||
}
|
||||
|
||||
public function store(PaymentEventChargesStoreRequest $request): JsonResponse
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'parent_id' => ['required', 'integer', 'min:1'],
|
||||
'event_name' => ['required', 'string', 'max:255'],
|
||||
'description' => ['nullable', 'string', 'max:1000'],
|
||||
'amount' => ['required', 'numeric', 'min:0'],
|
||||
'semester' => ['required', 'string', 'max:50'],
|
||||
'school_year' => ['required', 'string', 'max:50'],
|
||||
'student_ids' => ['nullable', 'array'],
|
||||
'student_ids.*' => ['integer', 'min:1'],
|
||||
'student_id' => ['nullable', 'integer', 'min:1'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$userId = (int) (auth()->id() ?? 0);
|
||||
|
||||
$count = $this->service->addCharges($payload, $userId);
|
||||
|
||||
@@ -4,12 +4,11 @@ namespace App\Http\Controllers\Api\Finance;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Payments\PaymentManualEditRequest;
|
||||
use App\Http\Requests\Payments\PaymentManualSearchRequest;
|
||||
use App\Http\Requests\Payments\PaymentManualSuggestRequest;
|
||||
use App\Http\Requests\Payments\PaymentManualUpdateRequest;
|
||||
use App\Services\Payments\PaymentManualService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class PaymentManualController extends BaseApiController
|
||||
{
|
||||
@@ -18,9 +17,22 @@ class PaymentManualController extends BaseApiController
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function search(PaymentManualSearchRequest $request): JsonResponse
|
||||
public function search(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'search_term' => ['nullable', 'string', 'max:255'],
|
||||
'school_year' => ['nullable', 'string', 'max:50'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$data = $this->service->search(
|
||||
(string) ($payload['search_term'] ?? ''),
|
||||
$payload['school_year'] ?? null
|
||||
@@ -29,9 +41,21 @@ class PaymentManualController extends BaseApiController
|
||||
return response()->json(['ok' => true] + $data);
|
||||
}
|
||||
|
||||
public function suggest(PaymentManualSuggestRequest $request): JsonResponse
|
||||
public function suggest(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'q' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$items = $this->service->suggest((string) ($payload['q'] ?? ''));
|
||||
|
||||
return response()->json(['ok' => true, 'items' => $items]);
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
namespace App\Http\Controllers\Api\Finance;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Payments\PaymentTransactionCreateRequest;
|
||||
use App\Http\Resources\Payments\PaymentTransactionResource;
|
||||
use App\Services\Payments\PaymentTransactionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class PaymentTransactionController extends BaseApiController
|
||||
{
|
||||
@@ -16,9 +16,31 @@ class PaymentTransactionController extends BaseApiController
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function store(PaymentTransactionCreateRequest $request): JsonResponse
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'transaction_id' => ['required', 'string', 'max:100'],
|
||||
'payment_id' => ['required', 'integer', 'min:1'],
|
||||
'transaction_date' => ['nullable', 'date'],
|
||||
'amount' => ['required', 'numeric', 'min:0.01'],
|
||||
'payment_method' => ['required', 'string', 'max:50'],
|
||||
'payment_status' => ['nullable', 'string', 'max:50'],
|
||||
'transaction_fee' => ['nullable', 'numeric', 'min:0'],
|
||||
'payment_reference' => ['nullable', 'string', 'max:100'],
|
||||
'is_full_payment' => ['nullable', 'boolean'],
|
||||
'school_year' => ['nullable', 'string', 'max:50'],
|
||||
'semester' => ['nullable', 'string', 'max:50'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$transaction = $this->service->create($payload);
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -4,8 +4,6 @@ namespace App\Http\Controllers\Api\Finance;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\PurchaseOrders\PurchaseOrderIndexRequest;
|
||||
use App\Http\Requests\PurchaseOrders\PurchaseOrderReceiveRequest;
|
||||
use App\Http\Requests\PurchaseOrders\PurchaseOrderStoreRequest;
|
||||
use App\Http\Resources\PurchaseOrders\PurchaseOrderItemResource;
|
||||
use App\Http\Resources\PurchaseOrders\PurchaseOrderResource;
|
||||
use App\Models\PurchaseOrder;
|
||||
@@ -14,6 +12,8 @@ use App\Services\PurchaseOrders\PurchaseOrderQueryService;
|
||||
use App\Services\PurchaseOrders\PurchaseOrderReceiveService;
|
||||
use App\Services\PurchaseOrders\PurchaseOrderStatusService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use RuntimeException;
|
||||
|
||||
class PurchaseOrderController extends BaseApiController
|
||||
@@ -63,9 +63,41 @@ class PurchaseOrderController extends BaseApiController
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(PurchaseOrderStoreRequest $request): JsonResponse
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'po_number' => ['required', 'string', 'max:60'],
|
||||
'supplier_id' => ['required', 'integer', 'min:1', 'exists:suppliers,id'],
|
||||
'status' => ['nullable', 'string', 'in:' . implode(',', PurchaseOrder::allowedStatuses())],
|
||||
'order_date' => ['nullable', 'date'],
|
||||
'expected_date' => ['nullable', 'date'],
|
||||
'notes' => ['nullable', 'string', 'max:5000'],
|
||||
'items' => ['required', 'array', 'min:1'],
|
||||
'items.*.supply_id' => ['required', 'integer', 'min:1', 'exists:supplies,id'],
|
||||
'items.*.description' => ['nullable', 'string', 'max:1000'],
|
||||
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
||||
'items.*.unit_cost' => ['required', 'numeric', 'min:0'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
if (!empty($payload['order_date']) && !empty($payload['expected_date'])) {
|
||||
$orderTs = strtotime((string) $payload['order_date']);
|
||||
$expectedTs = strtotime((string) $payload['expected_date']);
|
||||
if ($orderTs !== false && $expectedTs !== false && $expectedTs < $orderTs) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => ['expected_date' => ['The expected date must be on or after the order date.']],
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$po = $this->createService->create($payload);
|
||||
@@ -81,9 +113,23 @@ class PurchaseOrderController extends BaseApiController
|
||||
], 201);
|
||||
}
|
||||
|
||||
public function receive(PurchaseOrderReceiveRequest $request, int $id): JsonResponse
|
||||
public function receive(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'items' => ['required', 'array', 'min:1'],
|
||||
'items.*.id' => ['required', 'integer', 'min:1'],
|
||||
'items.*.quantity' => ['required', 'integer', 'min:1'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
|
||||
$po = PurchaseOrder::query()->find($id);
|
||||
if (!$po) {
|
||||
|
||||
@@ -6,17 +6,13 @@ use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Files\FileNameRequest;
|
||||
use App\Http\Requests\Reimbursements\ReimbursementBatchAdminFileRequest;
|
||||
use App\Http\Requests\Reimbursements\ReimbursementBatchAssignmentRequest;
|
||||
use App\Http\Requests\Reimbursements\ReimbursementBatchCreateRequest;
|
||||
use App\Http\Requests\Reimbursements\ReimbursementBatchEmailRequest;
|
||||
use App\Http\Requests\Reimbursements\ReimbursementBatchLockRequest;
|
||||
use App\Http\Requests\Reimbursements\ReimbursementExportBatchRequest;
|
||||
use App\Http\Requests\Reimbursements\ReimbursementExportRequest;
|
||||
use App\Http\Requests\Reimbursements\ReimbursementIndexRequest;
|
||||
use App\Http\Requests\Reimbursements\ReimbursementMarkDonationRequest;
|
||||
use App\Http\Requests\Reimbursements\ReimbursementProcessRequest;
|
||||
use App\Http\Requests\Reimbursements\ReimbursementStoreRequest;
|
||||
use App\Http\Requests\Reimbursements\ReimbursementUnderProcessingRequest;
|
||||
use App\Http\Requests\Reimbursements\ReimbursementUpdateRequest;
|
||||
use App\Http\Resources\Reimbursements\ReimbursementBatchResource;
|
||||
use App\Http\Resources\Reimbursements\ReimbursementExpenseResource;
|
||||
use App\Http\Resources\Reimbursements\ReimbursementRecipientResource;
|
||||
@@ -37,6 +33,7 @@ use App\Services\Reimbursements\ReimbursementQueryService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use RuntimeException;
|
||||
|
||||
class ReimbursementController extends BaseApiController
|
||||
@@ -69,9 +66,21 @@ class ReimbursementController extends BaseApiController
|
||||
]);
|
||||
}
|
||||
|
||||
public function markDonation(ReimbursementMarkDonationRequest $request): JsonResponse
|
||||
public function markDonation(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'expense_id' => ['required', 'integer', 'min:1'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$userId = (int) (auth()->id() ?? 0);
|
||||
|
||||
try {
|
||||
@@ -87,9 +96,21 @@ class ReimbursementController extends BaseApiController
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
public function createBatch(ReimbursementBatchCreateRequest $request): JsonResponse
|
||||
public function createBatch(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'title' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$userId = (int) (auth()->id() ?? 0);
|
||||
$schoolYear = $this->context->getSchoolYear();
|
||||
$semester = $this->context->getSemester();
|
||||
@@ -289,9 +310,29 @@ class ReimbursementController extends BaseApiController
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(ReimbursementStoreRequest $request): JsonResponse
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'amount' => ['required', 'numeric', 'gt:0'],
|
||||
'reimbursed_to' => ['required', 'integer', 'min:1'],
|
||||
'reimbursement_method' => ['required', 'in:Cash,Check'],
|
||||
'check_number' => ['required_if:reimbursement_method,Check', 'nullable', 'string', 'max:50'],
|
||||
'receipt' => ['required_if:reimbursement_method,Check', 'nullable', 'file', 'max:2048', 'mimes:jpg,jpeg,png,webp,gif,pdf'],
|
||||
'expense_id' => ['nullable', 'integer', 'min:1'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
'semester' => ['nullable', 'string', 'max:20'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$userId = (int) (auth()->id() ?? 0);
|
||||
$receiptName = null;
|
||||
|
||||
@@ -361,14 +402,32 @@ class ReimbursementController extends BaseApiController
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(ReimbursementUpdateRequest $request, int $id): JsonResponse
|
||||
public function update(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$reimb = Reimbursement::query()->find($id);
|
||||
if (!$reimb) {
|
||||
return response()->json(['ok' => false, 'message' => 'Reimbursement not found.'], 404);
|
||||
}
|
||||
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'amount' => ['required', 'numeric', 'gt:0'],
|
||||
'reimbursed_to' => ['required', 'integer', 'min:1'],
|
||||
'reimbursement_method' => ['required', 'in:Cash,Check'],
|
||||
'check_number' => ['required_if:reimbursement_method,Check', 'nullable', 'string', 'max:50'],
|
||||
'receipt' => ['nullable', 'file', 'max:2048', 'mimes:jpg,jpeg,png,webp,gif,pdf'],
|
||||
'remove_receipt' => ['nullable', 'boolean'],
|
||||
'description' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$userId = (int) (auth()->id() ?? 0);
|
||||
|
||||
$receiptName = $reimb->receipt_path;
|
||||
|
||||
@@ -7,7 +7,6 @@ use App\Http\Requests\Incidents\IncidentCancelRequest;
|
||||
use App\Http\Requests\Incidents\IncidentCloseRequest;
|
||||
use App\Http\Requests\Incidents\IncidentListRequest;
|
||||
use App\Http\Requests\Incidents\IncidentStateRequest;
|
||||
use App\Http\Requests\Incidents\IncidentStoreRequest;
|
||||
use App\Http\Resources\Incidents\IncidentAnalysisStudentResource;
|
||||
use App\Http\Resources\Incidents\IncidentGradeResource;
|
||||
use App\Http\Resources\Incidents\IncidentResource;
|
||||
@@ -16,6 +15,8 @@ use App\Services\Incidents\CurrentIncidentService;
|
||||
use App\Services\Incidents\IncidentAnalysisService;
|
||||
use App\Services\Incidents\IncidentHistoryService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class IncidentController extends BaseApiController
|
||||
{
|
||||
@@ -83,9 +84,26 @@ class IncidentController extends BaseApiController
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(IncidentStoreRequest $request): JsonResponse
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'student_id' => ['required', 'integer', 'min:1'],
|
||||
'grade' => ['required', 'integer', 'min:1'],
|
||||
'incident' => ['required', 'string', 'max:255'],
|
||||
'description' => ['required', 'string', 'max:2000'],
|
||||
'school_year' => ['nullable', 'string', 'max:20'],
|
||||
'semester' => ['nullable', 'string', 'max:20'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$result = $this->currentService->addIncident($payload, (int) (auth()->id() ?? 0));
|
||||
|
||||
if (empty($result['ok'])) {
|
||||
@@ -109,9 +127,22 @@ class IncidentController extends BaseApiController
|
||||
]);
|
||||
}
|
||||
|
||||
public function close(IncidentCloseRequest $request, int $incidentId): JsonResponse
|
||||
public function close(Request $request, int $incidentId): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'state_description' => ['required', 'string', 'max:2000'],
|
||||
'action_taken' => ['nullable', 'string', 'max:2000'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$result = $this->currentService->closeIncident(
|
||||
$incidentId,
|
||||
(string) $payload['state_description'],
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
namespace App\Http\Controllers\Api\Scores;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Scores\ScoreClassRequest;
|
||||
use App\Http\Requests\Scores\ScoreUpdateRequest;
|
||||
use App\Http\Resources\Scores\ScoreStudentResource;
|
||||
use App\Models\Student;
|
||||
use App\Services\Scores\ExamScoreService;
|
||||
use App\Services\Scores\SemesterScoreService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class FinalController extends BaseApiController
|
||||
{
|
||||
@@ -20,9 +21,23 @@ class FinalController extends BaseApiController
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index(ScoreClassRequest $request): JsonResponse
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'class_section_id' => ['required', 'integer', 'min:1'],
|
||||
'semester' => ['nullable', 'string', 'max:50'],
|
||||
'school_year' => ['nullable', 'string', 'max:50'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$data = $this->service->list('final_exam', (int) $payload['class_section_id'], $payload['semester'] ?? null, $payload['school_year'] ?? null);
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -4,13 +4,14 @@ namespace App\Http\Controllers\Api\Scores;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Scores\ScoreAddColumnRequest;
|
||||
use App\Http\Requests\Scores\ScoreClassRequest;
|
||||
use App\Http\Requests\Scores\ScoreUpdateRequest;
|
||||
use App\Http\Resources\Scores\ScoreStudentResource;
|
||||
use App\Models\Student;
|
||||
use App\Services\Scores\HomeworkScoreService;
|
||||
use App\Services\Scores\SemesterScoreService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class HomeworkController extends BaseApiController
|
||||
{
|
||||
@@ -21,9 +22,23 @@ class HomeworkController extends BaseApiController
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index(ScoreClassRequest $request): JsonResponse
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'class_section_id' => ['required', 'integer', 'min:1'],
|
||||
'semester' => ['nullable', 'string', 'max:50'],
|
||||
'school_year' => ['nullable', 'string', 'max:50'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$data = $this->service->list(
|
||||
(int) $payload['class_section_id'],
|
||||
$payload['semester'] ?? null,
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
namespace App\Http\Controllers\Api\Scores;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Scores\ScoreClassRequest;
|
||||
use App\Http\Requests\Scores\ScoreUpdateRequest;
|
||||
use App\Http\Resources\Scores\ScoreStudentResource;
|
||||
use App\Models\Student;
|
||||
use App\Services\Scores\ExamScoreService;
|
||||
use App\Services\Scores\SemesterScoreService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class MidtermController extends BaseApiController
|
||||
{
|
||||
@@ -20,9 +21,23 @@ class MidtermController extends BaseApiController
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index(ScoreClassRequest $request): JsonResponse
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'class_section_id' => ['required', 'integer', 'min:1'],
|
||||
'semester' => ['nullable', 'string', 'max:50'],
|
||||
'school_year' => ['nullable', 'string', 'max:50'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$data = $this->service->list('midterm_exam', (int) $payload['class_section_id'], $payload['semester'] ?? null, $payload['school_year'] ?? null);
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
namespace App\Http\Controllers\Api\Scores;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Scores\ScoreClassRequest;
|
||||
use App\Http\Requests\Scores\ScoreUpdateRequest;
|
||||
use App\Http\Resources\Scores\ScoreStudentResource;
|
||||
use App\Models\Student;
|
||||
use App\Services\Scores\ParticipationScoreService;
|
||||
use App\Services\Scores\SemesterScoreService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class ParticipationController extends BaseApiController
|
||||
{
|
||||
@@ -20,9 +21,23 @@ class ParticipationController extends BaseApiController
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index(ScoreClassRequest $request): JsonResponse
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'class_section_id' => ['required', 'integer', 'min:1'],
|
||||
'semester' => ['nullable', 'string', 'max:50'],
|
||||
'school_year' => ['nullable', 'string', 'max:50'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$data = $this->service->list(
|
||||
(int) $payload['class_section_id'],
|
||||
$payload['semester'] ?? null,
|
||||
|
||||
@@ -4,13 +4,14 @@ namespace App\Http\Controllers\Api\Scores;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Scores\ScoreAddColumnRequest;
|
||||
use App\Http\Requests\Scores\ScoreClassRequest;
|
||||
use App\Http\Requests\Scores\ScoreUpdateRequest;
|
||||
use App\Http\Resources\Scores\ScoreStudentResource;
|
||||
use App\Models\Student;
|
||||
use App\Services\Scores\ProjectScoreService;
|
||||
use App\Services\Scores\SemesterScoreService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class ProjectController extends BaseApiController
|
||||
{
|
||||
@@ -21,9 +22,23 @@ class ProjectController extends BaseApiController
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index(ScoreClassRequest $request): JsonResponse
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'class_section_id' => ['required', 'integer', 'min:1'],
|
||||
'semester' => ['nullable', 'string', 'max:50'],
|
||||
'school_year' => ['nullable', 'string', 'max:50'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$data = $this->service->list(
|
||||
(int) $payload['class_section_id'],
|
||||
$payload['semester'] ?? null,
|
||||
|
||||
@@ -4,13 +4,14 @@ namespace App\Http\Controllers\Api\Scores;
|
||||
|
||||
use App\Http\Controllers\Api\BaseApiController;
|
||||
use App\Http\Requests\Scores\ScoreAddColumnRequest;
|
||||
use App\Http\Requests\Scores\ScoreClassRequest;
|
||||
use App\Http\Requests\Scores\ScoreUpdateRequest;
|
||||
use App\Http\Resources\Scores\ScoreStudentResource;
|
||||
use App\Models\Student;
|
||||
use App\Services\Scores\QuizScoreService;
|
||||
use App\Services\Scores\SemesterScoreService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class QuizController extends BaseApiController
|
||||
{
|
||||
@@ -21,9 +22,23 @@ class QuizController extends BaseApiController
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index(ScoreClassRequest $request): JsonResponse
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$payload = $request->validated();
|
||||
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
|
||||
$validator = Validator::make($data, [
|
||||
'class_section_id' => ['required', 'integer', 'min:1'],
|
||||
'semester' => ['nullable', 'string', 'max:50'],
|
||||
'school_year' => ['nullable', 'string', 'max:50'],
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$payload = $validator->validated();
|
||||
$data = $this->service->list(
|
||||
(int) $payload['class_section_id'],
|
||||
$payload['semester'] ?? null,
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use CodeIgniter\HTTP\CURLRequest;
|
||||
use Config\Api as ApiConfig;
|
||||
|
||||
class ApiClient
|
||||
{
|
||||
protected CURLRequest $http;
|
||||
protected ApiConfig $config;
|
||||
|
||||
public function __construct(CURLRequest $http, ApiConfig $config)
|
||||
{
|
||||
$this->http = $http;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function get(string $uri, array $query = [], array $headers = []): array
|
||||
{
|
||||
return $this->request('get', $uri, [
|
||||
'headers' => $this->mergeHeaders($headers),
|
||||
'query' => $query,
|
||||
]);
|
||||
}
|
||||
|
||||
public function post(string $uri, array $data = [], array $headers = []): array
|
||||
{
|
||||
return $this->request('post', $uri, [
|
||||
'headers' => $this->mergeHeaders($headers, ['Content-Type' => 'application/json']),
|
||||
'json' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function put(string $uri, array $data = [], array $headers = []): array
|
||||
{
|
||||
return $this->request('put', $uri, [
|
||||
'headers' => $this->mergeHeaders($headers, ['Content-Type' => 'application/json']),
|
||||
'json' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(string $uri, array $data = [], array $headers = []): array
|
||||
{
|
||||
return $this->request('delete', $uri, [
|
||||
'headers' => $this->mergeHeaders($headers, ['Content-Type' => 'application/json']),
|
||||
'json' => $data,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Low-level access for uncommon HTTP methods or options
|
||||
*/
|
||||
public function request(string $method, string $uri, array $options = []): array
|
||||
{
|
||||
$url = $this->fullUrl($uri);
|
||||
$options['timeout'] = $options['timeout'] ?? $this->config->timeout;
|
||||
|
||||
$response = $this->http->request(strtoupper($method), $url, $options);
|
||||
|
||||
$status = $response->getStatusCode();
|
||||
$body = (string) $response->getBody();
|
||||
|
||||
$decoded = null;
|
||||
if ($this->isJson($body)) {
|
||||
$decoded = json_decode($body, true);
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => $status >= 200 && $status < 300,
|
||||
'status' => $status,
|
||||
'headers' => $response->getHeaderLine('Content-Type'),
|
||||
'data' => $decoded,
|
||||
'raw' => $decoded === null ? $body : null,
|
||||
];
|
||||
}
|
||||
|
||||
protected function fullUrl(string $uri): string
|
||||
{
|
||||
if (preg_match('#^https?://#i', $uri)) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
$base = rtrim($this->config->baseURL ?? '', '/');
|
||||
if ($base === '') {
|
||||
return '/' . ltrim($uri, '/');
|
||||
}
|
||||
return $base . '/' . ltrim($uri, '/');
|
||||
}
|
||||
|
||||
protected function mergeHeaders(array $override = [], array $extra = []): array
|
||||
{
|
||||
$base = $this->config->defaultHeaders ?? [];
|
||||
return array_filter(array_merge($base, $extra, $override), static function ($v) {
|
||||
return $v !== null && $v !== '';
|
||||
});
|
||||
}
|
||||
|
||||
protected function isJson(string $string): bool
|
||||
{
|
||||
json_decode($string);
|
||||
return json_last_error() === JSON_ERROR_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\ClassSectionModel;
|
||||
use App\Models\StudentModel;
|
||||
use App\Models\TeacherModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
|
||||
class ClassController extends BaseController
|
||||
{
|
||||
protected $classsectionModel;
|
||||
protected $studentModel;
|
||||
protected $teacherModel;
|
||||
protected $configModel;
|
||||
protected $semester;
|
||||
protected $schoolYear;
|
||||
|
||||
protected $db;
|
||||
|
||||
public function __construct(private \CodeIgniter\HTTP\IncomingRequest $request)
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->classsectionModel = new ClassSectionModel();
|
||||
|
||||
// Load necessary models
|
||||
$this->studentModel = new StudentModel();
|
||||
$this->teacherModel = new TeacherModel();
|
||||
$this->configModel = new ConfigurationModel();
|
||||
|
||||
// Get the semester from the configuration table
|
||||
$this->semester = $this->configModel->getConfig('semester');
|
||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
// Retrieve all classes from the database
|
||||
$classsection = $this->classsectionModel->getAllClasses();
|
||||
// Pass the classes to the view
|
||||
return view('administrator/class_section', ['classsection' => $classsection]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
// Show the form to create a new class
|
||||
return view('administrator/create_class');
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
// Handle the form submission to create a new class
|
||||
$data = [
|
||||
'name' => $this->request->getPost('name'),
|
||||
'description' => $this->request->getPost('description'),
|
||||
];
|
||||
//$this->classModel->createClass($data);
|
||||
return redirect()->to('/administrator/classes');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
// Retrieve the class details to edit
|
||||
//$class = $this->ClassSectionModel->getClassById($id);
|
||||
//return view('administrator/edit_class', ['class' => $class]);
|
||||
}
|
||||
|
||||
public function updateClass($id)
|
||||
{
|
||||
// Handle the form submission to update an existing class
|
||||
$data = [
|
||||
'name' => $this->request->getPost('name'),
|
||||
'description' => $this->request->getPost('description'),
|
||||
];
|
||||
//$this->classModel->updateClass($id, $data);
|
||||
return redirect()->to('/administrator/classes');
|
||||
}
|
||||
|
||||
public function destroyClass($id)
|
||||
{
|
||||
// Delete the class
|
||||
//$this->classModel->deleteClass($id);
|
||||
return redirect()->to('/administrator/classes');
|
||||
}
|
||||
|
||||
public function addClasses()
|
||||
{
|
||||
// Include the shared database connection file
|
||||
$file = __DIR__ . '/../db_connection.php';
|
||||
|
||||
if (file_exists($file)) {
|
||||
require_once $file;
|
||||
} else {
|
||||
die("Error: Could not find the required file '$file'.");
|
||||
}
|
||||
|
||||
|
||||
$classes = [
|
||||
['class_name' => 'Class 1', 'teacher_id' => 1, 'schedule' => 'Monday 9:00 AM - 10:00 AM', 'capacity' => 30],
|
||||
['class_name' => 'Class 2', 'teacher_id' => 2, 'schedule' => 'Tuesday 9:00 AM - 10:00 AM', 'capacity' => 30],
|
||||
['class_name' => 'Class 3', 'teacher_id' => 3, 'schedule' => 'Wednesday 9:00 AM - 10:00 AM', 'capacity' => 30],
|
||||
['class_name' => 'Class 4', 'teacher_id' => 4, 'schedule' => 'Thursday 9:00 AM - 10:00 AM', 'capacity' => 30],
|
||||
['class_name' => 'Class 5', 'teacher_id' => 5, 'schedule' => 'Friday 9:00 AM - 10:00 AM', 'capacity' => 30],
|
||||
['class_name' => 'Class 6', 'teacher_id' => 6, 'schedule' => 'Monday 10:00 AM - 11:00 AM', 'capacity' => 30],
|
||||
['class_name' => 'Class 7', 'teacher_id' => 7, 'schedule' => 'Tuesday 10:00 AM - 11:00 AM', 'capacity' => 30],
|
||||
['class_name' => 'Class 8', 'teacher_id' => 8, 'schedule' => 'Wednesday 10:00 AM - 11:00 AM', 'capacity' => 30],
|
||||
['class_name' => 'Class 9', 'teacher_id' => 9, 'schedule' => 'Thursday 10:00 AM - 11:00 AM', 'capacity' => 30],
|
||||
['class_name' => 'Youth', 'teacher_id' => 10, 'schedule' => 'Friday 10:00 AM - 11:00 AM', 'capacity' => 30],
|
||||
];
|
||||
|
||||
foreach ($classes as $class) {
|
||||
$stmt = $conn->prepare("INSERT INTO classes (class_name, teacher_id, schedule, capacity) VALUES (?, ?, ?, ?)");
|
||||
if (!$stmt) {
|
||||
die("Prepare failed: (" . $conn->errno . ") " . $conn->error);
|
||||
}
|
||||
$stmt->bind_param("sisi", $class['class_name'], $class['teacher_id'], $class['schedule'], $class['capacity']);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
$conn->close();
|
||||
|
||||
return redirect()->to('/parent/classes')->with('success', 'Classes added successfully.');
|
||||
}
|
||||
|
||||
public function classAttendance($class_section_id)
|
||||
{
|
||||
// Get the teacher's ID from the session
|
||||
$teacherId = session()->get('user_id');
|
||||
|
||||
// Fetch teacher data
|
||||
$teacherData = $this->teacherModel->find($teacherId);
|
||||
|
||||
// Fetch students for the class
|
||||
$studentsData = $this->studentModel->join('student_class', 'students.id = student_class.student_id')
|
||||
->where('student_class.class_section_id', $class_section_id)
|
||||
->select('students.*, student_class.class_section_id')
|
||||
->findAll();
|
||||
|
||||
// Get class name (assuming it's stored in the students data)
|
||||
$className = !empty($studentsData) && isset($studentsData[0]['class_name']) ? $studentsData[0]['class_name'] : 'Class Name Not Available';
|
||||
|
||||
// Check if the teacher data and students data are available
|
||||
if (empty($teacherData) || empty($studentsData)) {
|
||||
return redirect()->to('/teacher/classes')->with('error', 'No data found for this class.');
|
||||
}
|
||||
|
||||
// Pass data to the view
|
||||
return view('/teacher/classes', [
|
||||
'teacher_name' => $teacherData['teacher_first'] . ' ' . $teacherData['teacher_last'],
|
||||
'students' => $studentsData,
|
||||
'class_name' => $className,
|
||||
'semester' => $this->semester,
|
||||
'class_section_id' => $class_section_id, // Pass the class_id for attendance update forms
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
|
||||
class ContactController extends BaseController
|
||||
{
|
||||
public function __construct(private \CodeIgniter\HTTP\IncomingRequest $request)
|
||||
{
|
||||
helper('form'); // Load the form helper
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return view('/parent/contact');
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
helper('form'); // Ensure the form helper is loaded
|
||||
|
||||
$validation = \Config\Services::validation();
|
||||
|
||||
// Define validation rules
|
||||
$validation->setRules([
|
||||
'name' => 'required|min_length[3]',
|
||||
'email' => 'required|valid_email',
|
||||
'subject' => 'required|min_length[3]',
|
||||
'message' => 'required|min_length[10]'
|
||||
]);
|
||||
|
||||
if (!$validation->withRequest($this->request)->run()) {
|
||||
return view('/parent/contact', [
|
||||
'validation' => $validation
|
||||
]);
|
||||
}
|
||||
|
||||
// Process form data
|
||||
$name = $this->request->getPost('name');
|
||||
$email = strtolower($this->request->getPost('email'));
|
||||
$subject = $this->request->getPost('subject');
|
||||
$message = $this->request->getPost('message');
|
||||
|
||||
// Initialize the EmailController
|
||||
$emailController = new \App\Controllers\View\EmailController();
|
||||
|
||||
// Prepare the message to send
|
||||
$formattedMessage = "
|
||||
<p><strong>From:</strong> {$name} ({$email})</p>
|
||||
<p><strong>Subject:</strong> {$subject}</p>
|
||||
<p>{$message}</p>
|
||||
";
|
||||
|
||||
// Send the email using EmailController
|
||||
if ($emailController->sendEmail('support@alrahmaisgl.org', $subject, $formattedMessage)) {
|
||||
$session = session();
|
||||
$session->setFlashdata('success', 'Thank you for contacting us! We will get back to you soon.');
|
||||
} else {
|
||||
$session = session();
|
||||
$session->setFlashdata('error', 'There was an error sending your message. Please try again later.');
|
||||
}
|
||||
|
||||
return redirect()->to('/parent/contact');
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use App\Models\RoleModel;
|
||||
|
||||
class DashboardRedirectController extends Controller
|
||||
{
|
||||
public function dashboard()
|
||||
{
|
||||
$session = session();
|
||||
$roles = $session->get('roles') ?? [];
|
||||
|
||||
// Fallback: if only a single role is stored
|
||||
if (empty($roles) && $session->get('role')) {
|
||||
$roles = [$session->get('role')];
|
||||
}
|
||||
|
||||
// Use your existing method
|
||||
return $this->redirectToDashboard($roles);
|
||||
}
|
||||
|
||||
// Your existing function (unchanged)
|
||||
|
||||
private function redirectToDashboard(array $roles)
|
||||
{
|
||||
if (empty($roles)) {
|
||||
log_message('error', 'Empty roles array passed to redirectToDashboard.');
|
||||
return redirect()->to('/landing_page/guest_dashboard');
|
||||
}
|
||||
|
||||
$roleModel = new RoleModel();
|
||||
|
||||
// Resolve all candidate roles (by name or slug), ordered by priority ASC
|
||||
$rows = $roleModel->findByNamesOrSlugs($roles);
|
||||
|
||||
if (!empty($rows)) {
|
||||
$route = $rows[0]['dashboard_route'] ?? '/landing_page/guest_dashboard';
|
||||
log_message('debug', 'Redirecting user to: ' . $route);
|
||||
return redirect()->to($route);
|
||||
}
|
||||
|
||||
log_message('warning', 'No matching role found. Redirecting to guest dashboard.');
|
||||
return redirect()->to('/landing_page/guest_dashboard');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
|
||||
class DocsController extends BaseController
|
||||
{
|
||||
public function swagger()
|
||||
{
|
||||
// Present both specs in a Swagger UI dropdown
|
||||
$specs = [
|
||||
[
|
||||
'name' => 'Administrator API',
|
||||
'url' => '/docs/openapi_administrator_controller.yaml',
|
||||
],
|
||||
[
|
||||
'name' => 'Parent API',
|
||||
'url' => '/docs/openapi_parent_controller.yaml',
|
||||
],
|
||||
];
|
||||
|
||||
return view('swagger_ui', ['specs' => $specs]);
|
||||
}
|
||||
}
|
||||
@@ -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,360 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Models\EventModel;
|
||||
use App\Models\EventChargesModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\StudentModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\EnrollmentModel;
|
||||
use App\Controllers\View\InvoiceController;
|
||||
use CodeIgniter\RESTful\ResourceController;
|
||||
|
||||
class EventController extends ResourceController
|
||||
{
|
||||
protected $eventChargesModel;
|
||||
protected $studentModel;
|
||||
protected $userModel;
|
||||
protected $configModel;
|
||||
protected $eventModel;
|
||||
protected $invoiceController;
|
||||
protected $schoolYear;
|
||||
protected $semester;
|
||||
protected $categories;
|
||||
protected $enrollmentModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->eventChargesModel = new EventChargesModel(); //eventChargesModel
|
||||
$this->studentModel = new StudentModel();
|
||||
$this->configModel = new ConfigurationModel();
|
||||
$this->userModel = new UserModel(); // Add this
|
||||
$this->eventModel = new EventModel();
|
||||
$this->invoiceController = new InvoiceController();
|
||||
$this->enrollmentModel = new EnrollmentModel();
|
||||
|
||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||
$this->semester = $this->configModel->getConfig('semester');
|
||||
$this->categories = [
|
||||
'workshops',
|
||||
'orientations',
|
||||
'field trips',
|
||||
'Ramadan programs',
|
||||
];
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$eventModel = new EventModel();
|
||||
|
||||
$today = local_date(utc_now(), 'Y-m-d');
|
||||
|
||||
// Fetch all events
|
||||
$events = $eventModel
|
||||
->orderBy('created_at', 'DESC')
|
||||
->findAll();
|
||||
|
||||
// Fetch active events (not expired)
|
||||
$activeEventCount = $eventModel
|
||||
->where('expiration_date >=', $today)
|
||||
->countAllResults();
|
||||
|
||||
return view('administrator/events/event_list', [
|
||||
'events' => $events,
|
||||
'activeEventCount' => $activeEventCount
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function create()
|
||||
{
|
||||
helper(['form']);
|
||||
if (strtolower($this->request->getMethod()) === 'post') {
|
||||
$file = $this->request->getFile('flyer');
|
||||
$flyerPath = null;
|
||||
|
||||
if ($file && $file->isValid() && !$file->hasMoved()) {
|
||||
// Move to public/uploads/event_flyers
|
||||
$newName = $file->getRandomName();
|
||||
$file->move(FCPATH . 'uploads/event_flyers', $newName);
|
||||
$flyerPath = 'event_flyers/' . $newName; // store relative path
|
||||
}
|
||||
|
||||
$eventId = $this->eventModel->insert([
|
||||
'event_name' => $this->request->getPost('event_name'),
|
||||
'event_category' => $this->request->getPost('event_category'),
|
||||
'description' => $this->request->getPost('description'),
|
||||
'amount' => $this->request->getPost('amount'),
|
||||
'flyer' => $flyerPath,
|
||||
'expiration_date' => $this->request->getPost('expiration_date'),
|
||||
'semester' => $this->request->getPost('semester'),
|
||||
'school_year' => $this->request->getPost('school_year'),
|
||||
'created_by' => session()->get('user_id'),
|
||||
]);
|
||||
|
||||
if ($eventId) {
|
||||
$amount = (float) $this->request->getPost('amount');
|
||||
$semester = (string) $this->request->getPost('semester');
|
||||
$schoolYear = (string) $this->request->getPost('school_year');
|
||||
$userId = (int) (session()->get('user_id') ?? 0);
|
||||
|
||||
$enrollments = $this->enrollmentModel
|
||||
->select('enrollments.student_id, students.parent_id')
|
||||
->join('students', 'students.id = enrollments.student_id', 'left')
|
||||
->where('enrollments.school_year', $schoolYear)
|
||||
->whereIn('enrollments.enrollment_status', ['enrolled', 'payment pending'])
|
||||
->findAll();
|
||||
|
||||
$parentIds = [];
|
||||
foreach ($enrollments as $row) {
|
||||
$studentId = (int) ($row['student_id'] ?? 0);
|
||||
$parentId = (int) ($row['parent_id'] ?? 0);
|
||||
if ($studentId <= 0 || $parentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = $this->eventChargesModel
|
||||
->where('event_id', $eventId)
|
||||
->where('student_id', $studentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->first();
|
||||
|
||||
if ($exists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->eventChargesModel->insert([
|
||||
'event_id' => $eventId,
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
'participation' => 'yes',
|
||||
'charged' => $amount,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'updated_by' => $userId ?: null,
|
||||
]);
|
||||
|
||||
$parentIds[] = $parentId;
|
||||
}
|
||||
|
||||
$parentIds = array_unique($parentIds);
|
||||
foreach ($parentIds as $pid) {
|
||||
$this->invoiceController->generateInvoice((string) $pid);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->to('/administrator/events')->with('success', 'Event created successfully');
|
||||
}
|
||||
|
||||
return view('administrator/events/create_event', [
|
||||
'categories' => $this->categories,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function edit($id = null)
|
||||
{
|
||||
helper(['form']);
|
||||
$event = $this->eventModel->find($id);
|
||||
|
||||
if (!$event) {
|
||||
return redirect()->to('/administrator/events')->with('error', 'Event not found');
|
||||
}
|
||||
|
||||
if (strtolower($this->request->getMethod()) === 'post') {
|
||||
log_message('debug', 'POST detected');
|
||||
$file = $this->request->getFile('flyer');
|
||||
$flyerPath = $event['flyer']; // Default: keep old flyer
|
||||
|
||||
if ($file && $file->isValid() && !$file->hasMoved()) {
|
||||
$newName = $file->getRandomName();
|
||||
$file->move(FCPATH . 'uploads/event_flyers', $newName);
|
||||
$flyerPath = 'event_flyers/' . $newName; // store relative path
|
||||
}
|
||||
|
||||
$updated = $this->eventModel->update($id, [
|
||||
'event_name' => $this->request->getPost('event_name'),
|
||||
'event_category' => $this->request->getPost('event_category'),
|
||||
'description' => $this->request->getPost('description'),
|
||||
'amount' => $this->request->getPost('amount'),
|
||||
'flyer' => $flyerPath,
|
||||
'expiration_date' => $this->request->getPost('expiration_date'),
|
||||
'semester' => $this->request->getPost('semester'),
|
||||
'school_year' => $this->request->getPost('school_year'),
|
||||
]);
|
||||
|
||||
if ($updated) {
|
||||
return redirect()->to('/administrator/events')->with('success', 'Event updated successfully');
|
||||
} else {
|
||||
log_message('debug', 'GET detected');
|
||||
return redirect()->back()->with('error', 'Failed to update event');
|
||||
}
|
||||
}
|
||||
|
||||
return view('administrator/events/edit_event', [
|
||||
'event' => $event,
|
||||
'categories' => $this->categories,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function delete($id = null)
|
||||
{
|
||||
|
||||
$event = $this->eventModel->find($id);
|
||||
|
||||
if (!$event) {
|
||||
return redirect()->to('/administrator/events')->with('error', 'Event not found');
|
||||
}
|
||||
|
||||
// Delete related charges and collect parent IDs
|
||||
$charges = $this->eventChargesModel->where('event_id', $id)->findAll();
|
||||
$parentIds = [];
|
||||
|
||||
foreach ($charges as $charge) {
|
||||
$this->eventChargesModel->delete($charge['id']);
|
||||
$parentIds[] = $charge['parent_id'];
|
||||
}
|
||||
|
||||
// Delete event
|
||||
$this->eventModel->delete($id);
|
||||
|
||||
$parentIds = array_unique($parentIds);
|
||||
|
||||
foreach ($parentIds as $parentId) {
|
||||
$this->invoiceController->generateInvoice($parentId);
|
||||
}
|
||||
|
||||
return redirect()->to('/administrator/events')->with('success', 'Event, charges, and invoices updated.');
|
||||
}
|
||||
|
||||
|
||||
// Optionally keep your eventShow / eventUpdate for legacy administrator event charges
|
||||
public function eventShow()
|
||||
{
|
||||
|
||||
$schoolYear = $this->request->getGet('school_year') ?? $this->schoolYear;
|
||||
$semester = $this->request->getGet('semester') ?? $this->semester;
|
||||
|
||||
$parents = $this->userModel->getParents();
|
||||
$events = $this->eventModel->getActiveEvents($this->schoolYear);
|
||||
|
||||
$charges = $this->eventChargesModel
|
||||
->select('event_charges.*,
|
||||
users.firstname AS parent_firstname, users.lastname AS parent_lastname,
|
||||
students.firstname AS student_firstname, students.lastname AS student_lastname,
|
||||
events.event_name')
|
||||
->join('users', 'users.id = event_charges.parent_id', 'left')
|
||||
->join('students', 'students.id = event_charges.student_id', 'left')
|
||||
->join('events', 'events.id = event_charges.event_id', 'left')
|
||||
->where('event_charges.school_year', $schoolYear)
|
||||
->where('event_charges.semester', $semester)
|
||||
->orderBy('event_charges.created_at', 'DESC')
|
||||
->findAll();
|
||||
|
||||
return view('administrator/events/event_charges', [
|
||||
'charges' => $charges,
|
||||
'parents' => $parents,
|
||||
'events' => $events,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function eventUpdate()
|
||||
{
|
||||
$schoolYear = $this->request->getPost('school_year') ?? $this->schoolYear;
|
||||
$semester = $this->request->getPost('semester') ?? $this->semester;
|
||||
|
||||
$parentId = $this->request->getPost('parent_id');
|
||||
$eventId = $this->request->getPost('event_id');
|
||||
$participations = $this->request->getPost('participation') ?? [];
|
||||
|
||||
if (!$parentId || !$eventId || empty($participations)) {
|
||||
return redirect()->back()->with('error', 'Missing required information.');
|
||||
}
|
||||
|
||||
$userId = session()->get('user_id');
|
||||
$event = $this->eventModel->getEvent($eventId, $schoolYear);
|
||||
|
||||
foreach ($participations as $studentId => $value) {
|
||||
$existing = $this->eventChargesModel->where([
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
'event_id' => $eventId,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester
|
||||
])->first();
|
||||
|
||||
if ($value === 'no') {
|
||||
if ($existing) {
|
||||
$this->eventChargesModel->delete($existing['id']);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// value is 'yes'
|
||||
if ($existing) {
|
||||
$this->eventChargesModel->update($existing['id'], [
|
||||
'participation' => 'yes',
|
||||
'charged' => $event['amount'],
|
||||
'updated_by' => $userId
|
||||
]);
|
||||
} else {
|
||||
$this->eventChargesModel->insert([
|
||||
'parent_id' => $parentId,
|
||||
'student_id' => $studentId,
|
||||
'event_id' => $eventId,
|
||||
'participation' => 'yes',
|
||||
'charged' => $event['amount'],
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'created_by' => $userId,
|
||||
'updated_by' => $userId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->invoiceController->generateInvoice($parentId);
|
||||
|
||||
return redirect()->back()->with('success', 'Event charges updated successfully.');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public function getStudentsWithCharges()
|
||||
{
|
||||
$parentId = $this->request->getGet('parent_id');
|
||||
$semester = $this->request->getGet('semester');
|
||||
$schoolYear = $this->request->getGet('school_year');
|
||||
|
||||
// Get students for parent
|
||||
$students = $this->studentModel->where('parent_id', $parentId)->findAll();
|
||||
|
||||
// Get student_ids that already have charges
|
||||
$chargedStudentIds = $this->eventChargesModel
|
||||
->where('parent_id', $parentId)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', $schoolYear)
|
||||
->groupBy('student_id')
|
||||
->select('student_id')
|
||||
->findColumn('student_id');
|
||||
|
||||
$data = [];
|
||||
foreach ($students as $student) {
|
||||
$data[] = [
|
||||
'id' => $student['id'],
|
||||
'name' => $student['firstname'] . ' ' . $student['lastname'],
|
||||
'charged' => in_array($student['id'], $chargedStudentIds ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->response->setJSON($data);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,487 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use App\Models\InvoiceModel;
|
||||
use App\Models\PaymentModel;
|
||||
use App\Models\StudentClassModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
|
||||
class FamilyAdminController extends BaseController
|
||||
{
|
||||
public function index(): ResponseInterface
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$studentId = (int) ($this->request->getGet('student_id') ?? 0);
|
||||
$guardianId = (int) ($this->request->getGet('guardian_id') ?? 0);
|
||||
$data = ['student' => null, 'families' => [], 'guardians' => [], 'students' => []];
|
||||
|
||||
// Simple student list for select
|
||||
$data['students'] = $db->query("SELECT id, CONCAT(lastname, ', ', firstname) AS name FROM students ORDER BY lastname, firstname")->getResultArray();
|
||||
// Preload search datasets (students + guardians/parents)
|
||||
$data['searchStudents'] = $db->query("SELECT id, firstname, lastname FROM students ORDER BY lastname, firstname")->getResultArray();
|
||||
$data['searchGuardians'] = $db->query(
|
||||
"SELECT DISTINCT u.id, u.firstname, u.lastname, u.email, u.cellphone
|
||||
FROM family_guardians fg
|
||||
JOIN users u ON u.id = fg.user_id
|
||||
ORDER BY u.lastname, u.firstname"
|
||||
)->getResultArray();
|
||||
|
||||
// If a guardian is provided, resolve one of their students and redirect to use existing flow
|
||||
if (!$studentId && $guardianId) {
|
||||
$row = $db->query(
|
||||
"SELECT s.id AS student_id
|
||||
FROM family_guardians fg
|
||||
JOIN family_students fs ON fs.family_id = fg.family_id
|
||||
JOIN students s ON s.id = fs.student_id
|
||||
WHERE fg.user_id = ?
|
||||
ORDER BY s.lastname, s.firstname
|
||||
LIMIT 1",
|
||||
[$guardianId]
|
||||
)->getRowArray();
|
||||
if (!$row) {
|
||||
$row = $db->query(
|
||||
"SELECT s.id AS student_id
|
||||
FROM students s
|
||||
WHERE s.parent_id = ?
|
||||
ORDER BY s.lastname, s.firstname
|
||||
LIMIT 1",
|
||||
[$guardianId]
|
||||
)->getRowArray();
|
||||
}
|
||||
if ($row && !empty($row['student_id'])) {
|
||||
return redirect()->to(site_url('family?student_id='.(int)$row['student_id']));
|
||||
}
|
||||
}
|
||||
|
||||
if ($studentId) {
|
||||
$data['student'] = $db->query("SELECT id, firstname, lastname FROM students WHERE id = ?", [$studentId])->getRowArray();
|
||||
|
||||
// Fetch all families for this student with extended fields
|
||||
$families = $db->query(
|
||||
"SELECT f.id, f.family_code, f.household_name, f.address_line1, f.address_line2, f.city, f.state, f.postal_code, f.country,
|
||||
f.primary_phone, f.preferred_lang, f.preferred_contact_method, f.is_active,
|
||||
fs.is_primary_home
|
||||
FROM family_students fs
|
||||
JOIN families f ON f.id = fs.family_id
|
||||
WHERE fs.student_id = ?
|
||||
ORDER BY fs.is_primary_home DESC, f.household_name",
|
||||
[$studentId]
|
||||
)->getResultArray();
|
||||
|
||||
// Enrich each family with guardians, students, and financials
|
||||
$invoiceModel = new InvoiceModel();
|
||||
$paymentModel = new PaymentModel();
|
||||
$studentClassModel = new StudentClassModel();
|
||||
$configModel = new ConfigurationModel();
|
||||
$schoolYear = (string) ($configModel->getConfig('school_year') ?? '');
|
||||
|
||||
foreach ($families as &$fam) {
|
||||
$fid = (int) $fam['id'];
|
||||
|
||||
// Guardians (with contact info)
|
||||
$guardians = $db->query(
|
||||
"SELECT u.id AS user_id, u.firstname, u.lastname, u.email, u.cellphone,
|
||||
u.address_street, u.city, u.state, u.zip,
|
||||
fg.relation, fg.is_primary, fg.receive_emails, fg.receive_sms
|
||||
FROM family_guardians fg
|
||||
JOIN users u ON u.id = fg.user_id
|
||||
WHERE fg.family_id = ?
|
||||
ORDER BY fg.is_primary DESC, u.lastname, u.firstname",
|
||||
[$fid]
|
||||
)->getResultArray();
|
||||
$fam['guardians'] = $guardians;
|
||||
|
||||
// Students in this family
|
||||
$studentsRows = $db->query(
|
||||
"SELECT s.id, s.firstname, s.lastname
|
||||
FROM family_students fs
|
||||
JOIN students s ON s.id = fs.student_id
|
||||
WHERE fs.family_id = ?
|
||||
ORDER BY s.lastname, s.firstname",
|
||||
[$fid]
|
||||
)->getResultArray();
|
||||
// Enrich with grade label if available
|
||||
if (!empty($studentsRows)) {
|
||||
foreach ($studentsRows as &$sr) {
|
||||
$sid = (int) ($sr['id'] ?? 0);
|
||||
$sr['grade'] = $sid ? (string) ($studentClassModel->getClassSectionsByStudentId($sid, $schoolYear) ?? '') : '';
|
||||
}
|
||||
unset($sr);
|
||||
}
|
||||
$fam['students'] = $studentsRows;
|
||||
|
||||
// Financials: invoices and payments for all guardians (by user_id)
|
||||
$parentIds = array_map(static fn($g) => (int)($g['user_id'] ?? 0), $guardians);
|
||||
$parentIds = array_values(array_filter($parentIds));
|
||||
|
||||
$fam['invoices'] = [];
|
||||
$fam['payments'] = [];
|
||||
$fam['finance_summary'] = [
|
||||
'invoices_count' => 0,
|
||||
'total_amount' => 0.0,
|
||||
'paid_amount' => 0.0,
|
||||
'balance' => 0.0,
|
||||
];
|
||||
|
||||
if (!empty($parentIds)) {
|
||||
// Invoices
|
||||
$invRows = $db->table('invoices')
|
||||
->select('id, parent_id, invoice_number, status, total_amount, paid_amount, balance, issue_date, due_date')
|
||||
->whereIn('parent_id', $parentIds)
|
||||
->orderBy('issue_date', 'DESC')
|
||||
->get()->getResultArray();
|
||||
$fam['invoices'] = $invRows;
|
||||
// Map invoice id -> number for payments table
|
||||
$invoiceMap = [];
|
||||
foreach ($invRows as $ir) {
|
||||
$invoiceMap[(int)$ir['id']] = (string)($ir['invoice_number'] ?? '');
|
||||
}
|
||||
$fam['invoice_map'] = $invoiceMap;
|
||||
|
||||
// Aggregate summary
|
||||
foreach ($invRows as $ir) {
|
||||
$fam['finance_summary']['invoices_count']++;
|
||||
$fam['finance_summary']['total_amount'] += (float)($ir['total_amount'] ?? 0);
|
||||
$fam['finance_summary']['paid_amount'] += (float)($ir['paid_amount'] ?? 0);
|
||||
$fam['finance_summary']['balance'] += (float)($ir['balance'] ?? 0);
|
||||
}
|
||||
|
||||
// Recent payments (limit 10)
|
||||
$payRows = $db->table('payments')
|
||||
->select('id, parent_id, invoice_id, paid_amount, balance, payment_method, payment_date, status')
|
||||
->whereIn('parent_id', $parentIds)
|
||||
->orderBy('payment_date', 'DESC')
|
||||
->limit(10)
|
||||
->get()->getResultArray();
|
||||
$fam['payments'] = $payRows;
|
||||
}
|
||||
}
|
||||
unset($fam);
|
||||
|
||||
$data['families'] = $families;
|
||||
|
||||
// Back-compat: also expose guardians of first family for old UI pieces
|
||||
if (!empty($families)) {
|
||||
$familyId = (int)$families[0]['id'];
|
||||
$data['guardians'] = $db->query(
|
||||
"SELECT u.id, u.firstname, u.lastname, u.email, fg.relation, fg.is_primary, fg.receive_emails
|
||||
FROM family_guardians fg JOIN users u ON u.id = fg.user_id
|
||||
WHERE fg.family_id = ? ORDER BY fg.is_primary DESC, u.lastname, u.firstname",
|
||||
[$familyId]
|
||||
)->getResultArray();
|
||||
}
|
||||
}
|
||||
|
||||
return service('response')->setBody(view('family/index', $data));
|
||||
}
|
||||
|
||||
// GET /family/search?q=..
|
||||
public function search(): ResponseInterface
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$q = trim((string)($this->request->getGet('q') ?? ''));
|
||||
if ($q === '') {
|
||||
return $this->response->setJSON(['items' => []]);
|
||||
}
|
||||
$qs = '%' . $db->escapeLikeString($q) . '%';
|
||||
|
||||
// Students suggestions
|
||||
$students = $db->query(
|
||||
"SELECT id, firstname, lastname
|
||||
FROM students
|
||||
WHERE CONCAT_WS(' ', firstname, lastname) LIKE ?
|
||||
ORDER BY lastname, firstname
|
||||
LIMIT 8",
|
||||
[$qs]
|
||||
)->getResultArray();
|
||||
|
||||
// Parents/Guardians suggestions (users)
|
||||
$guardians = $db->query(
|
||||
"SELECT id, firstname, lastname, email, cellphone
|
||||
FROM users
|
||||
WHERE (
|
||||
CONCAT_WS(' ', firstname, lastname) LIKE ?
|
||||
OR email LIKE ?
|
||||
OR cellphone LIKE ?
|
||||
)
|
||||
ORDER BY lastname, firstname
|
||||
LIMIT 8",
|
||||
[$qs, $qs, $qs]
|
||||
)->getResultArray();
|
||||
|
||||
$items = [];
|
||||
foreach ($students as $s) {
|
||||
$items[] = [
|
||||
'type' => 'student',
|
||||
'id' => (int)$s['id'],
|
||||
'label'=> trim(($s['firstname'] ?? '').' '.($s['lastname'] ?? '')),
|
||||
'sub' => 'Student',
|
||||
];
|
||||
}
|
||||
foreach ($guardians as $g) {
|
||||
$items[] = [
|
||||
'type' => 'guardian',
|
||||
'id' => (int)$g['id'],
|
||||
'label' => trim(($g['firstname'] ?? '').' '.($g['lastname'] ?? '')),
|
||||
'sub' => trim(($g['email'] ?? '').' '.($g['cellphone'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->response->setJSON(['items' => $items]);
|
||||
}
|
||||
|
||||
// GET /family/card?student_id=.. | guardian_id=.. | family_id=..
|
||||
public function card(): ResponseInterface
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$studentId = (int) ($this->request->getGet('student_id') ?? 0);
|
||||
$guardianId = (int) ($this->request->getGet('guardian_id') ?? 0);
|
||||
$familyId = (int) ($this->request->getGet('family_id') ?? 0);
|
||||
|
||||
if (!$familyId) {
|
||||
if ($studentId) {
|
||||
$row = $db->query(
|
||||
"SELECT f.id
|
||||
FROM family_students fs
|
||||
JOIN families f ON f.id = fs.family_id
|
||||
WHERE fs.student_id = ?
|
||||
ORDER BY fs.is_primary_home DESC, f.household_name
|
||||
LIMIT 1",
|
||||
[$studentId]
|
||||
)->getRowArray();
|
||||
if (!empty($row['id'])) $familyId = (int) $row['id'];
|
||||
} elseif ($guardianId) {
|
||||
// 1) Try via guardians link
|
||||
$row = $db->query(
|
||||
"SELECT f.id
|
||||
FROM family_guardians fg
|
||||
JOIN families f ON f.id = fg.family_id
|
||||
WHERE fg.user_id = ?
|
||||
ORDER BY f.household_name
|
||||
LIMIT 1",
|
||||
[$guardianId]
|
||||
)->getRowArray();
|
||||
if (!empty($row['id'])) {
|
||||
$familyId = (int) $row['id'];
|
||||
} else {
|
||||
// 2) Fallback via students.parent_id → family_students
|
||||
$row = $db->query(
|
||||
"SELECT f.id
|
||||
FROM students s
|
||||
JOIN family_students fs ON fs.student_id = s.id
|
||||
JOIN families f ON f.id = fs.family_id
|
||||
WHERE s.parent_id = ?
|
||||
ORDER BY fs.is_primary_home DESC, f.household_name
|
||||
LIMIT 1",
|
||||
[$guardianId]
|
||||
)->getRowArray();
|
||||
if (!empty($row['id'])) $familyId = (int) $row['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$familyId) {
|
||||
return $this->response->setStatusCode(404)->setBody('<div class="p-3 text-danger">Family not found.</div>');
|
||||
}
|
||||
|
||||
$family = $db->query(
|
||||
"SELECT f.id, f.family_code, f.household_name, f.address_line1, f.address_line2, f.city, f.state, f.postal_code, f.country,
|
||||
f.primary_phone, f.preferred_lang, f.preferred_contact_method, f.is_active
|
||||
FROM families f
|
||||
WHERE f.id = ?",
|
||||
[$familyId]
|
||||
)->getRowArray();
|
||||
|
||||
if (!$family) {
|
||||
return $this->response->setStatusCode(404)->setBody('<div class="p-3 text-danger">Family not found.</div>');
|
||||
}
|
||||
|
||||
// Hydrate with guardians, students (+grades), invoices, payments
|
||||
$invoiceModel = new \App\Models\InvoiceModel();
|
||||
$paymentModel = new \App\Models\PaymentModel();
|
||||
$studentClassModel = new \App\Models\StudentClassModel();
|
||||
$configModel = new \App\Models\ConfigurationModel();
|
||||
$schoolYear = (string) ($configModel->getConfig('school_year') ?? '');
|
||||
|
||||
// Guardians
|
||||
$guardians = $db->query(
|
||||
"SELECT u.id AS user_id, u.firstname, u.lastname, u.email, u.cellphone,
|
||||
u.address_street, u.city, u.state, u.zip,
|
||||
fg.relation, fg.is_primary, fg.receive_emails, fg.receive_sms
|
||||
FROM family_guardians fg
|
||||
JOIN users u ON u.id = fg.user_id
|
||||
WHERE fg.family_id = ?
|
||||
ORDER BY fg.is_primary DESC, u.lastname, u.firstname",
|
||||
[$familyId]
|
||||
)->getResultArray();
|
||||
$family['guardians'] = $guardians;
|
||||
|
||||
// Students
|
||||
$studentsRows = $db->query(
|
||||
"SELECT s.id, s.firstname, s.lastname
|
||||
FROM family_students fs
|
||||
JOIN students s ON s.id = fs.student_id
|
||||
WHERE fs.family_id = ?
|
||||
ORDER BY s.lastname, s.firstname",
|
||||
[$familyId]
|
||||
)->getResultArray();
|
||||
if (!empty($studentsRows)) {
|
||||
foreach ($studentsRows as &$sr) {
|
||||
$sid = (int) ($sr['id'] ?? 0);
|
||||
$sr['grade'] = $sid ? (string) ($studentClassModel->getClassSectionsByStudentId($sid, $schoolYear) ?? '') : '';
|
||||
}
|
||||
unset($sr);
|
||||
}
|
||||
$family['students'] = $studentsRows;
|
||||
|
||||
// Financials
|
||||
$parentIds = array_map(static fn($g) => (int)($g['user_id'] ?? 0), $guardians);
|
||||
$parentIds = array_values(array_filter($parentIds));
|
||||
|
||||
$family['invoices'] = [];
|
||||
$family['payments'] = [];
|
||||
$family['finance_summary'] = [
|
||||
'invoices_count' => 0,
|
||||
'total_amount' => 0.0,
|
||||
'paid_amount' => 0.0,
|
||||
'balance' => 0.0,
|
||||
];
|
||||
// Emergency contacts (by guardian/parent)
|
||||
$family['emergency_contacts'] = [];
|
||||
if (!empty($parentIds)) {
|
||||
// Map guardian name by user_id
|
||||
$gmap = [];
|
||||
foreach ($guardians as $g) {
|
||||
$gid = (int)($g['user_id'] ?? 0);
|
||||
if ($gid > 0) $gmap[$gid] = trim(($g['firstname'] ?? '') . ' ' . ($g['lastname'] ?? ''));
|
||||
}
|
||||
$ecRows = $db->table('emergency_contacts')
|
||||
->select('id, parent_id, emergency_contact_name, relation, cellphone, email, school_year, semester, created_at, updated_at')
|
||||
->whereIn('parent_id', $parentIds)
|
||||
->orderBy('updated_at', 'DESC')
|
||||
->get()->getResultArray();
|
||||
if (!empty($ecRows)) {
|
||||
foreach ($ecRows as &$ec) {
|
||||
$pid = (int)($ec['parent_id'] ?? 0);
|
||||
$ec['parent_label'] = $gmap[$pid] ?? ('Parent #'.$pid);
|
||||
}
|
||||
unset($ec);
|
||||
$family['emergency_contacts'] = $ecRows;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($parentIds)) {
|
||||
// Invoices
|
||||
$invRows = $db->table('invoices')
|
||||
->select('id, parent_id, invoice_number, status, total_amount, paid_amount, balance, issue_date, due_date')
|
||||
->whereIn('parent_id', $parentIds)
|
||||
->orderBy('issue_date', 'DESC')
|
||||
->get()->getResultArray();
|
||||
$family['invoices'] = $invRows;
|
||||
$invoiceMap = [];
|
||||
foreach ($invRows as $ir) {
|
||||
$invoiceMap[(int)$ir['id']] = (string)($ir['invoice_number'] ?? '');
|
||||
}
|
||||
$family['invoice_map'] = $invoiceMap;
|
||||
|
||||
foreach ($invRows as $ir) {
|
||||
$family['finance_summary']['invoices_count']++;
|
||||
$family['finance_summary']['total_amount'] += (float)($ir['total_amount'] ?? 0);
|
||||
$family['finance_summary']['paid_amount'] += (float)($ir['paid_amount'] ?? 0);
|
||||
$family['finance_summary']['balance'] += (float)($ir['balance'] ?? 0);
|
||||
}
|
||||
|
||||
// Payments
|
||||
$payRows = $db->table('payments')
|
||||
->select('id, parent_id, invoice_id, paid_amount, balance, payment_method, payment_date, status')
|
||||
->whereIn('parent_id', $parentIds)
|
||||
->orderBy('payment_date', 'DESC')
|
||||
->limit(10)
|
||||
->get()->getResultArray();
|
||||
$family['payments'] = $payRows;
|
||||
}
|
||||
|
||||
return service('response')->setBody(view('family/card', ['f' => $family]));
|
||||
}
|
||||
|
||||
public function composeEmail()
|
||||
{
|
||||
$to = trim((string)$this->request->getGet('to'));
|
||||
$name = trim((string)$this->request->getGet('name'));
|
||||
$returnUrl = trim((string)$this->request->getGet('return_url'));
|
||||
if ($returnUrl === '') {
|
||||
$returnUrl = trim((string)$this->request->getServer('HTTP_REFERER'));
|
||||
}
|
||||
if ($returnUrl === '') {
|
||||
$returnUrl = site_url('family');
|
||||
}
|
||||
|
||||
return view('family/compose_email', [
|
||||
'to' => $to,
|
||||
'name' => $name,
|
||||
'return_url' => $returnUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sendComposeEmail()
|
||||
{
|
||||
if (!$this->request->is('post')) {
|
||||
return redirect()->to(site_url('family'));
|
||||
}
|
||||
|
||||
$to = trim((string)$this->request->getPost('to'));
|
||||
$subject = trim((string)$this->request->getPost('subject'));
|
||||
$html = (string)($this->request->getPost('html') ?? '');
|
||||
$returnUrl = trim((string)$this->request->getPost('return_url'));
|
||||
|
||||
if ($returnUrl === '' || !$this->isLocalReturnUrl($returnUrl)) {
|
||||
$returnUrl = site_url('family');
|
||||
}
|
||||
|
||||
if ($to === '' || !filter_var($to, FILTER_VALIDATE_EMAIL)) {
|
||||
return redirect()->back()->withInput()->with('error', 'Please enter a valid email address.');
|
||||
}
|
||||
if ($subject === '') {
|
||||
return redirect()->back()->withInput()->with('error', 'Subject is required.');
|
||||
}
|
||||
if (trim($html) === '') {
|
||||
return redirect()->back()->withInput()->with('error', 'Email body is required.');
|
||||
}
|
||||
|
||||
$wrapped = view('emails/custom_html', [
|
||||
'subject' => $subject,
|
||||
'body_html' => $html,
|
||||
]);
|
||||
|
||||
$mailer = new \App\Controllers\View\EmailController();
|
||||
$ok = $mailer->sendEmail($to, $subject, $wrapped, 'communication');
|
||||
|
||||
if ($ok) {
|
||||
return redirect()->to($returnUrl)->with('status', 'Email sent.');
|
||||
}
|
||||
|
||||
return redirect()->to($returnUrl)->with('error', 'Unable to send email.');
|
||||
}
|
||||
|
||||
private function isLocalReturnUrl(string $url): bool
|
||||
{
|
||||
$url = trim($url);
|
||||
if ($url === '') return false;
|
||||
|
||||
$parts = parse_url($url);
|
||||
if ($parts === false) return false;
|
||||
|
||||
if (!empty($parts['scheme']) || !empty($parts['host'])) {
|
||||
$base = parse_url(site_url('/'));
|
||||
if (!$base || empty($base['host'])) return false;
|
||||
$hostMatch = strcasecmp((string)($parts['host'] ?? ''), (string)$base['host']) === 0;
|
||||
return $hostMatch;
|
||||
}
|
||||
|
||||
// Relative URL (e.g., /family?x=1 or family?x=1)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,477 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
|
||||
// MODELS you should already have (or create tiny ones if not)
|
||||
use App\Models\FamilyModel;
|
||||
use App\Models\FamilyGuardianModel;
|
||||
use App\Models\FamilyStudentModel;
|
||||
|
||||
// Your app's models
|
||||
use App\Models\StudentModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
/**
|
||||
* FamilyController
|
||||
*
|
||||
* Handles creating/linking Family ←→ Guardians (users) ←→ Students,
|
||||
* including bootstrap from existing schema where students.parent_id is the
|
||||
* “first parent”, and a second parent may be present as a users row
|
||||
* or only as an email (stub user creation).
|
||||
*/
|
||||
class FamilyController extends BaseController
|
||||
{
|
||||
protected FamilyModel $families;
|
||||
protected FamilyGuardianModel $guardians;
|
||||
protected FamilyStudentModel $familyStudents;
|
||||
|
||||
protected StudentModel $students;
|
||||
protected UserModel $users;
|
||||
|
||||
protected BaseConnection $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->families = new FamilyModel();
|
||||
$this->guardians = new FamilyGuardianModel();
|
||||
$this->familyStudents = new FamilyStudentModel();
|
||||
|
||||
$this->students = new StudentModel();
|
||||
$this->users = new UserModel();
|
||||
|
||||
$this->db = \Config\Database::connect();
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* SECTION A — APIs your UI uses
|
||||
* =======================================================*/
|
||||
|
||||
// GET /api/students/{id}/families
|
||||
public function familiesByStudent(int $studentId)
|
||||
{
|
||||
$rows = $this->db->query(
|
||||
"SELECT f.*, fs.is_primary_home
|
||||
FROM family_students fs
|
||||
JOIN families f ON f.id = fs.family_id
|
||||
WHERE fs.student_id = ? AND f.is_active = 1
|
||||
ORDER BY fs.is_primary_home DESC, f.household_name",
|
||||
[$studentId]
|
||||
)->getResultArray();
|
||||
|
||||
return $this->response->setJSON(['data' => $rows]);
|
||||
}
|
||||
|
||||
// GET /api/families/{id}/guardians
|
||||
public function guardiansByFamily(int $familyId)
|
||||
{
|
||||
$rows = $this->db->query(
|
||||
"SELECT u.id AS user_id, u.firstname, u.lastname, u.email,
|
||||
fg.relation, fg.is_primary, fg.receive_emails, fg.receive_sms
|
||||
FROM family_guardians fg
|
||||
JOIN users u ON u.id = fg.user_id
|
||||
WHERE fg.family_id = ?
|
||||
ORDER BY fg.is_primary DESC, u.lastname, u.firstname",
|
||||
[$familyId]
|
||||
)->getResultArray();
|
||||
|
||||
return $this->response->setJSON(['data' => $rows]);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* SECTION B — Bootstrap & Linking helpers
|
||||
* =======================================================*/
|
||||
|
||||
// POST /families/bootstrap
|
||||
// Creates/ensures families for every student with parent_id,
|
||||
// links the student to that family, and (optionally) links a second parent.
|
||||
public function bootstrap()
|
||||
{
|
||||
// Optionally restrict this to admins
|
||||
// if (! $this->userCan('families.bootstrap')) return $this->deny();
|
||||
|
||||
// Guard: ensure required tables exist
|
||||
foreach (['families','family_students','family_guardians'] as $t) {
|
||||
if (! $this->db->tableExists($t)) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => "Missing required table '{$t}'. Run migrations: php spark migrate",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
// 1) Get all distinct primary parents from students.parent_id
|
||||
$primaryParents = $this->db->query(
|
||||
"SELECT DISTINCT parent_id FROM students WHERE parent_id IS NOT NULL"
|
||||
)->getResultArray();
|
||||
|
||||
$created = 0;
|
||||
$linkedStudents = 0;
|
||||
$linkedGuardians = 0;
|
||||
|
||||
foreach ($primaryParents as $row) {
|
||||
$primaryUserId = (int) $row['parent_id'];
|
||||
if ($primaryUserId <= 0) continue;
|
||||
|
||||
$familyId = $this->ensureFamilyForPrimaryParent($primaryUserId, $created);
|
||||
|
||||
// Link all students of this primary parent
|
||||
$students = $this->db->query(
|
||||
"SELECT id FROM students WHERE parent_id = ?",
|
||||
[$primaryUserId]
|
||||
)->getResultArray();
|
||||
|
||||
foreach ($students as $s) {
|
||||
$linkedStudents += $this->attachStudentToFamily((int)$s['id'], $familyId);
|
||||
}
|
||||
|
||||
// Ensure primary parent is guardian on that family
|
||||
$linkedGuardians += $this->attachGuardianUser($familyId, $primaryUserId, 'primary', true, 1, 0);
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
$payload = [
|
||||
'status' => $this->db->transStatus() ? 'ok' : 'error',
|
||||
'families_created' => $created,
|
||||
'students_linked' => $linkedStudents,
|
||||
'guardians_linked' => $linkedGuardians,
|
||||
];
|
||||
|
||||
// If accessed via GET (browser), redirect back with flash
|
||||
if (strtolower($this->request->getMethod()) === 'get') {
|
||||
$msg = json_encode($payload);
|
||||
return redirect()->to('/family')->with('message', "Bootstrap result: {$msg}");
|
||||
}
|
||||
|
||||
return $this->response->setJSON($payload);
|
||||
}
|
||||
|
||||
// POST /families/attach-second-by-user
|
||||
// body: student_id, user_id, relation='secondary'
|
||||
public function attachSecondByUser()
|
||||
{
|
||||
$studentId = (int) $this->request->getPost('student_id');
|
||||
$userId = (int) $this->request->getPost('user_id');
|
||||
$relation = (string) ($this->request->getPost('relation') ?? 'secondary');
|
||||
|
||||
if (!$studentId || !$userId) return $this->bad('student_id and user_id required');
|
||||
|
||||
$familyId = $this->familyIdFromStudentPrimary($studentId);
|
||||
if (!$familyId) return $this->bad('No primary family found for this student');
|
||||
|
||||
$rows = $this->attachGuardianUser($familyId, $userId, $relation, false, 1, 0);
|
||||
|
||||
return $this->response->setJSON(['status' => 'ok', 'attached' => $rows, 'family_id' => $familyId]);
|
||||
}
|
||||
|
||||
// POST /families/attach-second-by-email
|
||||
// body: student_id, email, firstname, lastname, relation='secondary'
|
||||
// Creates a stub user if email not in users, then links.
|
||||
public function attachSecondByEmail()
|
||||
{
|
||||
$studentId = (int) $this->request->getPost('student_id');
|
||||
$email = trim((string)$this->request->getPost('email'));
|
||||
$first = trim((string)$this->request->getPost('firstname'));
|
||||
$last = trim((string)$this->request->getPost('lastname'));
|
||||
$relation = (string) ($this->request->getPost('relation') ?? 'secondary');
|
||||
|
||||
if (!$studentId || !$email) return $this->bad('student_id and email required');
|
||||
|
||||
$familyId = $this->familyIdFromStudentPrimary($studentId);
|
||||
if (!$familyId) return $this->bad('No primary family found for this student');
|
||||
|
||||
$user = $this->users->where('email', $email)->first();
|
||||
if (!$user) {
|
||||
// Create a minimal/stub user; adjust fields to your schema
|
||||
$this->users->insert([
|
||||
'firstname' => $first ?: '',
|
||||
'lastname' => $last ?: '',
|
||||
'email' => $email,
|
||||
'status' => 'Active',
|
||||
'user_type' => 'secondary', // align with secondary guardian role
|
||||
]);
|
||||
$userId = (int) $this->users->getInsertID();
|
||||
} else {
|
||||
$userId = (int) $user['id'];
|
||||
}
|
||||
|
||||
$rows = $this->attachGuardianUser($familyId, $userId, $relation, false, 1, 0);
|
||||
|
||||
return $this->response->setJSON(['status' => 'ok', 'attached' => $rows, 'family_id' => $familyId, 'user_id' => $userId]);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* SECTION C — Maintenance actions
|
||||
* =======================================================*/
|
||||
|
||||
// POST /families/set-primary-home
|
||||
// body: family_id, student_id, is_primary_home (0/1)
|
||||
public function setPrimaryHome()
|
||||
{
|
||||
$familyId = (int) $this->request->getPost('family_id');
|
||||
$studentId = (int) $this->request->getPost('student_id');
|
||||
$flag = (int) $this->request->getPost('is_primary_home');
|
||||
|
||||
if (!$familyId || !$studentId) return $this->bad('family_id and student_id required');
|
||||
|
||||
$this->familyStudents
|
||||
->where(['family_id' => $familyId, 'student_id' => $studentId])
|
||||
->set(['is_primary_home' => $flag ? 1 : 0])
|
||||
->update();
|
||||
|
||||
return $this->response->setJSON(['status' => 'ok']);
|
||||
}
|
||||
|
||||
// POST /families/set-guardian-flags
|
||||
// body: family_id, user_id, receive_emails(0/1), is_primary(0/1), receive_sms(0/1)
|
||||
public function setGuardianFlags()
|
||||
{
|
||||
$familyId = (int) $this->request->getPost('family_id');
|
||||
$userId = (int) $this->request->getPost('user_id');
|
||||
|
||||
if (!$familyId || !$userId) return $this->bad('family_id and user_id required');
|
||||
|
||||
$data = [];
|
||||
foreach (['receive_emails', 'is_primary', 'receive_sms', 'relation'] as $k) {
|
||||
if (null !== $this->request->getPost($k)) {
|
||||
$val = $this->request->getPost($k);
|
||||
$data[$k] = in_array($k, ['receive_emails', 'is_primary', 'receive_sms'])
|
||||
? (int)$val
|
||||
: (string)$val;
|
||||
}
|
||||
}
|
||||
if (!$data) return $this->bad('No flags provided');
|
||||
|
||||
$this->guardians->where(['family_id' => $familyId, 'user_id' => $userId])->set($data)->update();
|
||||
|
||||
return $this->response->setJSON(['status' => 'ok']);
|
||||
}
|
||||
|
||||
// POST /families/unlink-guardian
|
||||
// body: family_id, user_id
|
||||
public function unlinkGuardian()
|
||||
{
|
||||
$familyId = (int) $this->request->getPost('family_id');
|
||||
$userId = (int) $this->request->getPost('user_id');
|
||||
if (!$familyId || !$userId) return $this->bad('family_id and user_id required');
|
||||
|
||||
$this->guardians->where(['family_id' => $familyId, 'user_id' => $userId])->delete();
|
||||
return $this->response->setJSON(['status' => 'ok']);
|
||||
}
|
||||
|
||||
// POST /families/unlink-student
|
||||
// body: family_id, student_id
|
||||
public function unlinkStudent()
|
||||
{
|
||||
$familyId = (int) $this->request->getPost('family_id');
|
||||
$studentId = (int) $this->request->getPost('student_id');
|
||||
if (!$familyId || !$studentId) return $this->bad('family_id and student_id required');
|
||||
|
||||
$this->familyStudents->where(['family_id' => $familyId, 'student_id' => $studentId])->delete();
|
||||
return $this->response->setJSON(['status' => 'ok']);
|
||||
}
|
||||
|
||||
/* =========================================================
|
||||
* SECTION D — Private helpers
|
||||
* =======================================================*/
|
||||
|
||||
// Ensure there is exactly one family per primary parent (users.id)
|
||||
// Returns $familyId and increments $created by reference if newly created.
|
||||
protected function ensureFamilyForPrimaryParent(int $primaryUserId, int &$createdCounter): int
|
||||
{
|
||||
$code = 'FAM-' . $primaryUserId;
|
||||
$row = $this->families->where('family_code', $code)->first();
|
||||
if ($row) return (int)$row['id'];
|
||||
|
||||
$this->families->insert([
|
||||
'family_code' => $code,
|
||||
'household_name' => 'Family of User ' . $primaryUserId,
|
||||
'is_active' => 1,
|
||||
]);
|
||||
$createdCounter++;
|
||||
return (int)$this->families->getInsertID();
|
||||
}
|
||||
|
||||
// Attach a student to a family (idempotent; returns rows affected >0 if inserted)
|
||||
protected function attachStudentToFamily(int $studentId, int $familyId): int
|
||||
{
|
||||
// INSERT IGNORE pattern
|
||||
$sql = "INSERT IGNORE INTO family_students (family_id, student_id, is_primary_home)
|
||||
VALUES (?, ?, 1)";
|
||||
$this->db->query($sql, [$familyId, $studentId]);
|
||||
return $this->db->affectedRows();
|
||||
}
|
||||
|
||||
// Attach a guardian user to family (idempotent)
|
||||
protected function attachGuardianUser(
|
||||
int $familyId,
|
||||
int $userId,
|
||||
string $relation = 'primary',
|
||||
bool $isPrimary = false,
|
||||
int $receiveEmails = 1,
|
||||
int $receiveSms = 0
|
||||
): int {
|
||||
$sql = "INSERT IGNORE INTO family_guardians (family_id, user_id, relation, is_primary, receive_emails, receive_sms)
|
||||
VALUES (?, ?, ?, ?, ?, ?)";
|
||||
$this->db->query($sql, [$familyId, $userId, $relation, $isPrimary ? 1 : 0, $receiveEmails, $receiveSms]);
|
||||
return $this->db->affectedRows();
|
||||
}
|
||||
|
||||
// Find the “primary” family for a student = family created from parent_id
|
||||
protected function familyIdFromStudentPrimary(int $studentId): ?int
|
||||
{
|
||||
// 1) Get primary parent id for the student
|
||||
$st = $this->students->select('parent_id')->find($studentId);
|
||||
if (!$st || empty($st['parent_id'])) return null;
|
||||
|
||||
// 2) The canonical family uses code FAM-{parent_id}
|
||||
$code = 'FAM-' . (int)$st['parent_id'];
|
||||
$row = $this->families->select('id')->where('family_code', $code)->first();
|
||||
if ($row) return (int)$row['id'];
|
||||
|
||||
// If not found, fall back to any family linked already
|
||||
$row = $this->db->query(
|
||||
"SELECT family_id FROM family_students WHERE student_id = ? ORDER BY is_primary_home DESC LIMIT 1",
|
||||
[$studentId]
|
||||
)->getRowArray();
|
||||
|
||||
return $row ? (int)$row['family_id'] : null;
|
||||
}
|
||||
|
||||
// Simple 400
|
||||
protected function bad(string $msg)
|
||||
{
|
||||
return $this->response->setStatusCode(400)->setJSON(['status' => 'error', 'message' => $msg]);
|
||||
}
|
||||
|
||||
// Example permission check hook (plug your own logic)
|
||||
protected function userCan(string $perm): bool
|
||||
{
|
||||
$perms = (array) (session('permissions') ?? []);
|
||||
return in_array($perm, $perms, true);
|
||||
}
|
||||
|
||||
protected function deny()
|
||||
{
|
||||
return redirect()->to('/access_denied')->setStatusCode(403);
|
||||
}
|
||||
|
||||
// Legacy import helper: map secondary parents from legacy 'parents' table
|
||||
public function importSecondParentsFromLegacy()
|
||||
{
|
||||
// Protect this route (e.g., admin only) via filter in routes
|
||||
$L = [
|
||||
'table' => 'parents',
|
||||
'firstparent_id' => 'firstparent_id', // users.id of primary (first) parent
|
||||
'second_user_id' => 'secondparent_id', // users.id of second parent (optional)
|
||||
'second_email' => 'secondparent_email',
|
||||
'second_firstname' => 'secondparent_firstname',
|
||||
'second_lastname' => 'secondparent_lastname',
|
||||
];
|
||||
|
||||
if (!$this->db->tableExists($L['table'])) {
|
||||
return $this->response->setJSON(['status' => 'error', 'message' => "Legacy table '{$L['table']}' not found"]);
|
||||
}
|
||||
foreach (['families','family_students','family_guardians'] as $t) {
|
||||
if (! $this->db->tableExists($t)) {
|
||||
return $this->response->setStatusCode(500)->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => "Missing required table '{$t}'. Run migrations: php spark migrate",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$rows = $this->db->table($L['table'])
|
||||
->select(
|
||||
"{$L['firstparent_id']} AS first_id, " .
|
||||
"{$L['second_user_id']} AS second_id, " .
|
||||
"{$L['second_email']} AS email, " .
|
||||
"{$L['second_firstname']} AS firstname, " .
|
||||
"{$L['second_lastname']} AS lastname",
|
||||
false
|
||||
)
|
||||
->groupStart()
|
||||
->where("{$L['second_user_id']} !=", 0)
|
||||
->orWhere("{$L['second_email']} !=", '')
|
||||
->groupEnd()
|
||||
->get()->getResultArray();
|
||||
|
||||
$createdUsers = 0;
|
||||
$linked = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($rows as $r) {
|
||||
$firstId = (int)($r['first_id'] ?? 0);
|
||||
$secondId = (int)($r['second_id'] ?? 0);
|
||||
$email = trim((string)($r['email'] ?? ''));
|
||||
|
||||
if (!$firstId || (!$secondId && $email === '')) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure/find family by primary parent
|
||||
$code = 'FAM-' . $firstId;
|
||||
$fam = $this->families->select('id')->where('family_code', $code)->first();
|
||||
if ($fam) {
|
||||
$familyId = (int)$fam['id'];
|
||||
} else {
|
||||
$tmp = 0; // counter sink
|
||||
$familyId = $this->ensureFamilyForPrimaryParent($firstId, $tmp);
|
||||
}
|
||||
|
||||
// Resolve/create user
|
||||
$userId = 0;
|
||||
if ($secondId > 0) {
|
||||
$userId = $secondId;
|
||||
} else {
|
||||
$user = $this->users->where('email', $email)->first();
|
||||
if (!$user) {
|
||||
$this->users->insert([
|
||||
'firstname' => $r['firstname'] ?? '',
|
||||
'lastname' => $r['lastname'] ?? '',
|
||||
'email' => $email,
|
||||
'status' => 'Active',
|
||||
'user_type' => 'secondary',
|
||||
]);
|
||||
$userId = (int)$this->users->getInsertID();
|
||||
$createdUsers++;
|
||||
} else {
|
||||
$userId = (int)$user['id'];
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure all students for this primary parent are linked to this family
|
||||
$stuRows = $this->db->query("SELECT id FROM students WHERE parent_id = ?", [$firstId])->getResultArray();
|
||||
foreach ($stuRows as $sr) {
|
||||
$sid = (int)($sr['id'] ?? 0);
|
||||
if ($sid > 0) {
|
||||
$this->attachStudentToFamily($sid, $familyId);
|
||||
}
|
||||
}
|
||||
|
||||
// Link as guardian (idempotent)
|
||||
$linked += $this->attachGuardianUser($familyId, $userId, 'guardian', false, 1, 0);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'status' => 'ok',
|
||||
'created_users' => $createdUsers,
|
||||
'guardians_linked' => $linked,
|
||||
'skipped' => $skipped,
|
||||
'total_source' => count($rows),
|
||||
];
|
||||
|
||||
if (strtolower($this->request->getMethod()) === 'get') {
|
||||
$msg = json_encode($payload);
|
||||
return redirect()->to('/family')->with('message', "Import legacy result: {$msg}");
|
||||
}
|
||||
|
||||
return $this->response->setJSON($payload);
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
<?php
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\PaymentModel;
|
||||
use App\Models\InvoiceModel;
|
||||
use App\Models\ClassSectionModel;
|
||||
|
||||
class FeeCalculationService
|
||||
{
|
||||
public function calculateRefund(array $students, int $parentId): float
|
||||
{
|
||||
$configModel = new ConfigurationModel();
|
||||
$paymentModel = new PaymentModel();
|
||||
$invoiceModel = new InvoiceModel();
|
||||
$classSectionModel = new ClassSectionModel();
|
||||
|
||||
$schoolYear = $configModel->getConfig('school_year');
|
||||
$refundDeadline = date('Y-m-d', strtotime($configModel->getConfig('refund_deadline')));
|
||||
$weekOfStudy = (float) ($configModel->getConfig('weeks_study') ?? 8);
|
||||
$schoolEndDate = date('Y-m-d', strtotime($configModel->getConfig('last_school_day')));
|
||||
$totalPaid = $paymentModel->getTotalPaidByParentId($parentId, $schoolYear);
|
||||
|
||||
if ($totalPaid <= 0) {
|
||||
log_message('info', "No payments made. Refund = 0.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Classify and enrich student data
|
||||
$registeredStudents = [];
|
||||
$withdrawnStudents = [];
|
||||
|
||||
foreach ($students as &$student) {
|
||||
$gradeName = $classSectionModel->getClassSectionNameBySectionId($student['class_section_id']);
|
||||
$student['grade'] = strtoupper(trim($gradeName));
|
||||
|
||||
if (in_array($student['enrollment_status'], ['withdrawn', 'refund pending', 'withdraw under review'])) {
|
||||
$withdrawnStudents[] = $student;
|
||||
} elseif (
|
||||
in_array($student['enrollment_status'], ['enrolled', 'payment pending']) &&
|
||||
$student['admission_status'] === 'accepted'
|
||||
) {
|
||||
$registeredStudents[] = $student;
|
||||
}
|
||||
}
|
||||
unset($student);
|
||||
|
||||
if (empty($withdrawnStudents)) {
|
||||
log_message('info', "No withdrawn students found. Refund = 0.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Combine all students for proper fee tiering
|
||||
$allStudents = array_merge($registeredStudents, $withdrawnStudents);
|
||||
|
||||
// Sort all students by grade for correct tiering
|
||||
usort($allStudents, function ($a, $b) {
|
||||
return $this->compareGrades($a['grade'], $b['grade']);
|
||||
});
|
||||
|
||||
// Retrieve fee configs
|
||||
$firstStudentFee = (float) ($configModel->getConfig('first_student_fee') ?? 350);
|
||||
$secondStudentFee = (float) ($configModel->getConfig('second_student_fee') ?? 200);
|
||||
$youthFee = (float) ($configModel->getConfig('youth_fee') ?? 180);
|
||||
|
||||
// Assign tuition_fee to all students (before filtering refunds)
|
||||
$regularCount = 0;
|
||||
foreach ($allStudents as &$student) {
|
||||
$gradeLevel = $this->getGradeLevel($student['grade']);
|
||||
|
||||
if ($gradeLevel > 9) {
|
||||
$studentFee = $youthFee;
|
||||
} else {
|
||||
$studentFee = ($regularCount === 0) ? $firstStudentFee : $secondStudentFee;
|
||||
$regularCount++;
|
||||
}
|
||||
}
|
||||
unset($student);
|
||||
|
||||
// Calculate refund for withdrawn students
|
||||
$refundAmount = 0;
|
||||
|
||||
foreach ($withdrawnStudents as $student) {
|
||||
if (empty($student['withdrawal_date'])) {
|
||||
log_message('warning', "Missing withdraw date for student ID: {$student['student_id']}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$withdrawDate = date('Y-m-d', strtotime($student['withdrawal_date']));
|
||||
if (strtotime($withdrawDate) > strtotime($refundDeadline)) {
|
||||
log_message('info', "Withdraw date {$withdrawDate} is after refund deadline {$refundDeadline}. No refund for this student.");
|
||||
continue;
|
||||
}
|
||||
|
||||
$withdrawDateObj = new \DateTime($withdrawDate);
|
||||
$schoolEndDateObj = new \DateTime($schoolEndDate);
|
||||
$daysRemaining = $withdrawDateObj->diff($schoolEndDateObj)->days;
|
||||
$weeksRemaining = min($weekOfStudy, max(0, ceil($daysRemaining / 7)));
|
||||
|
||||
//$studentFee = $student['tuition_fee'];
|
||||
$proportionalRefund = ($studentFee / $weekOfStudy) * $weeksRemaining;
|
||||
$refundAmount += $proportionalRefund;
|
||||
|
||||
log_message('info', "Student ID {$student['student_id']} refund portion: {$proportionalRefund} of {$studentFee} for {$weeksRemaining} weeks.");
|
||||
}
|
||||
|
||||
if ($refundAmount > $totalPaid) {
|
||||
log_message('info', "Refund capped at total paid amount: {$totalPaid}");
|
||||
return $totalPaid;
|
||||
}
|
||||
|
||||
log_message('info', "Final calculated refund: {$refundAmount}");
|
||||
return $refundAmount;
|
||||
}
|
||||
|
||||
|
||||
private function compareGrades($gradeA, $gradeB)
|
||||
{
|
||||
$valA = $this->getGradeLevel($gradeA);
|
||||
$valB = $this->getGradeLevel($gradeB);
|
||||
|
||||
if ($valA !== $valB) return $valA <=> $valB;
|
||||
|
||||
// Same level, compare suffix
|
||||
preg_match('/\d+([A-Z]*)$/i', strtoupper($gradeA), $suffixA);
|
||||
preg_match('/\d+([A-Z]*)$/i', strtoupper($gradeB), $suffixB);
|
||||
|
||||
return strcmp($suffixA[1] ?? '', $suffixB[1] ?? '');
|
||||
}
|
||||
|
||||
private function getGradeLevel($grade)
|
||||
{
|
||||
if (strtoupper($grade) === 'K') return 0;
|
||||
if (strtoupper($grade) === 'Y') return 99;
|
||||
|
||||
if (preg_match('/^(\d+)([A-Z]*)$/i', $grade, $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
return 999; // fallback for unknown/malformed grades
|
||||
}
|
||||
|
||||
|
||||
private function calculateTotalTuitionFee(array $students): float
|
||||
{
|
||||
$configModel = new ConfigurationModel();
|
||||
$classSectionModel = new \App\Models\ClassSectionModel();
|
||||
|
||||
$firstStudentFee = (float) ($configModel->getConfig('first_student_fee') ?? 350);
|
||||
$secondStudentFee = (float) ($configModel->getConfig('second_student_fee') ?? 200);
|
||||
$youthFee = (float) ($configModel->getConfig('youth_fee') ?? 180);
|
||||
|
||||
// ✅ Pre-fetch and assign grade/class section names before sorting
|
||||
foreach ($students as &$student) {
|
||||
$gradeName = $classSectionModel->getClassSectionNameBySectionId($student['class_section_id']);
|
||||
$student['grade'] = strtoupper(trim($gradeName));
|
||||
}
|
||||
unset($student); // break reference
|
||||
|
||||
// ✅ Sort students by grade
|
||||
usort($students, function ($a, $b) {
|
||||
return $this->compareGrades($a['grade'], $b['grade']);
|
||||
});
|
||||
|
||||
$regularCount = 0;
|
||||
$totalFee = 0;
|
||||
|
||||
// ✅ Calculate fee
|
||||
foreach ($students as $student) {
|
||||
$gradeLevel = $this->getGradeLevel($student['grade']);
|
||||
|
||||
if ($gradeLevel > 9) {
|
||||
$totalFee += $youthFee;
|
||||
} else {
|
||||
$totalFee += ($regularCount === 0) ? $firstStudentFee : $secondStudentFee;
|
||||
$regularCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return $totalFee;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class FrontendController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('/index');
|
||||
}
|
||||
|
||||
public function facility()
|
||||
{
|
||||
log_message('debug', 'FrontendController::facility called');
|
||||
helper('url'); // Ensure URL helper is loaded
|
||||
return view('/facility');
|
||||
}
|
||||
|
||||
public function team()
|
||||
{
|
||||
log_message('debug', 'FrontendController::team called');
|
||||
return view('/team');
|
||||
}
|
||||
|
||||
public function callToAction()
|
||||
{
|
||||
log_message('debug', 'FrontendController::callToAction called');
|
||||
return view('/call_to_action');
|
||||
}
|
||||
|
||||
public function testimonial()
|
||||
{
|
||||
log_message('debug', 'FrontendController::testimonial called');
|
||||
return view('/testimonial');
|
||||
}
|
||||
|
||||
public function notFound()
|
||||
{
|
||||
log_message('debug', 'FrontendController::notFound called');
|
||||
return view('/notFound');
|
||||
}
|
||||
|
||||
public function fetchUser()
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Include the shared database connection file
|
||||
$file = __DIR__ . '/../db_connection.php';
|
||||
|
||||
if (file_exists($file)) {
|
||||
require_once $file;
|
||||
} else {
|
||||
die("Error: Could not find the required file '$file'.");
|
||||
}
|
||||
|
||||
// Assuming the logged-in user's information is available in the session
|
||||
session_start();
|
||||
if (!isset($_SESSION['user_id'])) {
|
||||
echo json_encode(['error' => 'User not logged in']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$userId = $_SESSION['user_id'];
|
||||
|
||||
// Fetch user data from the database
|
||||
$sql = "SELECT firstname, lastname FROM users WHERE id = ?";
|
||||
$stmt = $conn->prepare($sql);
|
||||
$stmt->bind_param("i", $userId);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
$user = $result->fetch_assoc();
|
||||
|
||||
if ($user) {
|
||||
echo json_encode($user);
|
||||
} else {
|
||||
echo json_encode(['error' => 'User not found']);
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class HealthController extends Controller
|
||||
{
|
||||
private function checkPath(string $label, string $path): array
|
||||
{
|
||||
return [
|
||||
'label' => $label,
|
||||
'path' => $path,
|
||||
'exists' => is_dir($path),
|
||||
'writable' => is_writable($path),
|
||||
];
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$uploadsBase = rtrim(WRITEPATH, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'uploads';
|
||||
$paths = [
|
||||
'uploads' => $uploadsBase,
|
||||
'uploads/reimbursements' => $uploadsBase . DIRECTORY_SEPARATOR . 'reimbursements',
|
||||
'uploads/receipts' => $uploadsBase . DIRECTORY_SEPARATOR . 'receipts',
|
||||
'uploads/checks' => $uploadsBase . DIRECTORY_SEPARATOR . 'checks',
|
||||
'uploads/early_dismissal_signatures' => $uploadsBase . DIRECTORY_SEPARATOR . 'early_dismissal_signatures',
|
||||
];
|
||||
|
||||
$pathsStatus = [];
|
||||
foreach ($paths as $label => $p) {
|
||||
$pathsStatus[] = $this->checkPath($label, $p);
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$dbChecks = [
|
||||
'user_preferences_exists' => $db->tableExists('user_preferences'),
|
||||
'settings_exists' => $db->tableExists('settings'),
|
||||
'migrations_exists' => $db->tableExists('migrations'),
|
||||
];
|
||||
|
||||
$dbChecks['user_preferences_has_timezone'] = $dbChecks['user_preferences_exists']
|
||||
? $db->fieldExists('timezone', 'user_preferences')
|
||||
: false;
|
||||
|
||||
$dbChecks['settings_has_timezone'] = $dbChecks['settings_exists']
|
||||
? $db->fieldExists('timezone', 'settings')
|
||||
: false;
|
||||
|
||||
$okPaths = array_reduce($pathsStatus, function ($carry, $row) {
|
||||
return $carry && $row['exists'] && $row['writable'];
|
||||
}, true);
|
||||
|
||||
$okDb = true;
|
||||
if ($dbChecks['user_preferences_exists'] && !$dbChecks['user_preferences_has_timezone']) {
|
||||
$okDb = false;
|
||||
}
|
||||
if ($dbChecks['settings_exists'] && !$dbChecks['settings_has_timezone']) {
|
||||
$okDb = false;
|
||||
}
|
||||
|
||||
$ok = $okPaths && $okDb;
|
||||
|
||||
$payload = [
|
||||
'ok' => $ok,
|
||||
'paths' => $pathsStatus,
|
||||
'database' => $dbChecks,
|
||||
'write_path' => WRITEPATH,
|
||||
'timestamp' => date('c'),
|
||||
];
|
||||
|
||||
$status = $ok ? 200 : 503;
|
||||
return $this->response->setStatusCode($status)->setJSON($payload);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use App\Models\UserModel;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
class InfoIconController extends Controller
|
||||
{
|
||||
public function profileIcon()
|
||||
{
|
||||
$session = session();
|
||||
$userId = $session->get('user_id');
|
||||
|
||||
if (!$userId) {
|
||||
return view('errors/html/error_401', ['message' => 'User not logged in']);
|
||||
}
|
||||
|
||||
$userModel = new UserModel();
|
||||
$user = $userModel->getUserInfoById($userId);
|
||||
|
||||
if ($user) {
|
||||
$data = [
|
||||
'userName' => $user['firstname'] . ' ' . $user['lastname'],
|
||||
'userInitials' => strtoupper($user['firstname'][0] . $user['lastname'][0])
|
||||
];
|
||||
} else {
|
||||
$data = [
|
||||
'userName' => 'User not found',
|
||||
'userInitials' => '??'
|
||||
];
|
||||
}
|
||||
|
||||
return view('partials/header', $data);
|
||||
// return view('dashboard', ['content' => view('partials/user_icon', $data)]);
|
||||
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,148 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\IpAttemptModel;
|
||||
|
||||
class IpBanController extends BaseController
|
||||
{
|
||||
protected $ipAttemptModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->ipAttemptModel = new IpAttemptModel();
|
||||
helper(['url', 'form']);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$now = utc_now();
|
||||
$status = strtolower(trim((string)($this->request->getGet('status') ?? 'all')));
|
||||
|
||||
$builder = $this->ipAttemptModel->orderBy('updated_at', 'DESC');
|
||||
if ($status === 'active') {
|
||||
$builder = $builder->where('blocked_until >', $now);
|
||||
}
|
||||
|
||||
$banned = $builder->findAll();
|
||||
|
||||
return view('administrator/ip_bans', [
|
||||
'banned' => $banned,
|
||||
'status' => $status,
|
||||
'now' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
public function unban()
|
||||
{
|
||||
$isAjax = $this->request->isAJAX();
|
||||
$json = function(array $p, int $code=200){
|
||||
$p['csrfTokenName'] = csrf_token();
|
||||
$p['csrfHash'] = csrf_hash();
|
||||
$p[csrf_token()] = csrf_hash();
|
||||
return $this->response->setStatusCode($code)->setJSON($p);
|
||||
};
|
||||
|
||||
$id = (int) $this->request->getPost('id');
|
||||
$ip = trim((string) $this->request->getPost('ip'));
|
||||
$all = (int) $this->request->getPost('all') === 1;
|
||||
|
||||
try {
|
||||
if ($all) {
|
||||
// Clear all active bans
|
||||
$now = utc_now();
|
||||
$builder = $this->ipAttemptModel->builder();
|
||||
$builder->where('blocked_until >', $now)
|
||||
->set(['blocked_until' => null, 'attempts' => 0])
|
||||
->update();
|
||||
|
||||
$cnt = $this->ipAttemptModel->db->affectedRows();
|
||||
$msg = $cnt . ' IP(s) unbanned.';
|
||||
return $isAjax ? $json(['ok'=>true,'message'=>$msg,'count'=>$cnt])
|
||||
: redirect()->to(site_url('administrator/ip_bans'))->with('success', $msg);
|
||||
}
|
||||
|
||||
$row = null;
|
||||
if ($id > 0) {
|
||||
$row = $this->ipAttemptModel->find($id);
|
||||
} elseif ($ip !== '') {
|
||||
$row = $this->ipAttemptModel->where('ip_address', $ip)->first();
|
||||
}
|
||||
|
||||
if (!$row) {
|
||||
$msg = 'IP record not found.';
|
||||
return $isAjax ? $json(['ok'=>false,'message'=>$msg],404)
|
||||
: redirect()->back()->with('error', $msg);
|
||||
}
|
||||
|
||||
$ok = $this->ipAttemptModel->update($row['id'], [
|
||||
'blocked_until' => null,
|
||||
'attempts' => 0,
|
||||
]);
|
||||
|
||||
if (!$ok) {
|
||||
throw new \RuntimeException('Failed to update record.');
|
||||
}
|
||||
|
||||
$msg = 'IP ' . $row['ip_address'] . ' unbanned.';
|
||||
return $isAjax ? $json(['ok'=>true,'message'=>$msg])
|
||||
: redirect()->to(site_url('administrator/ip_bans'))->with('success', $msg);
|
||||
} catch (\Throwable $e) {
|
||||
$msg = 'Unable to unban: ' . $e->getMessage();
|
||||
return $isAjax ? $json(['ok'=>false,'message'=>$msg],500)
|
||||
: redirect()->back()->with('error', $msg);
|
||||
}
|
||||
}
|
||||
|
||||
public function banNow()
|
||||
{
|
||||
$isAjax = $this->request->isAJAX();
|
||||
$json = function(array $p, int $code=200){
|
||||
$p['csrfTokenName'] = csrf_token();
|
||||
$p['csrfHash'] = csrf_hash();
|
||||
$p[csrf_token()] = csrf_hash();
|
||||
return $this->response->setStatusCode($code)->setJSON($p);
|
||||
};
|
||||
|
||||
// Accept both POST and GET params
|
||||
$id = (int) ($this->request->getPost('id') ?? $this->request->getGet('id') ?? 0);
|
||||
$ip = trim((string) ($this->request->getPost('ip') ?? $this->request->getGet('ip') ?? ''));
|
||||
$hours = (int) ($this->request->getPost('hours') ?? $this->request->getGet('hours') ?? 24);
|
||||
if ($hours <= 0) $hours = 24;
|
||||
|
||||
try {
|
||||
$row = null;
|
||||
if ($id > 0) {
|
||||
$row = $this->ipAttemptModel->find($id);
|
||||
} elseif ($ip !== '') {
|
||||
$row = $this->ipAttemptModel->where('ip_address', $ip)->first();
|
||||
}
|
||||
|
||||
if (!$row) {
|
||||
$msg = 'IP record not found.';
|
||||
return $isAjax ? $json(['ok'=>false,'message'=>$msg],404)
|
||||
: redirect()->to(site_url('administrator/ip_bans'))->with('error', $msg);
|
||||
}
|
||||
|
||||
$blockedUntil = date('Y-m-d H:i:s', strtotime("+{$hours} hours"));
|
||||
$ok = $this->ipAttemptModel->update($row['id'], [
|
||||
'blocked_until' => $blockedUntil,
|
||||
// Optionally set attempts to threshold for clarity
|
||||
'attempts' => max((int)$row['attempts'], 10),
|
||||
]);
|
||||
|
||||
if (!$ok) {
|
||||
throw new \RuntimeException('Failed to update record.');
|
||||
}
|
||||
|
||||
$msg = 'IP ' . $row['ip_address'] . ' banned for ' . $hours . 'h.';
|
||||
return $isAjax ? $json(['ok'=>true,'message'=>$msg])
|
||||
: redirect()->to(site_url('administrator/ip_bans'))->with('success', $msg);
|
||||
} catch (\Throwable $e) {
|
||||
$msg = 'Unable to ban: ' . $e->getMessage();
|
||||
return $isAjax ? $json(['ok'=>false,'message'=>$msg],500)
|
||||
: redirect()->back()->with('error', $msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,882 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\UserRoleModel;
|
||||
use App\Models\StudentClassModel;
|
||||
use App\Models\InvoiceModel;
|
||||
use App\Models\TeacherClassModel;
|
||||
use App\Models\ClassSectionModel;
|
||||
use App\Models\SemesterScoreModel;
|
||||
use App\Models\ScoreCommentModel;
|
||||
use App\Models\AttendanceRecordModel;
|
||||
use App\Models\AttendanceDayModel;
|
||||
use App\Models\CalendarModel;
|
||||
use \Config\Database;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use App\Models\ConfigurationModel;
|
||||
|
||||
class LandingPageController extends BaseController
|
||||
{
|
||||
protected $classSectionId;
|
||||
protected $db;
|
||||
protected $schoolYear;
|
||||
protected $semester;
|
||||
protected $configModel;
|
||||
protected $lastDayOfRegistration;
|
||||
protected $refundDeadline;
|
||||
protected $studentClassModel;
|
||||
protected $invoiceModel;
|
||||
protected $userRoleModel;
|
||||
protected $teacherClassModel;
|
||||
protected $classSectionModel;
|
||||
protected $semesterScoreModel;
|
||||
protected $scoreCommentModel;
|
||||
protected $attendanceRecordModel;
|
||||
protected $attendanceDayModel;
|
||||
protected $calendarModel;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->configModel = new ConfigurationModel();
|
||||
$this->studentClassModel = new StudentClassModel();
|
||||
$this->invoiceModel = new InvoiceModel();
|
||||
$this->userRoleModel = new UserRoleModel();
|
||||
$this->teacherClassModel = new TeacherClassModel();
|
||||
$this->semesterScoreModel = new SemesterScoreModel();
|
||||
$this->scoreCommentModel = new ScoreCommentModel();
|
||||
$this->attendanceRecordModel = new AttendanceRecordModel();
|
||||
$this->attendanceDayModel = new AttendanceDayModel();
|
||||
$this->classSectionModel = new ClassSectionModel();
|
||||
$this->calendarModel = new CalendarModel();
|
||||
|
||||
// Fetch Enrollment and Refund Deadlines from Configuration
|
||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||
$this->semester = $this->configModel->getConfig('semester');
|
||||
$this->lastDayOfRegistration = $this->configModel->getConfig('enrollment_deadline') ?? 'Not set';
|
||||
$this->refundDeadline = $this->configModel->getConfig('refund_deadline') ?? 'Not set';
|
||||
|
||||
// Get class_id and store it in session
|
||||
$this->classSectionId = $this->classSectionId();
|
||||
session()->set('class_section_id', $this->classSectionId);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$userRole = $this->getUserRole(); // Assume this function gets the user role
|
||||
|
||||
switch (strtolower($userRole)) {
|
||||
case 'administrator':
|
||||
return $this->administrator();
|
||||
case 'admin':
|
||||
return $this->admin();
|
||||
case 'teacher':
|
||||
return $this->teacher();
|
||||
case 'student':
|
||||
return $this->student();
|
||||
case 'parent':
|
||||
case 'authorized_user':
|
||||
return $this->parentDashboard();
|
||||
default:
|
||||
return $this->guest();
|
||||
}
|
||||
}
|
||||
|
||||
public function classSectionId()
|
||||
{
|
||||
// Get user_id from the session
|
||||
$user_id = session()->get('user_id');
|
||||
|
||||
if (!$user_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get all class assignments for this teacher in the current term
|
||||
$assignments = $this->teacherClassModel->getClassAssignmentsByUserId(
|
||||
(int)$user_id,
|
||||
(string)$this->schoolYear,
|
||||
(string)$this->semester
|
||||
);
|
||||
|
||||
$ids = array_map(static fn($a) => (int)($a['class_section_id'] ?? 0), $assignments);
|
||||
$ids = array_values(array_filter(array_unique($ids)));
|
||||
|
||||
if (empty($ids)) {
|
||||
log_message('error', "No class section found for user ID: $user_id");
|
||||
return null;
|
||||
}
|
||||
|
||||
$current = (int)(session()->get('class_section_id') ?? 0);
|
||||
$chosen = in_array($current, $ids, true) ? $current : $ids[0];
|
||||
|
||||
session()->set([
|
||||
'class_section_id' => $chosen,
|
||||
'class_section_ids' => $ids,
|
||||
]);
|
||||
|
||||
return $chosen;
|
||||
}
|
||||
|
||||
|
||||
public function administrator()
|
||||
{
|
||||
return view('administrator/administratordashboard');
|
||||
}
|
||||
|
||||
public function admin()
|
||||
{
|
||||
return view('/landing_page/admin_dashboard');
|
||||
}
|
||||
|
||||
public function teacher()
|
||||
{
|
||||
$userId = (int)(session()->get('user_id') ?? 0);
|
||||
$assignments = $this->teacherClassModel->getClassAssignmentsByUserId(
|
||||
$userId,
|
||||
(string)$this->schoolYear,
|
||||
(string)$this->semester
|
||||
);
|
||||
|
||||
$availableIds = array_map(static fn($a) => (int)($a['class_section_id'] ?? 0), $assignments);
|
||||
$availableIds = array_values(array_filter(array_unique($availableIds)));
|
||||
|
||||
$requestedId = (int)($this->request->getGet('class_section_id') ?? 0);
|
||||
$activeId = null;
|
||||
|
||||
if ($requestedId && in_array($requestedId, $availableIds, true)) {
|
||||
$activeId = $requestedId;
|
||||
} elseif (!empty($availableIds)) {
|
||||
$activeId = in_array((int)$this->classSectionId, $availableIds, true)
|
||||
? (int)$this->classSectionId
|
||||
: $availableIds[0];
|
||||
}
|
||||
|
||||
if ($activeId) {
|
||||
session()->set('class_section_id', $activeId);
|
||||
}
|
||||
|
||||
$activeName = null;
|
||||
foreach ($assignments as $a) {
|
||||
if ((int)$a['class_section_id'] === (int)$activeId) {
|
||||
$activeName = $a['class_section_name'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$normalizedSemester = ucfirst(strtolower(trim((string)($this->semester ?? ''))));
|
||||
$classSectionIds = array_values(array_filter(array_unique(array_map(static fn($a) => (int)($a['class_section_id'] ?? 0), $assignments)), static fn($id) => $id > 0));
|
||||
$dashboardSummary = $this->buildTeacherDashboardSummary($classSectionIds, $normalizedSemester);
|
||||
|
||||
return view('/landing_page/teacher_dashboard', [
|
||||
'class_section_id' => $activeId,
|
||||
'active_class_name' => $activeName,
|
||||
'classes' => $assignments,
|
||||
'jobs' => $assignments,
|
||||
'jobCount' => count($assignments),
|
||||
'scoreSummary' => $dashboardSummary['scoreSummary'],
|
||||
'commentSummary' => $dashboardSummary['commentSummary'],
|
||||
'attendanceSummary' => $dashboardSummary['attendanceSummary'],
|
||||
'deadlineEvents' => $dashboardSummary['deadlineEvents'],
|
||||
'participationSummary' => $dashboardSummary['participationSummary'],
|
||||
'attendanceStatus' => $dashboardSummary['attendanceStatus'],
|
||||
'dashboardNotifications' => $dashboardSummary['notifications'],
|
||||
'school_year' => (string)$this->schoolYear,
|
||||
'semester' => (string)$this->semester,
|
||||
]);
|
||||
}
|
||||
|
||||
private function buildTeacherDashboardSummary(array $classSectionIds, string $semester): array
|
||||
{
|
||||
$normalizedSemester = ucfirst(strtolower(trim((string)$semester)));
|
||||
if ($normalizedSemester === '' || !in_array($normalizedSemester, ['Fall', 'Spring'], true)) {
|
||||
$normalizedSemester = 'Fall';
|
||||
}
|
||||
|
||||
$scoreField = ($normalizedSemester === 'Spring') ? 'final_exam_score' : 'midterm_exam_score';
|
||||
$scoreLabel = ($normalizedSemester === 'Spring') ? 'Final Exam' : 'Midterm';
|
||||
|
||||
$summary = [
|
||||
'scoreSummary' => [
|
||||
'completionPct' => 0,
|
||||
'missing' => 0,
|
||||
'expected' => 0,
|
||||
'filled' => 0,
|
||||
'fieldLabel' => $scoreLabel,
|
||||
],
|
||||
'commentSummary' => [
|
||||
'total' => 0,
|
||||
'pendingReview' => 0,
|
||||
'reviewed' => 0,
|
||||
'byType' => [],
|
||||
],
|
||||
'attendanceSummary' => [
|
||||
'recorded' => 0,
|
||||
'students' => 0,
|
||||
'completionPct' => 0,
|
||||
'avgAbsences' => 0,
|
||||
],
|
||||
'participationSummary' => [
|
||||
'filled' => 0,
|
||||
'missing' => 0,
|
||||
'completionPct' => 0,
|
||||
'expected' => 0,
|
||||
],
|
||||
'deadlineEvents' => [],
|
||||
];
|
||||
|
||||
if (!empty($classSectionIds)) {
|
||||
$studentRows = $this->db->table('student_class sc')
|
||||
->select('sc.student_id, sc.class_section_id')
|
||||
->join('students s', 's.id = sc.student_id', 'inner')
|
||||
->whereIn('sc.class_section_id', $classSectionIds)
|
||||
->where('s.is_active', 1)
|
||||
->where('sc.school_year', (string)$this->schoolYear)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$studentIds = [];
|
||||
foreach ($studentRows as $row) {
|
||||
$studentId = (int)($row['student_id'] ?? 0);
|
||||
$sectionId = (int)($row['class_section_id'] ?? 0);
|
||||
if ($studentId <= 0 || $sectionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$studentIds[] = $studentId;
|
||||
}
|
||||
$uniqueStudentIds = array_values(array_unique($studentIds));
|
||||
} else {
|
||||
$uniqueStudentIds = [];
|
||||
}
|
||||
|
||||
$totalStudents = count($uniqueStudentIds);
|
||||
$summary['attendanceSummary']['students'] = $totalStudents;
|
||||
|
||||
$scoreRows = [];
|
||||
if (!empty($classSectionIds)) {
|
||||
$scoreRows = $this->semesterScoreModel
|
||||
->select('student_id, class_section_id, midterm_exam_score, final_exam_score, participation_score')
|
||||
->whereIn('class_section_id', $classSectionIds)
|
||||
->where('semester', $normalizedSemester)
|
||||
->where('school_year', (string)$this->schoolYear)
|
||||
->findAll();
|
||||
}
|
||||
|
||||
$scoreTypesConfig = [
|
||||
'participation' => ['label' => 'Participation', 'field' => 'participation_score'],
|
||||
];
|
||||
if ($normalizedSemester === 'Fall') {
|
||||
$scoreTypesConfig = array_merge([
|
||||
'midterm' => ['label' => 'Midterm', 'field' => 'midterm_exam_score'],
|
||||
], $scoreTypesConfig);
|
||||
}
|
||||
if ($normalizedSemester === 'Spring') {
|
||||
$scoreTypesConfig = array_merge([
|
||||
'final' => ['label' => 'Final exam', 'field' => 'final_exam_score'],
|
||||
], $scoreTypesConfig);
|
||||
}
|
||||
|
||||
$filledCounts = array_fill_keys(array_keys($scoreTypesConfig), 0);
|
||||
foreach ($scoreRows as $row) {
|
||||
foreach ($scoreTypesConfig as $key => $cfg) {
|
||||
$value = trim((string)($row[$cfg['field']] ?? ''));
|
||||
if ($value !== '') {
|
||||
$filledCounts[$key]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$expectedScoreCount = $totalStudents > 0 ? $totalStudents : count($scoreRows);
|
||||
$scoreDetails = [];
|
||||
foreach ($scoreTypesConfig as $key => $cfg) {
|
||||
$filled = $filledCounts[$key] ?? 0;
|
||||
$missing = max(0, $expectedScoreCount - $filled);
|
||||
$completionPct = $expectedScoreCount > 0 ? (int)round(min(100, ($filled / $expectedScoreCount) * 100)) : 0;
|
||||
$scoreDetails[$key] = [
|
||||
'label' => $cfg['label'],
|
||||
'field' => $cfg['field'],
|
||||
'filled' => $filled,
|
||||
'missing' => $missing,
|
||||
'expected' => $expectedScoreCount,
|
||||
'completionPct' => $completionPct,
|
||||
];
|
||||
}
|
||||
|
||||
$scoreTypeByField = [];
|
||||
if ($normalizedSemester === 'Fall') {
|
||||
$scoreTypeByField['midterm_exam_score'] = 'midterm';
|
||||
}
|
||||
if ($normalizedSemester === 'Spring') {
|
||||
$scoreTypeByField['final_exam_score'] = 'final';
|
||||
}
|
||||
$activeScoreType = $scoreTypeByField[$scoreField] ?? 'midterm';
|
||||
$activeScoreDetails = $scoreDetails[$activeScoreType] ?? $scoreDetails['midterm'];
|
||||
|
||||
$summary['scoreSummary'] = [
|
||||
'completionPct' => $activeScoreDetails['completionPct'],
|
||||
'missing' => $activeScoreDetails['missing'],
|
||||
'expected' => $activeScoreDetails['expected'],
|
||||
'filled' => $activeScoreDetails['filled'],
|
||||
'fieldLabel' => $scoreLabel,
|
||||
'types' => $scoreDetails,
|
||||
];
|
||||
|
||||
$participationMetrics = $scoreDetails['participation'];
|
||||
$summary['participationSummary'] = [
|
||||
'filled' => $participationMetrics['filled'],
|
||||
'missing' => $participationMetrics['missing'],
|
||||
'completionPct' => $participationMetrics['completionPct'],
|
||||
'expected' => $participationMetrics['expected'],
|
||||
];
|
||||
|
||||
if (!empty($uniqueStudentIds)) {
|
||||
$commentRows = $this->scoreCommentModel
|
||||
->select('student_id, score_type, comment, comment_review')
|
||||
->whereIn('student_id', $uniqueStudentIds)
|
||||
->where('semester', $normalizedSemester)
|
||||
->where('school_year', (string)$this->schoolYear)
|
||||
->findAll();
|
||||
} else {
|
||||
$commentRows = [];
|
||||
}
|
||||
|
||||
$expectedCommentTypes = array_values(array_filter([
|
||||
'ptap',
|
||||
$normalizedSemester === 'Fall' ? 'midterm' : null,
|
||||
$normalizedSemester === 'Spring' ? 'final' : null,
|
||||
]));
|
||||
$commentStats = [];
|
||||
$commentsByStudent = [];
|
||||
|
||||
foreach ($commentRows as $row) {
|
||||
$sid = (int)($row['student_id'] ?? 0);
|
||||
$type = strtolower(trim((string)($row['score_type'] ?? '')));
|
||||
if ($type === '') {
|
||||
$type = 'general';
|
||||
}
|
||||
if (!isset($commentStats[$type])) {
|
||||
$commentStats[$type] = ['pending' => 0, 'reviewed' => 0];
|
||||
}
|
||||
|
||||
$commentText = trim((string)($row['comment'] ?? ''));
|
||||
$reviewValue = trim((string)($row['comment_review'] ?? ''));
|
||||
|
||||
if ($sid > 0 && $commentText !== '') {
|
||||
$commentsByStudent[$sid][$type] = $commentText;
|
||||
}
|
||||
|
||||
if ($commentText === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($reviewValue === '') {
|
||||
$commentStats[$type]['pending']++;
|
||||
} else {
|
||||
$commentStats[$type]['reviewed']++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($expectedCommentTypes as $type) {
|
||||
if (!isset($commentStats[$type])) {
|
||||
$commentStats[$type] = ['pending' => 0, 'reviewed' => 0];
|
||||
}
|
||||
}
|
||||
|
||||
$missingComments = 0;
|
||||
$missingDetail = [];
|
||||
foreach ($uniqueStudentIds as $sid) {
|
||||
foreach ($expectedCommentTypes as $type) {
|
||||
$text = $commentsByStudent[$sid][$type] ?? '';
|
||||
if ($text === '') {
|
||||
$missingDetail[$type] = ($missingDetail[$type] ?? 0) + 1;
|
||||
$missingComments++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$commentTypes = [];
|
||||
foreach ($expectedCommentTypes as $type) {
|
||||
$stats = $commentStats[$type];
|
||||
$filled = $stats['pending'] + $stats['reviewed'];
|
||||
$completionPct = $totalStudents > 0 ? (int)round(min(100, ($filled / $totalStudents) * 100)) : 0;
|
||||
|
||||
$commentTypes[$type] = [
|
||||
'label' => ucfirst($type) . ' comments',
|
||||
'pending' => $stats['pending'],
|
||||
'reviewed' => $stats['reviewed'],
|
||||
'filled' => $filled,
|
||||
'missing' => $missingDetail[$type] ?? 0,
|
||||
'expected' => $totalStudents,
|
||||
'completionPct' => $completionPct,
|
||||
];
|
||||
}
|
||||
|
||||
$totalPending = array_sum(array_column($commentTypes, 'pending'));
|
||||
$totalReviewed = array_sum(array_column($commentTypes, 'reviewed'));
|
||||
$totalFilled = array_sum(array_column($commentTypes, 'filled'));
|
||||
$totalExpected = array_sum(array_column($commentTypes, 'expected'));
|
||||
$commentCompletionPct = $totalExpected > 0 ? (int)round(min(100, ($totalFilled / $totalExpected) * 100)) : 0;
|
||||
|
||||
$summary['commentSummary'] = [
|
||||
'total' => $totalFilled,
|
||||
'pendingReview' => $totalPending,
|
||||
'reviewed' => $totalReviewed,
|
||||
'missing' => $missingComments,
|
||||
'missingDetail' => $missingDetail,
|
||||
'completionPct' => $commentCompletionPct,
|
||||
'types' => $commentTypes,
|
||||
];
|
||||
|
||||
$attendanceRows = [];
|
||||
if (!empty($classSectionIds)) {
|
||||
$attendanceRows = $this->attendanceRecordModel
|
||||
->select('student_id, class_section_id, total_absence')
|
||||
->whereIn('class_section_id', $classSectionIds)
|
||||
->where('semester', $normalizedSemester)
|
||||
->where('school_year', (string)$this->schoolYear)
|
||||
->findAll();
|
||||
}
|
||||
|
||||
$attendanceSet = [];
|
||||
$totalAbsences = 0;
|
||||
foreach ($attendanceRows as $record) {
|
||||
$studentId = (int)($record['student_id'] ?? 0);
|
||||
$sectionId = (int)($record['class_section_id'] ?? 0);
|
||||
if ($studentId <= 0 || $sectionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$key = "{$sectionId}:{$studentId}";
|
||||
$attendanceSet[$key] = true;
|
||||
$totalAbsences += (int)($record['total_absence'] ?? 0);
|
||||
}
|
||||
|
||||
$attendanceRecorded = count($attendanceSet);
|
||||
$attendancePct = $totalStudents > 0 ? (int)round(min(100, ($attendanceRecorded / $totalStudents) * 100)) : 0;
|
||||
$avgAbsence = $attendanceRecorded > 0 ? round($totalAbsences / $attendanceRecorded, 1) : 0;
|
||||
|
||||
$summary['attendanceSummary'] = [
|
||||
'recorded' => $attendanceRecorded,
|
||||
'students' => $totalStudents,
|
||||
'completionPct' => $attendancePct,
|
||||
'avgAbsences' => $avgAbsence,
|
||||
];
|
||||
$timezone = new DateTimeZone(date_default_timezone_get() ?: 'UTC');
|
||||
$today = (new DateTimeImmutable('now', $timezone))->format('Y-m-d');
|
||||
|
||||
$attendanceStatus = $this->buildAttendanceSubmissionStatus($classSectionIds, $normalizedSemester, $today);
|
||||
|
||||
$summary['deadlineEvents'] = $this->calendarModel
|
||||
->where('notify_teacher', 1)
|
||||
->where('school_year', (string)$this->schoolYear)
|
||||
->where('semester', $normalizedSemester)
|
||||
->where('date >=', $today)
|
||||
->orderBy('date', 'ASC')
|
||||
->limit(5)
|
||||
->findAll();
|
||||
|
||||
$summary['attendanceStatus'] = $attendanceStatus;
|
||||
if ($attendanceStatus['submitted'] === false && !empty($classSectionIds)) {
|
||||
$summary['attendanceSummary']['completionPct'] = 0;
|
||||
$summary['attendanceSummary']['recorded'] = 0;
|
||||
}
|
||||
|
||||
$summary['notifications'] = $this->buildDashboardNotifications($summary);
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
private function buildDashboardNotifications(array $summary): array
|
||||
{
|
||||
$notifications = [];
|
||||
$baseScoreLink = base_url('/teacher/scores');
|
||||
$buildScoreLink = static function (?string $focus) use ($baseScoreLink) {
|
||||
if (empty($focus)) {
|
||||
return $baseScoreLink;
|
||||
}
|
||||
return $baseScoreLink . '?focus=' . urlencode($focus);
|
||||
};
|
||||
$determineFocus = static function (array $event): string {
|
||||
$eventTypeText = strtolower(trim((string)($event['event_type'] ?? '')));
|
||||
if ($eventTypeText !== '' && str_contains($eventTypeText, 'draft')) {
|
||||
return 'comments';
|
||||
}
|
||||
$text = strtolower(trim(($event['title'] ?? '') . ' ' . ($event['description'] ?? '')));
|
||||
if ($text === '') {
|
||||
return 'scores';
|
||||
}
|
||||
if (str_contains($text, 'comment') || str_contains($text, 'ptap')) {
|
||||
return 'comments';
|
||||
}
|
||||
return 'scores';
|
||||
};
|
||||
$calendarLink = base_url('/teacher/calendar');
|
||||
$attendanceLink = base_url('/teacher/showupdate_attendance');
|
||||
$timezone = new DateTimeZone(date_default_timezone_get() ?: 'UTC');
|
||||
$todayDate = (new DateTimeImmutable('now', $timezone))->setTime(0, 0);
|
||||
$today = $todayDate->format('Y-m-d');
|
||||
|
||||
$calendarNotificationConfig = [
|
||||
'1st Semester Scores' => 14,
|
||||
'2nd Semester Scores' => 14,
|
||||
'Final' => 14,
|
||||
'Final Exam Draft' => 42,
|
||||
'Midterm' => 14,
|
||||
'Midterm Exam Draft' => 42,
|
||||
];
|
||||
|
||||
$activeDeadline = false;
|
||||
foreach ($summary['deadlineEvents'] ?? [] as $event) {
|
||||
$eventTypeRaw = trim((string)($event['event_type'] ?? ''));
|
||||
if ($eventTypeRaw === '') {
|
||||
continue;
|
||||
}
|
||||
$matchedType = null;
|
||||
$thresholdDays = null;
|
||||
foreach ($calendarNotificationConfig as $type => $days) {
|
||||
if (strcasecmp($eventTypeRaw, $type) === 0) {
|
||||
$matchedType = $type;
|
||||
$thresholdDays = $days;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($matchedType === null || $thresholdDays === null) {
|
||||
continue;
|
||||
}
|
||||
$rawDate = $event['date'] ?? '';
|
||||
if ($rawDate === '') {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$eventDateTime = new DateTimeImmutable($rawDate, $timezone);
|
||||
} catch (\Exception $e) {
|
||||
continue;
|
||||
}
|
||||
$eventDateTime = $eventDateTime->setTime(0, 0);
|
||||
$daysUntil = (int)$todayDate->diff($eventDateTime)->format('%r%a');
|
||||
|
||||
if ($daysUntil <= 0) {
|
||||
$activeDeadline = true;
|
||||
$eventFocus = $determineFocus($event);
|
||||
$notifications[] = [
|
||||
'type' => 'danger',
|
||||
'message' => sprintf(
|
||||
"Deadline today: %s is due. Submit the remaining entries before the day ends.",
|
||||
$matchedType
|
||||
),
|
||||
'link' => $buildScoreLink($eventFocus),
|
||||
'linkText' => 'Submit now',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($daysUntil > $thresholdDays) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relativeText = $daysUntil === 1 ? ' (tomorrow)' : " (in {$daysUntil} days)";
|
||||
$notifications[] = [
|
||||
'type' => 'warning',
|
||||
'message' => sprintf(
|
||||
"%s is due on %s%s. Review it on the calendar so you don’t miss it.",
|
||||
$matchedType,
|
||||
$eventDateTime->format('M j, Y'),
|
||||
$relativeText
|
||||
),
|
||||
'link' => $calendarLink,
|
||||
'linkText' => 'View deadline',
|
||||
];
|
||||
}
|
||||
|
||||
if (!$activeDeadline) {
|
||||
$scoreLabel = $summary['scoreSummary']['fieldLabel'] ?? 'Score';
|
||||
if (!empty($summary['scoreSummary']['missing'] ?? 0)) {
|
||||
$notifications[] = [
|
||||
'type' => 'warning',
|
||||
'message' => sprintf(
|
||||
"You have %s %s entries missing on the scores page. Please finish submitting them.",
|
||||
(int)$summary['scoreSummary']['missing'],
|
||||
$scoreLabel
|
||||
),
|
||||
'link' => $buildScoreLink('scores'),
|
||||
'linkText' => 'Score sheet',
|
||||
];
|
||||
}
|
||||
|
||||
foreach (['midterm', 'final', 'ptap'] as $type) {
|
||||
$count = (int)($summary['commentSummary']['missingDetail'][$type] ?? 0);
|
||||
if ($count <= 0) {
|
||||
continue;
|
||||
}
|
||||
$notifications[] = [
|
||||
'type' => 'warning',
|
||||
'message' => sprintf(
|
||||
"%s comment missing for %s student(s). Leave comments on the scores page.",
|
||||
ucfirst($type),
|
||||
$count
|
||||
),
|
||||
'link' => $buildScoreLink('comments'),
|
||||
'linkText' => ucfirst($type) . ' comments',
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($summary['participationSummary']['missing'] ?? 0)) {
|
||||
$notifications[] = [
|
||||
'type' => 'warning',
|
||||
'message' => sprintf(
|
||||
"Participation entries are still missing for %s student(s). Please add them from the scores page.",
|
||||
(int)$summary['participationSummary']['missing']
|
||||
),
|
||||
'link' => $buildScoreLink('scores'),
|
||||
'linkText' => 'Participation',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($summary['attendanceStatus']['missingSections'] ?? [])) {
|
||||
$missingNames = $summary['attendanceStatus']['missingNames'] ?? [];
|
||||
$label = '';
|
||||
if (!empty($missingNames)) {
|
||||
$label = ' (' . implode(', ', array_slice($missingNames, 0, 3)) . (count($missingNames) > 3 ? '…' : '') . ')';
|
||||
}
|
||||
$notifications[] = [
|
||||
'type' => 'danger',
|
||||
'message' => "Today's attendance is still unsubmitted for {$summary['attendanceStatus']['date']}{$label}. Please submit it now.",
|
||||
'link' => $attendanceLink,
|
||||
'linkText' => 'Record attendance',
|
||||
];
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
private function buildAttendanceSubmissionStatus(array $classSectionIds, string $semester, string $date): array
|
||||
{
|
||||
if (empty($classSectionIds)) {
|
||||
return [
|
||||
'date' => $date,
|
||||
'missingSections' => [],
|
||||
'missingNames' => [],
|
||||
'submitted' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$rows = $this->attendanceDayModel
|
||||
->select('class_section_id, status')
|
||||
->whereIn('class_section_id', $classSectionIds)
|
||||
->where('date', $date)
|
||||
->where('semester', $semester)
|
||||
->where('school_year', (string)$this->schoolYear)
|
||||
->findAll();
|
||||
|
||||
$statusMap = [];
|
||||
foreach ($rows as $row) {
|
||||
$csId = (int)($row['class_section_id'] ?? 0);
|
||||
if ($csId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$statusMap[$csId] = strtolower(trim((string)($row['status'] ?? '')));
|
||||
}
|
||||
|
||||
$missingSections = [];
|
||||
foreach ($classSectionIds as $classSectionId) {
|
||||
$status = $statusMap[$classSectionId] ?? '';
|
||||
if (!in_array($status, ['submitted', 'published', 'finalized'], true)) {
|
||||
$missingSections[] = $classSectionId;
|
||||
}
|
||||
}
|
||||
|
||||
$missingNames = [];
|
||||
if (!empty($missingSections)) {
|
||||
$sectionRows = $this->classSectionModel
|
||||
->select('class_section_id, class_section_name')
|
||||
->whereIn('class_section_id', $missingSections)
|
||||
->findAll();
|
||||
foreach ($sectionRows as $row) {
|
||||
$name = trim((string)($row['class_section_name'] ?? ''));
|
||||
if ($name === '') {
|
||||
$name = (string)($row['class_section_id'] ?? '');
|
||||
}
|
||||
$missingNames[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'date' => $date,
|
||||
'missingSections' => $missingSections,
|
||||
'missingNames' => $missingNames,
|
||||
'submitted' => empty($missingSections),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
public function student()
|
||||
{
|
||||
return view('/landing_page/student_dashboard');
|
||||
}
|
||||
|
||||
public function parentDashboard()
|
||||
{
|
||||
$parentId = session()->get('user_id');
|
||||
$userType = $_SESSION['user_type'];
|
||||
|
||||
// Map user type to roles
|
||||
if ($userType === 'primary') {
|
||||
// If user type is 'Primary', they are the firstparent
|
||||
$parentId = $parentId;
|
||||
} elseif ($userType === 'secondary') {
|
||||
// If user type is 'Secondary', find the parent_id from the parents table
|
||||
$parentData = $this->db->table('parents')
|
||||
->select('parent_id')
|
||||
->where('secondparent_user_id', $parentId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($parentData) {
|
||||
$parentId = $parentData['parent_id'];
|
||||
}
|
||||
} elseif ($userType === 'tertiary') {
|
||||
// If user type is 'Tertiary', find the parent_id from the authorized_users table
|
||||
$authUserData = $this->db->table('authorized_users')
|
||||
->select('user_id as parent_id')
|
||||
->where('authorized_user_id', $parentId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($authUserData) {
|
||||
$parentId = $authUserData['parent_id'];
|
||||
}
|
||||
}
|
||||
|
||||
// If no firstparent ID is found, show an error or redirect
|
||||
if (!$parentId) {
|
||||
return redirect()->back()->with(
|
||||
'error',
|
||||
'Unable to retrieve student data. Please contact support.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Fetch Notifications (only active, non-expired, non-deleted)
|
||||
$notifications = $this->db->table('notifications')
|
||||
->select([
|
||||
'notifications.id',
|
||||
'notifications.title',
|
||||
'notifications.message',
|
||||
'notifications.target_group',
|
||||
'notifications.created_at',
|
||||
'notifications.expires_at',
|
||||
'user_notifications.user_id',
|
||||
"CASE
|
||||
WHEN user_notifications.user_id IS NOT NULL THEN 'personal'
|
||||
ELSE 'broadcast'
|
||||
END as notification_type"
|
||||
])
|
||||
->join(
|
||||
'user_notifications',
|
||||
'user_notifications.notification_id = notifications.id AND user_notifications.user_id = ' . (int) $parentId,
|
||||
'left'
|
||||
)
|
||||
->groupStart()
|
||||
->where('notifications.target_group', 'parent')
|
||||
->orWhere('user_notifications.user_id', $parentId)
|
||||
->groupEnd()
|
||||
->where('notifications.deleted_at IS NULL') // Exclude soft-deleted notifications
|
||||
->groupStart()
|
||||
->where('notifications.expires_at IS NULL')
|
||||
->orWhere('notifications.expires_at > NOW()') // Exclude expired
|
||||
->groupEnd()
|
||||
->orderBy('notifications.created_at', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
// Fetch Student Information (no filtering needed by school year or semester)
|
||||
|
||||
$students = $this->db->table('students')
|
||||
->where('parent_id', $parentId)
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
foreach ($students as &$student) {
|
||||
// Get class_section_name and treat it as grade
|
||||
$classSection = $this->studentClassModel->getClassSectionsByStudentId($student['id'], $this->schoolYear);
|
||||
$student['class_section'] = $classSection;
|
||||
$student['grade'] = $classSection; // grade same as section
|
||||
}
|
||||
unset($student);
|
||||
|
||||
|
||||
// Fetch Attendance Records (filtered by most recent school year and semester)
|
||||
$attendanceData = $this->db->table('attendance_data')
|
||||
->select('attendance_data.*, students.firstname, students.lastname')
|
||||
->join('students', 'students.id = attendance_data.student_id')
|
||||
->where('students.parent_id', $parentId)
|
||||
// ->orWhere('students.secondparent_user_id', $parentId)
|
||||
->where('attendance_data.school_year', $this->schoolYear)
|
||||
->where('attendance_data.semester', $this->semester)
|
||||
->orderBy('attendance_data.date', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
// Fetch Grades (filtered by most recent school year and semester)
|
||||
$grades = $this->db->table('final_score') // Correct table name
|
||||
->select('final_score.*, students.firstname, students.lastname') // Ensure table name is correct
|
||||
->join('students', 'students.id = final_score.student_id')
|
||||
->where('students.parent_id', $parentId)
|
||||
// ->orWhere('students.secondparent_user_id', $parentId)
|
||||
->where('final_score.school_year', $this->schoolYear) // Ensure correct table column names
|
||||
->where('final_score.semester', $this->semester)
|
||||
->orderBy('final_score.created_at', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
// Fetch Enrollments (filtered by most recent school year and semester)
|
||||
$enrollments = $this->db->table('enrollments')
|
||||
->select('enrollments.*, classes.class_name')
|
||||
->join('students', 'students.id = enrollments.student_id')
|
||||
->join('classes', 'classes.id = enrollments.class_section_id') // Ensure this join is correct
|
||||
->where('students.parent_id', $parentId)
|
||||
// ->orWhere('students.secondparent_user_id', $parentId)
|
||||
->where('enrollments.school_year', $this->schoolYear)
|
||||
->where('enrollments.semester', $this->semester)
|
||||
->orderBy('enrollments.enrollment_date', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
// Fetch latest invoice balance
|
||||
$paymentBalance = $this->invoiceModel->getLatestInvoiceTotalAmount($parentId);
|
||||
|
||||
// Pass data to the view, including the deadlines
|
||||
return view('/landing_page/parent_dashboard', [
|
||||
'notifications' => $notifications,
|
||||
'students' => $students,
|
||||
'attendance' => $attendanceData,
|
||||
'grades' => $grades,
|
||||
'enrollments' => $enrollments,
|
||||
'lastDayOfRegistration' => $this->lastDayOfRegistration, // Add the enrollment deadline to the view
|
||||
'withdrawalDeadline' => $this->refundDeadline, // Add the refund deadline to the view
|
||||
'paymentBalance' => $paymentBalance, // 🔹 New
|
||||
]);
|
||||
}
|
||||
|
||||
public function guest()
|
||||
{
|
||||
return view('/landing_page/guest_dashboard');
|
||||
}
|
||||
|
||||
protected function getUserRole()
|
||||
{
|
||||
// Assuming you have a session variable storing the user role
|
||||
return session()->get('user_role', 'guest'); // Default to guest if not set
|
||||
}
|
||||
|
||||
protected function getUserRoleFromDatabase($user_id)
|
||||
{
|
||||
// Fetching user role from the database
|
||||
$role = $this->userRoleModel->getRoleByUserId($user_id);
|
||||
|
||||
return $role ?? 'guest';
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\LateSlipLogModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
|
||||
class LateSlipLogsController extends BaseController
|
||||
{
|
||||
private LateSlipLogModel $logModel;
|
||||
private ConfigurationModel $configModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->logModel = new LateSlipLogModel();
|
||||
$this->configModel = new ConfigurationModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$req = $this->request;
|
||||
|
||||
$defaultYear = (string) ($this->configModel->getConfig('school_year') ?? '');
|
||||
$defaultSem = (string) ($this->configModel->getConfig('semester') ?? '');
|
||||
$schoolYear = trim((string) ($req->getGet('school_year') ?? $defaultYear));
|
||||
$semester = trim((string) ($req->getGet('semester') ?? $defaultSem));
|
||||
$q = trim((string) ($req->getGet('q') ?? ''));
|
||||
$dateFrom = trim((string) ($req->getGet('date_from') ?? ''));
|
||||
$dateTo = trim((string) ($req->getGet('date_to') ?? ''));
|
||||
|
||||
// Normalize dates to Y-m-d for filtering
|
||||
$toDbDate = static function (?string $s): ?string {
|
||||
$s = trim((string) $s);
|
||||
if ($s === '') return null;
|
||||
$ts = strtotime($s);
|
||||
return $ts ? date('Y-m-d', $ts) : null;
|
||||
};
|
||||
$df = $toDbDate($dateFrom);
|
||||
$dt = $toDbDate($dateTo);
|
||||
|
||||
$model = $this->logModel;
|
||||
if ($schoolYear !== '') $model = $model->where('school_year', $schoolYear);
|
||||
if ($semester !== '') $model = $model->where('semester', $semester);
|
||||
if ($q !== '') $model = $model->like('student_name', $q);
|
||||
if ($df) $model = $model->where('slip_date >=', $df);
|
||||
if ($dt) $model = $model->where('slip_date <=', $dt);
|
||||
|
||||
// Limit to most recent 200 entries
|
||||
$logs = $model->orderBy('id', 'DESC')->findAll(200);
|
||||
|
||||
return view('administrator/late_slip_logs', [
|
||||
'schoolYear' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'filters' => [
|
||||
'q' => $q,
|
||||
'date_from' => $dateFrom,
|
||||
'date_to' => $dateTo,
|
||||
],
|
||||
'logs' => $logs,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Models\UserModel;
|
||||
use App\Models\StudentModel;
|
||||
use App\Models\TeacherClassModel;
|
||||
use App\Models\StudentClassModel;
|
||||
use App\Models\UserRoleModel;
|
||||
use App\Models\MessageModel;
|
||||
use App\Controllers\BaseController;
|
||||
|
||||
class MessagesController extends BaseController
|
||||
{
|
||||
protected $messageModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
//$this->messageModel = new MessageModel();
|
||||
}
|
||||
|
||||
public function inbox()
|
||||
{
|
||||
$userId = session()->get('user_id');
|
||||
$receivedMessages = $this->messageModel->getInboxMessages($userId);
|
||||
|
||||
return view('messages/inbox', [
|
||||
'receivedMessages' => $receivedMessages
|
||||
]);
|
||||
}
|
||||
|
||||
public function sent()
|
||||
{
|
||||
$userId = session()->get('user_id');
|
||||
$sentMessages = $this->messageModel->getSentMessages($userId);
|
||||
|
||||
return view('messages/sent', [
|
||||
'sentMessages' => $sentMessages
|
||||
]);
|
||||
}
|
||||
|
||||
public function drafts()
|
||||
{
|
||||
$userId = session()->get('user_id');
|
||||
$draftMessages = $this->messageModel->getDraftMessages($userId);
|
||||
|
||||
return view('messages/drafts', [
|
||||
'draftMessages' => $draftMessages
|
||||
]);
|
||||
}
|
||||
|
||||
public function trash()
|
||||
{
|
||||
$userId = session()->get('user_id');
|
||||
$trashedMessages = $this->messageModel->getTrashedMessages($userId);
|
||||
|
||||
return view('messages/trash', [
|
||||
'trashedMessages' => $trashedMessages
|
||||
]);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$userRoleModel = new UserRoleModel();
|
||||
//$role = session()->get('role');
|
||||
$userId = session()->get('user_id');
|
||||
// Fetch the user role from the user_roles and roles tables
|
||||
$role = $userRoleModel->select('roles.name')
|
||||
->join('roles', 'roles.id = user_roles.role_id')
|
||||
->where('user_roles.user_id', $userId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
// Fetch received messages based on role
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('messages');
|
||||
$builder->select('messages.*, users.firstname AS sender_name');
|
||||
$builder->join('users', 'messages.sender_id = users.id');
|
||||
$builder->where('messages.recipient_id', $userId);
|
||||
$query = $builder->get();
|
||||
$receivedMessages = $query->getResultArray();
|
||||
|
||||
return view('/messages', [
|
||||
'receivedMessages' => $receivedMessages,
|
||||
'role' => $role['name']
|
||||
]);
|
||||
}
|
||||
|
||||
public function send()
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$userRoleModel = new UserRoleModel();
|
||||
//$role = session()->get('role');
|
||||
$userId = session()->get('user_id');
|
||||
// Fetch the user role from the user_roles and roles tables
|
||||
$role = $userRoleModel->select('roles.name')
|
||||
->join('roles', 'roles.id = user_roles.role_id')
|
||||
->where('user_roles.user_id', $userId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
// Handle file upload (if any)
|
||||
$attachmentPath = null;
|
||||
$file = $this->request->getFile('attachment');
|
||||
if ($file && $file->isValid() && !$file->hasMoved()) {
|
||||
$attachmentPath = $file->store();
|
||||
}
|
||||
|
||||
// Determine recipient based on role or input
|
||||
$recipientId = $this->getRecipientId($role);
|
||||
|
||||
// Prepare data for insertion
|
||||
$data = [
|
||||
'sender_id' => $userId,
|
||||
'recipient_id' => $recipientId,
|
||||
'subject' => $this->request->getPost('subject'),
|
||||
'message' => $this->request->getPost('message'),
|
||||
'sent_datetime' => utc_now(),
|
||||
'message_number' => $this->generateMessageNumber(),
|
||||
'priority' => $this->request->getPost('priority') ?: 'normal',
|
||||
'attachment' => $attachmentPath,
|
||||
'status' => 'sent'
|
||||
];
|
||||
|
||||
$builder = $db->table('messages');
|
||||
$builder->insert($data);
|
||||
|
||||
return redirect()->to(base_url('messages'))->with('status', 'Message sent successfully');
|
||||
}
|
||||
|
||||
public function receive()
|
||||
{
|
||||
$userId = session()->get('user_id');
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('messages');
|
||||
$builder->select('messages.*, users.firstname AS sender_name');
|
||||
$builder->join('users', 'messages.sender_id = users.id');
|
||||
$builder->where('messages.recipient_id', $userId);
|
||||
$builder->orderBy('sent_datetime', 'DESC');
|
||||
$query = $builder->get();
|
||||
$receivedMessages = $query->getResultArray();
|
||||
|
||||
// Mark messages as read
|
||||
foreach ($receivedMessages as $message) {
|
||||
if ($message['read_status'] == 0) {
|
||||
$this->markAsRead($message['id']);
|
||||
}
|
||||
}
|
||||
// Assuming $role['name'] contains the role name
|
||||
//$roleName = strtolower($role['name']); // Convert role name to lowercase if needed
|
||||
|
||||
// Construct the view path dynamically
|
||||
// $viewPath = '/' . $roleName . '/' . $roleName . '_messages';
|
||||
|
||||
// Return the view
|
||||
//return view($viewPath, ['receivedMessages' => $receivedMessages]);
|
||||
}
|
||||
|
||||
private function generateMessageNumber()
|
||||
{
|
||||
return 'MSG-' . date('YmdHis');
|
||||
}
|
||||
|
||||
private function markAsRead($messageId)
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('messages');
|
||||
$builder->where('id', $messageId);
|
||||
$builder->update([
|
||||
'read_status' => 1,
|
||||
'read_datetime' => utc_now()
|
||||
]);
|
||||
}
|
||||
|
||||
private function getRecipientId($role)
|
||||
{
|
||||
// You can customize this logic to retrieve recipient_id based on the role or input
|
||||
switch (strtolower($role['name'])) {
|
||||
case 'teacher':
|
||||
return 1; // Example: return teacher's ID
|
||||
case 'parent':
|
||||
return 2; // Example: return parent's ID
|
||||
case 'admin':
|
||||
return 3; // Example: return admin's ID
|
||||
case 'student':
|
||||
return 4; // Example: return student's ID
|
||||
case 'guest':
|
||||
return 5; // Example: return guest's ID
|
||||
case 'administrator':
|
||||
return 6; // Example: return administrator's ID
|
||||
default:
|
||||
throw new \Exception('Invalid role: ' . $role['name']);
|
||||
}
|
||||
}
|
||||
|
||||
public function getRecipients($type)
|
||||
{
|
||||
$recipients = [];
|
||||
$teacherClassModel = new TeacherClassModel();
|
||||
$studentClassModel = new StudentClassModel();
|
||||
$studentsModel = new StudentModel();
|
||||
$teacherClasses = $teacherClassModel->findAll();
|
||||
|
||||
if ($type === 'teacher') {
|
||||
$userModel = new UserModel(); // Assuming the UserModel is in the App\Models namespace
|
||||
|
||||
foreach ($teacherClasses as $teacher) {
|
||||
$user = $userModel->find($teacher['teacher_id']);
|
||||
if ($user) {
|
||||
$recipients[] = [
|
||||
'id' => $teacher['teacher_id'],
|
||||
'name' => $user['firstname'] . ' ' . $user['lastname']
|
||||
];
|
||||
}
|
||||
}
|
||||
} elseif ($type === 'parent') {
|
||||
foreach ($teacherClasses as $class) {
|
||||
$students = $studentClassModel
|
||||
->active()
|
||||
->where('student_class.class_section_id', $class['class_section_id'])
|
||||
->findAll();
|
||||
|
||||
foreach ($students as $student) {
|
||||
$studentData = $studentsModel
|
||||
->where('id', $student['student_id'])
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
if ($studentData) {
|
||||
if ($studentData['firstparent']) {
|
||||
$recipients[] = [
|
||||
'id' => $studentData['parent_id'],
|
||||
'name' => $studentData['firstparent']
|
||||
];
|
||||
}
|
||||
if ($studentData['secondparent']) {
|
||||
$recipients[] = [
|
||||
'id' => $studentData['secondparent_user_id'],
|
||||
'name' => $studentData['secondparent']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->response->setJSON($recipients);
|
||||
}
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\NavItemModel;
|
||||
use App\Models\RoleNavItemModel;
|
||||
use App\Services\NavbarService;
|
||||
|
||||
class NavBuilderController extends BaseController
|
||||
{
|
||||
protected NavItemModel $items;
|
||||
protected RoleNavItemModel $maps;
|
||||
protected NavbarService $service;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->items = new NavItemModel();
|
||||
$this->maps = new RoleNavItemModel();
|
||||
$this->service = new NavbarService();
|
||||
}
|
||||
|
||||
protected function ensureAdmin(): void
|
||||
{
|
||||
$sessionRole = session()->get('role'); // could be a string or array in your app
|
||||
$roleNames = is_array($sessionRole) ? $sessionRole : [$sessionRole];
|
||||
$roleNames = array_values(array_filter(array_map('strval', $roleNames)));
|
||||
|
||||
if (empty($roleNames)) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
// Map role names -> ids
|
||||
$roleIdRows = $db->table('roles')->select('id')->whereIn('name', $roleNames)->get()->getResultArray();
|
||||
$roleIds = array_map('intval', array_column($roleIdRows, 'id'));
|
||||
if (empty($roleIds)) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
// Is this route allowed for any of the user's roles?
|
||||
$allowed = $db->table('role_nav_items AS rni')
|
||||
->select('1')
|
||||
->join('nav_items AS ni', 'ni.id = rni.nav_item_id')
|
||||
->where('ni.url', 'nav-builder') // IMPORTANT: your current route path
|
||||
->whereIn('rni.role_id', $roleIds)
|
||||
->get(1)->getFirstRow();
|
||||
|
||||
if (!$allowed) {
|
||||
// You can show a nicer "Access Denied" view if you prefer
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
}
|
||||
|
||||
public function index(){
|
||||
$this->ensureAdmin();
|
||||
|
||||
helper(['url', 'form']);
|
||||
|
||||
return view('nav_builder/index');
|
||||
}
|
||||
|
||||
/** Case-insensitive, natural sort; ignores leading punctuation & articles (“a/an/the”) */
|
||||
private function labelKey(string $s): string
|
||||
{
|
||||
$s = trim(preg_replace('/\s+/', ' ', $s));
|
||||
$s = preg_replace('/^[^[:alnum:]]+/u', '', $s); // strip leading punctuation
|
||||
$s = preg_replace('/^(?i)(the|an|a)\s+/', '', $s); // drop leading articles
|
||||
return mb_strtolower($s, 'UTF-8');
|
||||
}
|
||||
|
||||
private function sortTreeAlpha(array &$nodes): void
|
||||
{
|
||||
usort($nodes, fn($a,$b) => strnatcasecmp(
|
||||
$this->labelKey($a['label'] ?? ''), $this->labelKey($b['label'] ?? '')
|
||||
));
|
||||
foreach ($nodes as &$n) {
|
||||
if (!empty($n['children'])) {
|
||||
$this->sortTreeAlpha($n['children']);
|
||||
}
|
||||
}
|
||||
unset($n);
|
||||
}
|
||||
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->ensureAdmin();
|
||||
|
||||
$id = (int) $this->request->getPost('id');
|
||||
|
||||
// Accept either "menu_parent_id" or "parent_id" from the form
|
||||
$menuParentRaw = $this->request->getPost('menu_parent_id');
|
||||
if ($menuParentRaw === null) {
|
||||
$menuParentRaw = $this->request->getPost('parent_id');
|
||||
}
|
||||
|
||||
$menuParentId = ($menuParentRaw === '' || $menuParentRaw === null) ? null : (int) $menuParentRaw;
|
||||
if ($id && $menuParentId === $id) {
|
||||
// item cannot be its own parent
|
||||
$menuParentId = null;
|
||||
}
|
||||
|
||||
// Validate parent exists (optional but helpful)
|
||||
if ($menuParentId !== null) {
|
||||
$parent = $this->items->select('id')->where('id', $menuParentId)->first();
|
||||
if (!$parent) {
|
||||
return redirect()->back()->with('error', 'Selected parent does not exist.')->withInput();
|
||||
}
|
||||
}
|
||||
|
||||
$data = [
|
||||
'menu_parent_id' => $menuParentId,
|
||||
'label' => trim((string) $this->request->getPost('label')),
|
||||
'url' => trim((string) $this->request->getPost('url')) ?: null,
|
||||
'icon_class' => trim((string) $this->request->getPost('icon_class')) ?: null,
|
||||
'target' => trim((string) $this->request->getPost('target')) ?: null,
|
||||
'sort_order' => (int) $this->request->getPost('sort_order'),
|
||||
'is_enabled' => (int) ($this->request->getPost('is_enabled') ? 1 : 0),
|
||||
];
|
||||
|
||||
// Save (force insert to return ID)
|
||||
if ($id) {
|
||||
$this->items->update($id, $data);
|
||||
} else {
|
||||
$id = (int) $this->items->insert($data, true); // ensure insertID is returned
|
||||
}
|
||||
|
||||
// Roles
|
||||
$roleIds = array_values(array_unique(array_filter(
|
||||
array_map('intval', (array) ($this->request->getPost('roles') ?? [])),
|
||||
fn ($v) => $v > 0
|
||||
)));
|
||||
|
||||
$this->maps->where('nav_item_id', $id)->delete();
|
||||
foreach ($roleIds as $rid) {
|
||||
$this->maps->insert(['role_id' => $rid, 'nav_item_id' => $id]);
|
||||
}
|
||||
|
||||
$this->service->clearCache();
|
||||
return redirect()->back()->with('success', 'Menu saved.');
|
||||
}
|
||||
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$this->ensureAdmin();
|
||||
$id = (int) $id;
|
||||
|
||||
$this->items->delete($id); // children handled by FK (SET NULL on parent)
|
||||
$this->service->clearCache();
|
||||
|
||||
return redirect()->back()->with('success', 'Menu item deleted.');
|
||||
}
|
||||
|
||||
public function reorder()
|
||||
{
|
||||
$this->ensureAdmin();
|
||||
|
||||
$orders = $this->request->getPost('orders') ?? [];
|
||||
foreach ($orders as $id => $order) {
|
||||
$this->items->update((int) $id, ['sort_order' => (int) $order]);
|
||||
}
|
||||
$this->service->clearCache();
|
||||
return $this->response->setJSON(['ok' => true]);
|
||||
}
|
||||
|
||||
protected function distinctRoles(): array
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
return $db->table('roles')
|
||||
->select('id, name')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
public function data()
|
||||
{
|
||||
$this->ensureAdmin();
|
||||
return $this->response->setJSON($this->buildNavPayload());
|
||||
}
|
||||
|
||||
private function buildNavPayload(): array
|
||||
{
|
||||
$all = $this->items
|
||||
->orderBy('menu_parent_id', 'ASC')
|
||||
->orderBy('label', 'ASC')
|
||||
->orderBy('id', 'ASC')
|
||||
->findAll();
|
||||
|
||||
$byId = [];
|
||||
foreach ($all as $a) {
|
||||
$a['children'] = [];
|
||||
$byId[$a['id']] = $a;
|
||||
}
|
||||
|
||||
$tree = [];
|
||||
foreach ($byId as $id => &$node) {
|
||||
$pid = $node['menu_parent_id'] ?: null;
|
||||
if ($pid && isset($byId[$pid])) {
|
||||
$byId[$pid]['children'][] = &$node;
|
||||
} else {
|
||||
$tree[] = &$node;
|
||||
}
|
||||
}
|
||||
unset($node);
|
||||
|
||||
$this->sortTreeAlpha($tree);
|
||||
|
||||
$flatAlpha = $all;
|
||||
usort($flatAlpha, fn($a, $b) => strnatcasecmp(
|
||||
$this->labelKey($a['label'] ?? ''),
|
||||
$this->labelKey($b['label'] ?? '')
|
||||
));
|
||||
|
||||
$roleAssignments = [];
|
||||
$roleRows = $this->maps
|
||||
->select('role_nav_items.nav_item_id, roles.id AS role_id, roles.name AS role_name')
|
||||
->join('roles', 'roles.id = role_nav_items.role_id', 'left')
|
||||
->findAll();
|
||||
|
||||
foreach ($roleRows as $row) {
|
||||
$navId = (int) ($row['nav_item_id'] ?? 0);
|
||||
if ($navId <= 0) continue;
|
||||
|
||||
$roleId = (int) ($row['role_id'] ?? 0);
|
||||
$roleName = $row['role_name'] ?? ($roleId ? ('#' . $roleId) : null);
|
||||
|
||||
if ($roleId > 0) {
|
||||
$roleAssignments[$navId]['ids'][] = $roleId;
|
||||
}
|
||||
if ($roleName !== null) {
|
||||
$roleAssignments[$navId]['names'][] = $roleName;
|
||||
}
|
||||
}
|
||||
|
||||
$flattened = $this->flattenTreeForResponse($tree, $roleAssignments);
|
||||
|
||||
$parentOptions = array_map(static function ($row) {
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'label' => (string) ($row['label'] ?? ''),
|
||||
];
|
||||
}, $flatAlpha);
|
||||
|
||||
$roles = array_map(static function ($role) {
|
||||
return [
|
||||
'id' => (int) ($role['id'] ?? 0),
|
||||
'name' => (string) ($role['name'] ?? ''),
|
||||
];
|
||||
}, $this->distinctRoles());
|
||||
|
||||
return [
|
||||
'items' => $flattened,
|
||||
'roles' => $roles,
|
||||
'parentOptions' => $parentOptions,
|
||||
];
|
||||
}
|
||||
|
||||
private function flattenTreeForResponse(array $nodes, array $roleAssignments, ?string $parentLabel = null, int $depth = 0, array &$rows = []): array
|
||||
{
|
||||
foreach ($nodes as $node) {
|
||||
$raw = $node;
|
||||
unset($raw['children']);
|
||||
|
||||
$navId = (int) ($raw['id'] ?? 0);
|
||||
$roles = $roleAssignments[$navId] ?? ['ids' => [], 'names' => []];
|
||||
|
||||
$rows[] = [
|
||||
'id' => $navId,
|
||||
'label' => (string) ($raw['label'] ?? ''),
|
||||
'url' => $raw['url'] ?? null,
|
||||
'parent_label' => $parentLabel ?? '—',
|
||||
'parent_id' => isset($raw['menu_parent_id']) && (int) $raw['menu_parent_id'] !== 0
|
||||
? (int) $raw['menu_parent_id']
|
||||
: null,
|
||||
'order' => (int) ($raw['sort_order'] ?? 0),
|
||||
'enabled' => (int) ($raw['is_enabled'] ?? 0) === 1,
|
||||
'target' => $raw['target'] ?? null,
|
||||
'depth' => $depth,
|
||||
'roles' => [
|
||||
'ids' => array_values(array_unique($roles['ids'] ?? [])),
|
||||
'names' => array_values(array_unique($roles['names'] ?? [])),
|
||||
],
|
||||
'raw' => $raw,
|
||||
];
|
||||
|
||||
if (!empty($node['children'])) {
|
||||
$this->flattenTreeForResponse(
|
||||
$node['children'],
|
||||
$roleAssignments,
|
||||
(string) ($raw['label'] ?? ''),
|
||||
$depth + 1,
|
||||
$rows
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\NavItemModel;
|
||||
use App\Models\RoleNavItemModel;
|
||||
use CodeIgniter\Cache\CacheInterface;
|
||||
|
||||
class NavbarService
|
||||
{
|
||||
public function __construct(
|
||||
protected NavItemModel $navModel = new NavItemModel(),
|
||||
protected RoleNavItemModel $mapModel = new RoleNavItemModel(),
|
||||
protected ?CacheInterface $cache = null
|
||||
){
|
||||
$this->cache ??= cache();
|
||||
}
|
||||
|
||||
public function getMenuForRoles(array $roles): array
|
||||
{
|
||||
$roles = array_map(fn($r)=>strtolower(trim((string)$r)), $roles);
|
||||
$cacheKey = 'navbar_' . md5(json_encode($roles));
|
||||
|
||||
if ($menu = $this->cache->get($cacheKey)) {
|
||||
return $menu;
|
||||
}
|
||||
|
||||
// Which items this user can see
|
||||
$allowedIds = $this->mapModel->getNavItemIdsForRoles($roles);
|
||||
if (empty($allowedIds)) return [];
|
||||
|
||||
// Load all enabled items, then filter
|
||||
$rows = $this->navModel->where('is_enabled', 1)
|
||||
->orderBy('sort_order', 'ASC')
|
||||
->findAll();
|
||||
|
||||
// index by id
|
||||
$byId = [];
|
||||
foreach ($rows as $r) {
|
||||
if (in_array($r['id'], $allowedIds, true)) {
|
||||
$r['children'] = [];
|
||||
$byId[$r['id']] = $r;
|
||||
}
|
||||
}
|
||||
|
||||
// Build tree
|
||||
$tree = [];
|
||||
foreach ($byId as $id => &$node) {
|
||||
$pid = $node['menu_parent_id'];
|
||||
if ($pid && isset($byId[$pid])) {
|
||||
$byId[$pid]['children'][] = &$node;
|
||||
} else {
|
||||
$tree[] = &$node;
|
||||
}
|
||||
}
|
||||
|
||||
$this->cache->save($cacheKey, $tree, 300); // 5 minutes
|
||||
return $tree;
|
||||
}
|
||||
|
||||
public function clearCache(): void
|
||||
{
|
||||
// simplest: flush; or if you have a tagged cache, clear only keys
|
||||
cache()->clean();
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use CodeIgniter\Events\Events;
|
||||
|
||||
class NotificationService
|
||||
{
|
||||
/**
|
||||
* Trigger a notification event by type and payload.
|
||||
*
|
||||
* @param string $eventType The name of the event (e.g., 'userRegistered')
|
||||
* @param array $payload The data to pass to the listener
|
||||
* @return void
|
||||
*/
|
||||
public static function trigger(string $eventType, array $payload): void
|
||||
{
|
||||
Events::trigger($eventType, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for sending to a specific user.
|
||||
*
|
||||
* @param int $userId
|
||||
* @param string $title
|
||||
* @param string $message
|
||||
* @param array $channels
|
||||
* @return void
|
||||
*/
|
||||
public static function toUser(int $userId, string $title, string $message, array $channels = ['in_app']): void
|
||||
{
|
||||
self::trigger('customNotification', [
|
||||
'user_id' => $userId,
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'channels' => $channels
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Controllers\View\EmailController;
|
||||
use App\Models\NotificationModel;
|
||||
use App\Models\UserNotificationModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
class NotificationsController extends BaseController
|
||||
{
|
||||
protected $notificationModel;
|
||||
protected $userNotificationModel;
|
||||
protected $userModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->notificationModel = new NotificationModel();
|
||||
$this->userNotificationModel = new UserNotificationModel();
|
||||
$this->userModel = new UserModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$userId = session()->get('user_id');
|
||||
|
||||
$notifications = $this->userNotificationModel
|
||||
->select('notifications.*, user_notifications.is_read')
|
||||
->join('notifications', 'notifications.id = user_notifications.notification_id')
|
||||
->where('user_notifications.user_id', $userId)
|
||||
->orderBy('notifications.created_at', 'DESC')
|
||||
->findAll();
|
||||
|
||||
return view('notifications/index', ['notifications' => $notifications]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('notifications/create');
|
||||
}
|
||||
|
||||
public function send()
|
||||
{
|
||||
$title = $this->request->getPost('title');
|
||||
$message = $this->request->getPost('message');
|
||||
$group = $this->request->getPost('target_group');
|
||||
$channels = $this->request->getPost('channels'); // ['in_app', 'email', 'sms']
|
||||
|
||||
$notifId = $this->notificationModel->insert([
|
||||
'title' => $title,
|
||||
'message' => $message,
|
||||
'target_group' => $group,
|
||||
'delivery_channels' => implode(',', $channels),
|
||||
'created_at' => utc_now()
|
||||
]);
|
||||
|
||||
$users = $this->userModel->where('role', $group)->findAll();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$this->userNotificationModel->insert([
|
||||
'notification_id' => $notifId,
|
||||
'user_id' => $user['id'],
|
||||
'is_read' => false
|
||||
]);
|
||||
|
||||
if (in_array('email', $channels)) {
|
||||
$this->sendEmail($user['email'], $title, $message);
|
||||
}
|
||||
|
||||
if (in_array('sms', $channels) && !empty($user['phone'])) {
|
||||
$this->sendSMS($user['phone'], $message);
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', 'Notification sent successfully.');
|
||||
}
|
||||
|
||||
public function markAsRead($id)
|
||||
{
|
||||
$userId = session()->get('user_id');
|
||||
$this->userNotificationModel->where('notification_id', $id)
|
||||
->where('user_id', $userId)
|
||||
->set(['is_read' => 1])
|
||||
->update();
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function listActive()
|
||||
{
|
||||
helper('url');
|
||||
|
||||
$targetGroup = $this->request->getGet('target_group');
|
||||
|
||||
return view('notifications/list_active', [
|
||||
'notificationsEndpoint' => site_url('api/notifications/active'),
|
||||
'defaultTargetGroup' => $targetGroup,
|
||||
]);
|
||||
}
|
||||
|
||||
public function listDeleted()
|
||||
{
|
||||
helper(['url', 'form']);
|
||||
|
||||
return view('notifications/list_deleted', [
|
||||
'deletedNotificationsEndpoint' => site_url('api/notifications/deleted'),
|
||||
'restoreEndpoint' => site_url('notifications/restore'),
|
||||
'csrfTokenName' => csrf_token(),
|
||||
'csrfTokenValue' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function activeNotificationsData()
|
||||
{
|
||||
$targetGroup = $this->request->getGet('target_group');
|
||||
|
||||
return $this->response->setJSON($this->buildActiveNotificationsPayload($targetGroup));
|
||||
}
|
||||
|
||||
public function deletedNotificationsData()
|
||||
{
|
||||
return $this->response->setJSON($this->buildDeletedNotificationsPayload());
|
||||
}
|
||||
|
||||
public function restore($id)
|
||||
{
|
||||
if ($this->notificationModel->restoreNotification($id)) {
|
||||
return redirect()->to(base_url('notifications/deleted'))->with('success', 'Notification restored successfully.');
|
||||
}
|
||||
|
||||
return redirect()->to(base_url('notifications/deleted'))->with('error', 'Failed to restore notification.');
|
||||
}
|
||||
|
||||
protected function sendEmail($to, $subject, $message)
|
||||
{
|
||||
$mailer = new EmailController();
|
||||
$mailer->sendEmail($to, $subject, $message);
|
||||
}
|
||||
|
||||
protected function sendSMS($phone, $message)
|
||||
{
|
||||
log_message('info', "SMS to {$phone}: {$message}");
|
||||
}
|
||||
|
||||
private function buildActiveNotificationsPayload(?string $targetGroup): array
|
||||
{
|
||||
$targetGroup = $targetGroup !== null ? trim($targetGroup) : null;
|
||||
if ($targetGroup === '') {
|
||||
$targetGroup = null;
|
||||
}
|
||||
|
||||
$rows = $this->notificationModel->getActiveNotifications($targetGroup);
|
||||
if (!is_array($rows)) {
|
||||
$rows = [];
|
||||
}
|
||||
|
||||
$now = time();
|
||||
|
||||
$notifications = array_map(static function ($row) use ($now) {
|
||||
if (!is_array($row)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$expiresAt = $row['expires_at'] ?? null;
|
||||
$expiryTs = $expiresAt ? strtotime((string) $expiresAt) : false;
|
||||
$isExpired = $expiryTs !== false && $expiryTs <= $now;
|
||||
|
||||
return [
|
||||
'id' => $row['id'] ?? null,
|
||||
'title' => $row['title'] ?? null,
|
||||
'message' => $row['message'] ?? null,
|
||||
'priority' => $row['priority'] ?? null,
|
||||
'scheduled_at' => $row['scheduled_at'] ?? null,
|
||||
'expires_at' => $expiresAt,
|
||||
'created_at' => $row['created_at'] ?? null,
|
||||
'target_group' => $row['target_group'] ?? null,
|
||||
'isExpired' => $isExpired,
|
||||
];
|
||||
}, $rows);
|
||||
|
||||
return [
|
||||
'notifications' => $notifications,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildDeletedNotificationsPayload(): array
|
||||
{
|
||||
$rows = $this->notificationModel->getDeletedNotifications();
|
||||
if (!is_array($rows)) {
|
||||
$rows = [];
|
||||
}
|
||||
|
||||
$notifications = array_map(static function ($row) {
|
||||
if (!is_array($row)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $row['id'] ?? null,
|
||||
'title' => $row['title'] ?? null,
|
||||
'message' => $row['message'] ?? null,
|
||||
'priority' => $row['priority'] ?? null,
|
||||
'scheduled_at' => $row['scheduled_at'] ?? null,
|
||||
'expires_at' => $row['expires_at'] ?? null,
|
||||
'deleted_at' => $row['deleted_at'] ?? null,
|
||||
];
|
||||
}, $rows);
|
||||
|
||||
return [
|
||||
'notifications' => $notifications,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Models\ContactUsModel;
|
||||
use App\Controllers\BaseController;
|
||||
|
||||
class PageController extends BaseController
|
||||
{
|
||||
public function privacyPolicy()
|
||||
{
|
||||
return $this->response->setBody(file_get_contents(ROOTPATH . 'public/html/privacy_policy.html'))
|
||||
->setContentType('text/html');
|
||||
}
|
||||
|
||||
public function termsOfService()
|
||||
{
|
||||
return $this->response->setBody(file_get_contents(ROOTPATH . 'public/html/terms_of_service.html'))
|
||||
->setContentType('text/html');
|
||||
}
|
||||
|
||||
public function helpCenter()
|
||||
{
|
||||
return $this->response->setBody(file_get_contents(ROOTPATH . 'public/html/help_center.html'))
|
||||
->setContentType('text/html');
|
||||
}
|
||||
|
||||
public function submitContactForm()
|
||||
{
|
||||
try {
|
||||
$email = strtolower($this->request->getPost('email'));
|
||||
$message = $this->request->getPost('message');
|
||||
|
||||
// Save the message to the database
|
||||
$contactModel = new ContactUsModel();
|
||||
$contactData = [
|
||||
'email' => $email,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
if (!$contactModel->save($contactData)) {
|
||||
log_message('error', 'Failed to save contact data: ' . implode(', ', $contactModel->errors()));
|
||||
return redirect()->back()->with('error', 'Failed to save message. Please try again.');
|
||||
}
|
||||
|
||||
// Prepare the email content
|
||||
$recipient = 'alrahma.isgl@gmail.com'; // Change to the communication head's email
|
||||
$subject = 'Contact Us Form Submission';
|
||||
$emailMessage = "You have received a new message from $email:\n\n$message";
|
||||
|
||||
// Use EmailController to send the email
|
||||
$emailController = new \App\Controllers\View\EmailController();
|
||||
if (!$emailController->sendEmail($recipient, $subject, $emailMessage)) {
|
||||
log_message('error', 'Failed to send email.');
|
||||
return redirect()->back()->with('error', 'Failed to send message. Please try again.');
|
||||
}
|
||||
|
||||
return redirect()->to('/thank_you')->with('success', 'Message sent successfully.');
|
||||
} catch (\Exception $e) {
|
||||
log_message('error', 'Exception occurred: ' . $e->getMessage());
|
||||
return redirect()->back()->with('error', 'An unexpected error occurred. Please try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class PhoneFormatterService
|
||||
{
|
||||
/**
|
||||
* Format a raw phone number to (xxx)-xxx-xxxx format
|
||||
*
|
||||
* @param string $number
|
||||
* @return string|null
|
||||
*/
|
||||
public function formatPhoneNumber(string $number): ?string
|
||||
{
|
||||
// Remove non-digit characters
|
||||
$digits = preg_replace('/\D/', '', $number);
|
||||
|
||||
// Ensure it's exactly 10 digits
|
||||
if (strlen($digits) === 10) {
|
||||
return sprintf(
|
||||
'%s-%s-%s',
|
||||
substr($digits, 0, 3),
|
||||
substr($digits, 3, 3),
|
||||
substr($digits, 6)
|
||||
);
|
||||
}
|
||||
|
||||
// Invalid number
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
|
||||
|
||||
helper('document');
|
||||
|
||||
class PolicyController extends BaseController
|
||||
{
|
||||
|
||||
|
||||
public function index()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function schoolPicturePolicy()
|
||||
{
|
||||
// FIXED path: correct spelling of "policy"
|
||||
$content = include(APPPATH . 'Views/policy/picture_policy_partial.php');
|
||||
|
||||
// Pass the content to the view
|
||||
return view('/policy/picture_policy', $content);
|
||||
}
|
||||
|
||||
public function schoolPolicy()
|
||||
{
|
||||
// FIXED path: correct spelling of "policy"
|
||||
$content = include(APPPATH . 'Views/policy/school_policy_partial.php');
|
||||
|
||||
return view('policy/school_policy', $content);
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\PreferencesModel;
|
||||
|
||||
class PreferencesController extends BaseController
|
||||
{
|
||||
protected $preferencesModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->preferencesModel = new PreferencesModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display preferences page
|
||||
* GET /preferences/{userId}
|
||||
*/
|
||||
public function index($userId = null)
|
||||
{
|
||||
// Get user ID from parameter or session
|
||||
$userId = $userId ?? (int) session()->get('user_id');
|
||||
|
||||
if (!$userId) {
|
||||
return redirect()->to('/login')->with('error', 'Please log in to view preferences');
|
||||
}
|
||||
|
||||
// Fetch preferences for the current user
|
||||
$preferences = $this->preferencesModel->where('user_id', $userId)->first();
|
||||
|
||||
// If preferences do not exist, set default preferences
|
||||
if (!$preferences) {
|
||||
$preferences = [
|
||||
'receive_email_notifications' => 1,
|
||||
'receive_sms_notifications' => 1,
|
||||
'theme' => 'light',
|
||||
'language' => 'en',
|
||||
];
|
||||
}
|
||||
|
||||
// Append style/menu selections from session or defaults
|
||||
$styleConfig = config('Style');
|
||||
$preferences['style_color'] = $preferences['style_color'] ?? (session()->get('style_color') ?? ($styleConfig->defaultStyle ?? 'blue'));
|
||||
$preferences['menu_color'] = $preferences['menu_color'] ?? (session()->get('menu_color') ?? ($styleConfig->defaultMenu ?? 'white'));
|
||||
|
||||
// Options for selectors
|
||||
$styleOptions = array_keys($styleConfig->stylePalettes ?? []);
|
||||
$menuOptions = array_keys($styleConfig->menuPalettes ?? []);
|
||||
|
||||
// Load the view with the user's preferences
|
||||
return view('/preferences', [
|
||||
'preferences' => $preferences,
|
||||
'styleOptions' => $styleOptions,
|
||||
'menuOptions' => $menuOptions,
|
||||
'userId' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update preferences
|
||||
* POST /preferences/update/{userId}
|
||||
*/
|
||||
public function updatePreferences($userId = null)
|
||||
{
|
||||
// Get user ID from parameter or session
|
||||
$userId = $userId ?? (int) session()->get('user_id');
|
||||
|
||||
if (!$userId) {
|
||||
return redirect()->to('/login')->with('error', 'Please log in to update preferences');
|
||||
}
|
||||
|
||||
// Validation rules
|
||||
$validation = \Config\Services::validation();
|
||||
|
||||
$styleConfig = config('Style');
|
||||
$styleOptions = implode(',', array_keys($styleConfig->stylePalettes ?? []));
|
||||
$menuOptions = implode(',', array_keys($styleConfig->menuPalettes ?? []));
|
||||
|
||||
$validation->setRules([
|
||||
'receive_email_notifications' => 'permit_empty|in_list[0,1]',
|
||||
'receive_sms_notifications' => 'permit_empty|in_list[0,1]',
|
||||
'theme' => 'permit_empty|in_list[light,dark]',
|
||||
'language' => 'permit_empty|in_list[en,fr,es]',
|
||||
'style_color' => $styleOptions ? 'permit_empty|in_list[' . $styleOptions . ']' : 'permit_empty',
|
||||
'menu_color' => $menuOptions ? 'permit_empty|in_list[' . $menuOptions . ']' : 'permit_empty',
|
||||
]);
|
||||
|
||||
if (!$validation->run($this->request->getPost())) {
|
||||
$preferences = $this->preferencesModel->where('user_id', $userId)->first();
|
||||
if (!$preferences) {
|
||||
$preferences = [
|
||||
'receive_email_notifications' => 1,
|
||||
'receive_sms_notifications' => 1,
|
||||
'theme' => 'light',
|
||||
'language' => 'en',
|
||||
];
|
||||
}
|
||||
$preferences['style_color'] = $this->request->getPost('style_color') ?? ($preferences['style_color'] ?? (session()->get('style_color') ?? ($styleConfig->defaultStyle ?? 'blue')));
|
||||
$preferences['menu_color'] = $this->request->getPost('menu_color') ?? ($preferences['menu_color'] ?? (session()->get('menu_color') ?? ($styleConfig->defaultMenu ?? 'white')));
|
||||
|
||||
return view('/preferences', [
|
||||
'preferences' => $preferences,
|
||||
'styleOptions' => array_keys($styleConfig->stylePalettes ?? []),
|
||||
'menuOptions' => array_keys($styleConfig->menuPalettes ?? []),
|
||||
'validation' => $validation,
|
||||
'userId' => $userId,
|
||||
]);
|
||||
}
|
||||
|
||||
// Prepare updated data (map form fields to database fields)
|
||||
$updateData = [
|
||||
'receive_email_notifications' => $this->request->getPost('receive_email_notifications') ?? $this->request->getPost('notification_email') ?? 1,
|
||||
'receive_sms_notifications' => $this->request->getPost('receive_sms_notifications') ?? $this->request->getPost('notification_sms') ?? 1,
|
||||
'theme' => $this->request->getPost('theme') ?? 'light',
|
||||
'language' => $this->request->getPost('language') ?? 'en',
|
||||
];
|
||||
|
||||
// Store style/menu selections in session (no DB column dependency)
|
||||
$styleColor = $this->request->getPost('style_color');
|
||||
$menuColor = $this->request->getPost('menu_color');
|
||||
if ($styleColor && isset(($styleConfig->stylePalettes ?? [])[$styleColor])) {
|
||||
session()->set('style_color', $styleColor);
|
||||
$updateData['style_color'] = $styleColor;
|
||||
}
|
||||
if ($menuColor && isset(($styleConfig->menuPalettes ?? [])[$menuColor])) {
|
||||
session()->set('menu_color', $menuColor);
|
||||
$updateData['menu_color'] = $menuColor;
|
||||
}
|
||||
|
||||
// Check if preferences already exist, update if they do, otherwise insert new preferences
|
||||
$existing = $this->preferencesModel->where('user_id', $userId)->first();
|
||||
if ($existing) {
|
||||
$this->preferencesModel->update($existing['id'], $updateData);
|
||||
} else {
|
||||
$updateData['user_id'] = $userId;
|
||||
$this->preferencesModel->insert($updateData);
|
||||
}
|
||||
|
||||
// Redirect back to preferences page with success message
|
||||
return redirect()->to('/preferences/' . $userId)->with('success', 'Preferences updated successfully');
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\AttendanceRecordModel;
|
||||
use App\Models\BadgePrintLogModel;
|
||||
use App\Models\ClassSectionModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\ScoreCommentModel;
|
||||
use App\Models\SemesterScoreModel;
|
||||
use App\Models\StaffModel;
|
||||
use App\Models\StudentClassModel;
|
||||
use App\Models\StudentModel;
|
||||
use App\Models\TeacherClassModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
require_once APPPATH . 'ThirdParty/fpdf/fpdf.php';
|
||||
|
||||
class PrintablesBaseController extends BaseController
|
||||
{
|
||||
protected $userModel;
|
||||
protected $studentModel;
|
||||
protected $db;
|
||||
protected $classSectionModel;
|
||||
protected $configModel;
|
||||
protected $schoolYear;
|
||||
protected $semester;
|
||||
protected $staffModel;
|
||||
protected $teacherClassModel;
|
||||
protected $semesterScoreModel;
|
||||
protected $studentClassModel;
|
||||
protected $stickerWidth;
|
||||
protected $stickerHeight;
|
||||
protected $pageW;
|
||||
protected $pageH;
|
||||
protected $marginL;
|
||||
protected $marginT;
|
||||
protected $gapX;
|
||||
protected $gapY;
|
||||
protected $badgePrintLogModel;
|
||||
protected $scoreCommentModel;
|
||||
protected $attendanceRecordModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->userModel = new UserModel();
|
||||
$this->studentModel = new StudentModel();
|
||||
$this->classSectionModel = new ClassSectionModel();
|
||||
$this->configModel = new ConfigurationModel();
|
||||
$this->semesterScoreModel = new SemesterScoreModel();
|
||||
$this->studentClassModel = new StudentClassModel();
|
||||
$this->teacherClassModel = new TeacherClassModel();
|
||||
$this->staffModel = new StaffModel();
|
||||
$this->badgePrintLogModel = new BadgePrintLogModel();
|
||||
$this->scoreCommentModel = new ScoreCommentModel();
|
||||
$this->attendanceRecordModel = new AttendanceRecordModel();
|
||||
|
||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||
$this->semester = $this->configModel->getConfig('semester');
|
||||
$this->stickerWidth = $this->configModel->getConfig('stickerWidth');
|
||||
$this->stickerHeight = $this->configModel->getConfig('stickerHeight');
|
||||
$this->pageW = $this->configModel->getConfig('pageWidth');
|
||||
$this->pageH = $this->configModel->getConfig('pageHeight');
|
||||
$this->marginL = $this->configModel->getConfig('leftMargin');
|
||||
$this->marginT = $this->configModel->getConfig('topMargin');
|
||||
$this->gapX = $this->configModel->getConfig('horizontalGap');
|
||||
$this->gapY = $this->configModel->getConfig('verticalGap');
|
||||
}
|
||||
|
||||
protected function firstExisting(array $paths)
|
||||
{
|
||||
foreach ($paths as $p) {
|
||||
if (is_string($p) && file_exists($p)) {
|
||||
return $p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function resolveClassName($db, $classId)
|
||||
{
|
||||
$tables = [
|
||||
['classSection', 'class_section_id'],
|
||||
['classSection', 'id'],
|
||||
['class_sections', 'class_section_id'],
|
||||
['class_sections', 'id'],
|
||||
];
|
||||
|
||||
foreach ($tables as [$table, $pk]) {
|
||||
$result = $db->table($table)
|
||||
->select('class_section_name')
|
||||
->where($pk, $classId)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if ($result) {
|
||||
return $result['class_section_name'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function fetchStudentsByClass(int $sectionId, ?string $schoolYear)
|
||||
{
|
||||
$b = $this->studentClassModel
|
||||
->select('s.id, s.firstname, s.lastname, s.registration_grade, s.gender')
|
||||
->join('students s', 's.id = student_class.student_id', 'inner')
|
||||
->where('student_class.class_section_id', $sectionId)
|
||||
->orderBy('s.lastname', 'ASC');
|
||||
|
||||
if (!empty($schoolYear)) {
|
||||
$b->where('student_class.school_year', $schoolYear);
|
||||
}
|
||||
|
||||
return $b->findAll();
|
||||
}
|
||||
}
|
||||
@@ -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,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\StudentModel;
|
||||
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
class RFIDController extends BaseController
|
||||
{
|
||||
public function process()
|
||||
{
|
||||
$rfidTag = $this->request->getPost('rfid');
|
||||
|
||||
// Load the user model
|
||||
$userModel = new UserModel();
|
||||
|
||||
// Find the user by RFID tag
|
||||
$user = $userModel->where('rfid_tag', $rfidTag)->first();
|
||||
|
||||
if ($user) {
|
||||
// Log the scan (optional: store in a 'scan_log' table)
|
||||
$db = \Config\Database::connect();
|
||||
$db->table('scan_log')->insert([
|
||||
'user_id' => $user->id,
|
||||
'card_id' => $rfidTag,
|
||||
'scan_time' => Time::now(),
|
||||
]);
|
||||
|
||||
// Redirect with success message
|
||||
return redirect()->to('/rfid/log')->with('message', 'RFID recognized: ' . $user->name);
|
||||
} else {
|
||||
// Redirect with error message if RFID not found
|
||||
return redirect()->to('/')->with('error', 'RFID tag not recognized.');
|
||||
}
|
||||
}
|
||||
|
||||
public function log()
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
// Retrieve scan logs and join with users table to display user names
|
||||
$query = $db->table('scan_log')
|
||||
->select('users.firstname as user_firstname, users.lastname as user_lastname, students.firstname as student_firstname, students.lastname as student_lastname, scan_log.card_id, scan_log.scan_time')
|
||||
->join('users', 'users.id = scan_log.user_id', 'left')
|
||||
->join('students', 'students.rfid_tag = scan_log.card_id', 'left') // Join students table by RFID tag
|
||||
->orderBy('scan_time', 'DESC')
|
||||
->get();
|
||||
|
||||
// Fetch user and student data for display purposes
|
||||
$userModel = new UserModel();
|
||||
$studentModel = new StudentModel();
|
||||
|
||||
// Optional: Fetch all users and students (if needed elsewhere in the view)
|
||||
$users = $userModel->findAll();
|
||||
$students = $studentModel->findAll();
|
||||
|
||||
// Pass scan logs, users, and students data to the view
|
||||
$data['logs'] = $query->getResult(); // Logs with user and student info
|
||||
$data['users'] = $users; // All users data
|
||||
$data['students'] = $students; // All students data
|
||||
|
||||
return view('/rfid/rfid_coming_soon', $data);
|
||||
}
|
||||
|
||||
public function rfidComingSoon()
|
||||
{
|
||||
return view('rfid/rfid_coming_soon');
|
||||
}
|
||||
}
|
||||
@@ -1,785 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\RefundModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\PaymentModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\InvoiceModel;
|
||||
use App\Models\EnrollmentModel;
|
||||
|
||||
class RefundController extends BaseController
|
||||
{
|
||||
protected RefundModel $refundModel;
|
||||
protected UserModel $userModel;
|
||||
protected PaymentModel $paymentModel;
|
||||
protected ConfigurationModel $configModel;
|
||||
protected InvoiceModel $invoiceModel;
|
||||
protected EnrollmentModel $enrollmentModel;
|
||||
protected $db;
|
||||
|
||||
// Allowed request types (mapped to your `refunds.request` column)
|
||||
private const REQUEST_TYPES = ['tuition','overpayment','duplicate','extra'];
|
||||
// Allowed finalization decisions
|
||||
private const DECISIONS = ['Approved','Rejected'];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->refundModel = new RefundModel();
|
||||
$this->userModel = new UserModel();
|
||||
$this->paymentModel = new PaymentModel();
|
||||
$this->configModel = new ConfigurationModel();
|
||||
$this->invoiceModel = new InvoiceModel();
|
||||
$this->enrollmentModel = new EnrollmentModel();
|
||||
$this->db = \Config\Database::connect();
|
||||
}
|
||||
|
||||
/** Get current term (school_year, semester) from configuration */
|
||||
private function getCurrentTerm(): array
|
||||
{
|
||||
$rows = $this->configModel
|
||||
->select('config_key, config_value')
|
||||
->whereIn('config_key', ['school_year','semester'])
|
||||
->findAll();
|
||||
|
||||
$map = [];
|
||||
foreach ($rows as $r) {
|
||||
$map[$r['config_key']] = $r['config_value'];
|
||||
}
|
||||
return [
|
||||
'school_year' => $map['school_year'] ?? date('Y') . '-' . (date('Y') + 1),
|
||||
'semester' => $map['semester'] ?? 'Fall',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect overpayments per parent for current school_year and create/update Pending refunds.
|
||||
* When $triggerEvents is true, emits refundPending for newly created rows only.
|
||||
*
|
||||
* @return array [created_ids => int[], updated_ids => int[]]
|
||||
*/
|
||||
private function recalcOverpayments(bool $triggerEvents = false): array
|
||||
{
|
||||
$created = [];
|
||||
$updated = [];
|
||||
$term = $this->getCurrentTerm();
|
||||
$year = (string)$term['school_year'];
|
||||
$sem = (string)$term['semester'];
|
||||
|
||||
// Sum payments by parent for the term
|
||||
$pm = $this->paymentModel;
|
||||
$payTable = $pm->table;
|
||||
$hasStatus = $this->paymentModel->db->fieldExists('status', $payTable);
|
||||
$hasVoid = $this->paymentModel->db->fieldExists('is_void', $payTable);
|
||||
|
||||
$qbPaid = $pm->select('parent_id, COALESCE(SUM(paid_amount),0) AS total_paid')
|
||||
->where('school_year', $year)
|
||||
->groupBy('parent_id');
|
||||
if ($hasStatus) {
|
||||
$qbPaid->groupStart()
|
||||
->whereNotIn('status', ['void','voided','refunded','failed','chargeback','declined','reversed','canceled','cancelled'])
|
||||
->orWhere('status IS NULL', null, false)
|
||||
->groupEnd();
|
||||
}
|
||||
if ($hasVoid) {
|
||||
$qbPaid->groupStart()
|
||||
->where('is_void', 0)
|
||||
->orWhere('is_void IS NULL', null, false)
|
||||
->groupEnd();
|
||||
}
|
||||
$paidRows = $qbPaid->findAll();
|
||||
|
||||
// Sum invoices (charges) by parent for the term
|
||||
$invRows = $this->invoiceModel
|
||||
->select('parent_id, COALESCE(SUM(total_amount),0) AS total_invoiced')
|
||||
->where('school_year', $year)
|
||||
->groupBy('parent_id')
|
||||
->findAll();
|
||||
|
||||
// Sum refunds PAID (cash out) by parent for the term
|
||||
$refRows = $this->refundModel
|
||||
->select('parent_id, COALESCE(SUM(refund_paid_amount),0) AS total_refunded')
|
||||
->where('school_year', $year)
|
||||
->whereIn('status', ['Partial','Paid'])
|
||||
->groupBy('parent_id')
|
||||
->findAll();
|
||||
|
||||
$paidMap = [];
|
||||
foreach ($paidRows as $r) $paidMap[(int)$r['parent_id']] = (float)($r['total_paid'] ?? 0);
|
||||
$invMap = [];
|
||||
foreach ($invRows as $r) $invMap[(int)$r['parent_id']] = (float)($r['total_invoiced'] ?? 0);
|
||||
$refMap = [];
|
||||
foreach ($refRows as $r) $refMap[(int)$r['parent_id']] = (float)($r['total_refunded'] ?? 0);
|
||||
|
||||
$parentIds = array_unique(array_merge(array_keys($paidMap), array_keys($invMap)));
|
||||
|
||||
foreach ($parentIds as $pid) {
|
||||
$paid = (float)($paidMap[$pid] ?? 0);
|
||||
$invd = (float)($invMap[$pid] ?? 0);
|
||||
$rfnd = (float)($refMap[$pid] ?? 0);
|
||||
$over = round($paid - $invd - $rfnd, 2);
|
||||
if ($over > 0.00 + 0.0001) {
|
||||
// Parent-level policy: DO NOT create new overpayment rows; only keep existing parent-level
|
||||
// overpayment rows in sync if they are still open (Pending/Partial). Prefer per-invoice rows.
|
||||
$existing = $this->refundModel
|
||||
->where('parent_id', $pid)
|
||||
->where('school_year', $year)
|
||||
->where('request', 'overpayment')
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
|
||||
if ($existing && in_array($existing['status'], ['Pending','Partial'], true)) {
|
||||
$this->refundModel->update((int)$existing['id'], [
|
||||
'refund_amount' => $over,
|
||||
'updated_at' => utc_now(),
|
||||
'updated_by' => session()->get('user_id'),
|
||||
]);
|
||||
$updated[] = (int)$existing['id'];
|
||||
}
|
||||
// else: no parent-level row created
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Stage 2: Per-invoice overpayment detection (current school year) =====
|
||||
try {
|
||||
$invRows = $this->invoiceModel
|
||||
->select('id, parent_id, total_amount, school_year')
|
||||
->where('school_year', $year)
|
||||
->findAll();
|
||||
if ($invRows) {
|
||||
$invoiceIds = array_map(static fn($r) => (int)$r['id'], $invRows);
|
||||
|
||||
// payments by invoice
|
||||
$qbInvPaid = $this->paymentModel
|
||||
->select('invoice_id, COALESCE(SUM(paid_amount),0) AS total_paid')
|
||||
->whereIn('invoice_id', $invoiceIds)
|
||||
->groupBy('invoice_id');
|
||||
$payTable2 = $this->paymentModel->table;
|
||||
$hasStatus2 = $this->paymentModel->db->fieldExists('status', $payTable2);
|
||||
$hasVoid2 = $this->paymentModel->db->fieldExists('is_void', $payTable2);
|
||||
if ($hasStatus2) {
|
||||
$qbInvPaid->groupStart()
|
||||
->whereNotIn('status', ['void','voided','refunded','failed','chargeback','declined','reversed','canceled','cancelled'])
|
||||
->orWhere('status IS NULL', null, false)
|
||||
->groupEnd();
|
||||
}
|
||||
if ($hasVoid2) {
|
||||
$qbInvPaid->groupStart()
|
||||
->where('is_void', 0)
|
||||
->orWhere('is_void IS NULL', null, false)
|
||||
->groupEnd();
|
||||
}
|
||||
$invPaidRows = $qbInvPaid->findAll();
|
||||
|
||||
// discounts by invoice
|
||||
$invDiscRows = $this->db->table('discount_usages')
|
||||
->select('invoice_id, COALESCE(SUM(discount_amount),0) AS total_disc')
|
||||
->whereIn('invoice_id', $invoiceIds)
|
||||
->groupBy('invoice_id')
|
||||
->get()->getResultArray();
|
||||
|
||||
// refunds paid by invoice
|
||||
$invRefRows = $this->refundModel
|
||||
->select('invoice_id, COALESCE(SUM(refund_paid_amount),0) AS total_ref')
|
||||
->whereIn('invoice_id', $invoiceIds)
|
||||
->whereIn('status', ['Partial','Paid'])
|
||||
->groupBy('invoice_id')
|
||||
->findAll();
|
||||
|
||||
$paidByInv = [];
|
||||
foreach ($invPaidRows as $r) { $paidByInv[(int)$r['invoice_id']] = (float)($r['total_paid'] ?? 0); }
|
||||
$discByInv = [];
|
||||
foreach ($invDiscRows as $r) { $discByInv[(int)$r['invoice_id']] = (float)($r['total_disc'] ?? 0); }
|
||||
$refByInv = [];
|
||||
foreach ($invRefRows as $r) { $refByInv[(int)$r['invoice_id']] = (float)($r['total_ref'] ?? 0); }
|
||||
|
||||
foreach ($invRows as $inv) {
|
||||
$iid = (int)$inv['id'];
|
||||
$pid = (int)$inv['parent_id'];
|
||||
$total = (float)($inv['total_amount'] ?? 0);
|
||||
$paid = (float)($paidByInv[$iid] ?? 0);
|
||||
$disc = (float)($discByInv[$iid] ?? 0);
|
||||
$rfnd = (float)($refByInv[$iid] ?? 0);
|
||||
|
||||
$rawBalance = round($total - $disc - $paid - $rfnd, 2);
|
||||
if ($rawBalance < -0.0001) {
|
||||
$over = abs($rawBalance);
|
||||
|
||||
// If ANY open refund exists for this invoice, update it instead of creating another line
|
||||
$openRow = $this->refundModel
|
||||
->where('invoice_id', $iid)
|
||||
->whereIn('status', ['Pending','Approved','Partial'])
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
if ($openRow) {
|
||||
// Prefer merging into 'overpayment' if present, else update the open row
|
||||
$this->refundModel->update((int)$openRow['id'], [
|
||||
'refund_amount' => $over,
|
||||
'updated_at' => utc_now(),
|
||||
'updated_by' => session()->get('user_id'),
|
||||
]);
|
||||
$updated[] = (int)$openRow['id'];
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there is an open PARENT-LEVEL overpayment (invoice_id IS NULL), migrate it to this invoice
|
||||
$parentLevelOpen = $this->refundModel
|
||||
->where('parent_id', $pid)
|
||||
->where('school_year', $year)
|
||||
->where('invoice_id', null)
|
||||
->where('request', 'overpayment')
|
||||
->whereIn('status', ['Pending','Approved','Partial'])
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
if ($parentLevelOpen) {
|
||||
$this->refundModel->update((int)$parentLevelOpen['id'], [
|
||||
'invoice_id' => $iid,
|
||||
'refund_amount' => $over,
|
||||
'updated_at' => utc_now(),
|
||||
'updated_by' => session()->get('user_id'),
|
||||
]);
|
||||
$updated[] = (int)$parentLevelOpen['id'];
|
||||
continue; // merged instead of creating new
|
||||
}
|
||||
|
||||
// Existing overpayment refund for this invoice?
|
||||
$existing = $this->refundModel
|
||||
->where('invoice_id', $iid)
|
||||
->where('request', 'overpayment')
|
||||
->whereIn('status', ['Pending','Approved','Partial','Paid'])
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
|
||||
if (!$existing || $existing['status'] === 'Paid') {
|
||||
$ok = $this->refundModel->insert([
|
||||
'parent_id' => $pid,
|
||||
'school_year' => $year,
|
||||
'semester' => $sem,
|
||||
'invoice_id' => $iid,
|
||||
'refund_amount' => $over,
|
||||
'refund_paid_amount' => 0.00,
|
||||
'status' => 'Pending',
|
||||
'request' => 'overpayment',
|
||||
'reason' => 'Auto-detected per-invoice overpayment',
|
||||
'updated_by' => session()->get('user_id'),
|
||||
'created_at' => utc_now(),
|
||||
'updated_at' => utc_now(),
|
||||
]);
|
||||
if ($ok) {
|
||||
$created[] = (int)$this->refundModel->getInsertID();
|
||||
if ($triggerEvents) {
|
||||
$user = (new UserModel())->select('id, email, firstname, lastname')->find($pid) ?: [];
|
||||
$eventData = [
|
||||
'user_id' => (int)($user['id'] ?? $pid),
|
||||
'email' => $user['email'] ?? null,
|
||||
'firstname' => $user['firstname'] ?? null,
|
||||
'lastname' => $user['lastname'] ?? null,
|
||||
'amount' => $over,
|
||||
'portalLink'=> base_url('/login'),
|
||||
];
|
||||
\CodeIgniter\Events\Events::trigger('refundPending', $eventData, []);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Keep Pending/Partial in sync with current overpayment
|
||||
if (in_array($existing['status'], ['Pending','Partial'], true)) {
|
||||
$this->refundModel->update((int)$existing['id'], [
|
||||
'refund_amount' => $over,
|
||||
'updated_at' => utc_now(),
|
||||
'updated_by' => session()->get('user_id'),
|
||||
]);
|
||||
$updated[] = (int)$existing['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'recalcOverpayments per-invoice failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return ['created_ids' => $created, 'updated_ids' => $updated];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent financial summary for *current term* (adjust if you want all-time).
|
||||
* Assumes invoices.total_amount and payments.amount (positive=in, negative=out).
|
||||
*/
|
||||
private function getParentFinancialSummary(int $parentId, string $schoolYear, string $semester): array
|
||||
{
|
||||
// invoices total for term
|
||||
$inv = $this->invoiceModel
|
||||
->select('COALESCE(SUM(total_amount),0) AS total_invoiced')
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->first();
|
||||
|
||||
// payments in (amount > 0) for term
|
||||
$payIn = $this->paymentModel
|
||||
->select('COALESCE(SUM(amount),0) AS paid_in')
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->where('amount >', 0)
|
||||
->first();
|
||||
|
||||
// refunds already paid out (your refunds table)
|
||||
$refPaid = $this->refundModel
|
||||
->select('COALESCE(SUM(refund_paid_amount),0) AS refunded_cash')
|
||||
->where('parent_id', $parentId)
|
||||
->where('school_year', $schoolYear)
|
||||
->where('semester', $semester)
|
||||
->whereIn('status', ['Partial','Paid'])
|
||||
->first();
|
||||
|
||||
$totalInvoiced = (float)($inv['total_invoiced'] ?? 0);
|
||||
$paidIn = (float)($payIn['paid_in'] ?? 0);
|
||||
$refundedCash = (float)($refPaid['refunded_cash'] ?? 0);
|
||||
|
||||
// Unapplied balance = money in − invoices − cash refunds out
|
||||
$unapplied = $paidIn - $totalInvoiced - $refundedCash;
|
||||
|
||||
return [
|
||||
'total_invoiced' => $totalInvoiced,
|
||||
'total_paid_in' => $paidIn,
|
||||
'total_refunded' => $refundedCash,
|
||||
'unapplied_balance' => $unapplied,
|
||||
];
|
||||
}
|
||||
|
||||
/** Optional helper if you want a quick API for balances in UI */
|
||||
public function parentBalances(int $parentId)
|
||||
{
|
||||
$t = $this->getCurrentTerm();
|
||||
return $this->response->setJSON($this->getParentFinancialSummary($parentId, $t['school_year'], $t['semester']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create refund request.
|
||||
* Stores source/type in `refunds.request` as: tuition|overpayment|duplicate|extra
|
||||
*
|
||||
* @param int $parentId
|
||||
* @param float $amount
|
||||
* @param string|null $requestType tuition|overpayment|duplicate|extra
|
||||
* @param int|null $invoiceId required for tuition; useful for duplicate
|
||||
* @param int|null $paymentId useful for duplicate
|
||||
* @param string|null $reason
|
||||
*/
|
||||
public function requestRefund(
|
||||
int $parentId,
|
||||
float $amount,
|
||||
?string $requestType = 'tuition',
|
||||
?int $invoiceId = null,
|
||||
?int $paymentId = null,
|
||||
?string $reason = null
|
||||
) {
|
||||
$requestType = strtolower((string)$requestType);
|
||||
if (!in_array($requestType, self::REQUEST_TYPES, true)) {
|
||||
return $this->response->setJSON(['error' => 'Invalid refund request type.']);
|
||||
}
|
||||
|
||||
$parent = $this->userModel->find($parentId);
|
||||
if (!$parent) {
|
||||
return $this->response->setJSON(['error' => 'Parent not found.']);
|
||||
}
|
||||
if ($amount <= 0) {
|
||||
return $this->response->setJSON(['error' => 'Refund amount must be > 0.']);
|
||||
}
|
||||
|
||||
$term = $this->getCurrentTerm();
|
||||
$schoolYear = $term['school_year'];
|
||||
$semester = $term['semester'];
|
||||
|
||||
// Tuition requires an invoice check
|
||||
if ($requestType === 'tuition') {
|
||||
if (empty($invoiceId)) {
|
||||
return $this->response->setJSON(['error' => 'invoice_id is required for tuition refunds.']);
|
||||
}
|
||||
$inv = $this->invoiceModel->find($invoiceId);
|
||||
if (!$inv || (int)$inv['parent_id'] !== $parentId) {
|
||||
return $this->response->setJSON(['error' => 'Invoice not found for parent.']);
|
||||
}
|
||||
// Optionally cap to eligible amount per your policy.
|
||||
}
|
||||
|
||||
// Overpayment/extra must not exceed unapplied balance
|
||||
if (in_array($requestType, ['overpayment','extra'], true)) {
|
||||
$sum = $this->getParentFinancialSummary($parentId, $schoolYear, $semester);
|
||||
if ($sum['unapplied_balance'] <= 0) {
|
||||
return $this->response->setJSON(['error' => 'No unapplied balance available.']);
|
||||
}
|
||||
if ($amount > $sum['unapplied_balance'] + 0.0001) {
|
||||
return $this->response->setJSON([
|
||||
'error' => 'Amount exceeds available unapplied balance.',
|
||||
'available' => number_format($sum['unapplied_balance'], 2)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Duplicate: optionally tie to payment; otherwise your staff can fill details in reason/note
|
||||
if ($requestType === 'duplicate' && empty($paymentId) && empty($invoiceId)) {
|
||||
// Not hard-failing; but better to guide:
|
||||
// return $this->response->setJSON(['error' => 'Provide payment_id or invoice_id for duplicate refunds.']);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'parent_id' => $parentId,
|
||||
'school_year' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
'invoice_id' => $invoiceId,
|
||||
'refund_amount' => $amount,
|
||||
'refund_paid_amount' => 0.00, // IMPORTANT: your column is NOT NULL
|
||||
'status' => 'Pending',
|
||||
'reason' => $reason,
|
||||
'request' => $requestType, // <- store the source/type here
|
||||
'note' => $paymentId ? ('duplicate-of-payment#' . $paymentId) : null,
|
||||
'created_at' => utc_now(),
|
||||
'updated_at' => utc_now(),
|
||||
];
|
||||
$ok = $this->refundModel->insert($payload);
|
||||
|
||||
if (!$ok) {
|
||||
return $this->response->setJSON(['error' => 'Failed to create refund request.']);
|
||||
}
|
||||
|
||||
// Fire refundPending notification/event for newly created refunds
|
||||
try {
|
||||
$user = $this->userModel->select('id, email, firstname, lastname')->find($parentId) ?: [];
|
||||
$eventData = [
|
||||
'user_id' => (int)($user['id'] ?? $parentId),
|
||||
'email' => $user['email'] ?? null,
|
||||
'firstname' => $user['firstname'] ?? null,
|
||||
'lastname' => $user['lastname'] ?? null,
|
||||
'amount' => (float)$amount,
|
||||
'portalLink'=> base_url('/login'),
|
||||
];
|
||||
\CodeIgniter\Events\Events::trigger('refundPending', $eventData, []);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'requestRefund: failed to trigger refundPending: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $this->response->setJSON(['success' => 'Refund requested']);
|
||||
}
|
||||
|
||||
// Approve refund (no money movement)
|
||||
public function approveRefund(int $refundId)
|
||||
{
|
||||
$ok = $this->refundModel->update($refundId, [
|
||||
'status' => 'Approved',
|
||||
'approved_at' => utc_now(),
|
||||
'approved_by' => session()->get('user_id'),
|
||||
'updated_at' => utc_now(),
|
||||
'updated_by' => session()->get('user_id'),
|
||||
]);
|
||||
|
||||
return $this->response->setJSON($ok ? ['success' => 'Refund approved'] : ['error' => 'Approve failed']);
|
||||
}
|
||||
|
||||
// Reject refund
|
||||
public function rejectRefund(int $refundId)
|
||||
{
|
||||
$ok = $this->refundModel->update($refundId, [
|
||||
'status' => 'Rejected',
|
||||
'approved_at' => utc_now(),
|
||||
'approved_by' => session()->get('user_id'),
|
||||
'updated_at' => utc_now(),
|
||||
'updated_by' => session()->get('user_id'),
|
||||
]);
|
||||
|
||||
return $this->response->setJSON($ok ? ['success' => 'Refund rejected'] : ['error' => 'Reject failed']);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: refund_id, paid_amount, payment_method (Check|Online|Cash), check_number, check_file (optional)
|
||||
* Marks payout (can be partial) and inserts a negative Payment row to keep balances correct.
|
||||
*/
|
||||
public function updatePayment()
|
||||
{
|
||||
log_message('debug', 'POST data: ' . print_r($_POST, true));
|
||||
|
||||
$refundId = (int)$this->request->getPost('refund_id');
|
||||
$newPaidAmount = (float)$this->request->getPost('paid_amount');
|
||||
$refundMethod = $this->request->getPost('payment_method'); // Check|Online|Cash
|
||||
$checkNbr = $this->request->getPost('check_number');
|
||||
|
||||
if ($refundId <= 0) return $this->response->setJSON(['error' => 'Missing refund ID.']);
|
||||
if ($newPaidAmount <= 0) return $this->response->setJSON(['error' => 'Valid paid amount is required.']);
|
||||
|
||||
$refund = $this->refundModel->find($refundId);
|
||||
if (!$refund) return $this->response->setJSON(['error' => 'Refund not found.']);
|
||||
|
||||
if (!in_array($refund['status'], ['Approved','Partial'], true)) {
|
||||
return $this->response->setJSON(['error' => 'Refund must be Approved/Partial to pay.']);
|
||||
}
|
||||
|
||||
$oldPaid = (float)$refund['refund_paid_amount'];
|
||||
$target = (float)$refund['refund_amount'];
|
||||
$total = $oldPaid + $newPaidAmount;
|
||||
|
||||
if ($total > $target + 0.0001) {
|
||||
return $this->response->setJSON(['error' => 'Total paid cannot exceed refund amount.']);
|
||||
}
|
||||
|
||||
// Optional file upload for Check
|
||||
$checkFileName = $refund['check_file'] ?? null;
|
||||
if ($refundMethod === 'Check') {
|
||||
$checkFile = $this->request->getFile('check_file');
|
||||
if ($checkFile && $checkFile->isValid() && !$checkFile->hasMoved()) {
|
||||
$checkFileName = $checkFile->getRandomName();
|
||||
$checkFile->move(WRITEPATH . 'uploads/checks/', $checkFileName);
|
||||
}
|
||||
}
|
||||
|
||||
$newStatus = ($total < $target) ? 'Partial' : 'Paid';
|
||||
|
||||
$db = db_connect();
|
||||
$db->transStart();
|
||||
|
||||
// 0) If this refund is not tied to an invoice yet, assign it to the most overpaid invoice
|
||||
$assignInvoiceId = null;
|
||||
if (empty($refund['invoice_id'])) {
|
||||
try {
|
||||
// Gather invoices for this parent/year
|
||||
$invRows = $this->invoiceModel
|
||||
->select('id, total_amount')
|
||||
->where('parent_id', (int)$refund['parent_id'])
|
||||
->where('school_year', $refund['school_year'])
|
||||
->findAll();
|
||||
|
||||
if ($invRows) {
|
||||
$invoiceIds = array_map(static fn($r) => (int)$r['id'], $invRows);
|
||||
|
||||
// Payments per invoice
|
||||
$payRows = $this->paymentModel
|
||||
->select('invoice_id, COALESCE(SUM(paid_amount),0) AS total_paid')
|
||||
->whereIn('invoice_id', $invoiceIds)
|
||||
->groupBy('invoice_id')
|
||||
->findAll();
|
||||
$paidBy = [];
|
||||
foreach ($payRows as $r) { $paidBy[(int)$r['invoice_id']] = (float)($r['total_paid'] ?? 0); }
|
||||
|
||||
// Discounts per invoice
|
||||
$discRows = $this->db->table('discount_usages')
|
||||
->select('invoice_id, COALESCE(SUM(discount_amount),0) AS total_disc')
|
||||
->whereIn('invoice_id', $invoiceIds)
|
||||
->groupBy('invoice_id')
|
||||
->get()->getResultArray();
|
||||
$discBy = [];
|
||||
foreach ($discRows as $r) { $discBy[(int)$r['invoice_id']] = (float)($r['total_disc'] ?? 0); }
|
||||
|
||||
// Choose invoice with most negative raw (total - disc - paid)
|
||||
$minRaw = 0.0; $minId = null;
|
||||
foreach ($invRows as $ir) {
|
||||
$iid = (int)$ir['id'];
|
||||
$raw = (float)($ir['total_amount'] ?? 0) - (float)($discBy[$iid] ?? 0) - (float)($paidBy[$iid] ?? 0);
|
||||
if ($raw < $minRaw) { $minRaw = $raw; $minId = $iid; }
|
||||
}
|
||||
if ($minId !== null) {
|
||||
$assignInvoiceId = $minId;
|
||||
} else {
|
||||
// fallback: latest invoice in this year
|
||||
$row = $this->invoiceModel
|
||||
->select('id')
|
||||
->where('parent_id', (int)$refund['parent_id'])
|
||||
->where('school_year', $refund['school_year'])
|
||||
->orderBy('created_at', 'DESC')
|
||||
->first();
|
||||
if ($row && !empty($row['id'])) $assignInvoiceId = (int)$row['id'];
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', 'updatePayment: invoice assignment failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 1) Update refund row
|
||||
$this->refundModel->update($refundId, [
|
||||
'refund_paid_amount' => $total,
|
||||
'status' => $newStatus,
|
||||
'refunded_at' => utc_now(),
|
||||
'updated_at' => utc_now(),
|
||||
'updated_by' => session()->get('user_id'),
|
||||
'refund_method' => $refundMethod,
|
||||
'check_nbr' => $checkNbr,
|
||||
'check_file' => $checkFileName,
|
||||
// tie to invoice if determined
|
||||
'invoice_id' => $assignInvoiceId ?? $refund['invoice_id'],
|
||||
]);
|
||||
|
||||
// 2) Optional: If you want an accounting journal entry for payouts, write to a dedicated table.
|
||||
// We no longer insert a negative row into payments to avoid schema/validation conflicts.
|
||||
|
||||
$db->transComplete();
|
||||
|
||||
if ($db->transStatus() === false) {
|
||||
return $this->response->setJSON(['error' => 'Failed to update payment.']);
|
||||
}
|
||||
|
||||
return $this->response->setJSON(['success' => 'Payment updated successfully.']);
|
||||
}
|
||||
|
||||
/** Keep your listing; added extra fields for clarity */
|
||||
public function listRefunds()
|
||||
{
|
||||
// NOTE: We no longer auto-create/adjust refunds on page load to avoid duplicate lines
|
||||
// when staff are recording payouts. Use the "Recalculate" buttons to run detection on demand.
|
||||
|
||||
// 2) List refunds with joins
|
||||
$refunds = $this->refundModel
|
||||
->select('refunds.*,
|
||||
u.firstname, u.lastname, u.school_id,
|
||||
a.firstname AS approved_by_firstname,
|
||||
a.lastname AS approved_by_lastname')
|
||||
->join('users u', 'refunds.parent_id = u.id')
|
||||
->join('users a', 'refunds.approved_by = a.id', 'left')
|
||||
->orderBy('refunds.created_at', 'DESC')
|
||||
->findAll();
|
||||
|
||||
foreach ($refunds as &$r) {
|
||||
$r['approved_by_name'] = trim(($r['approved_by_firstname'] ?? '') . ' ' . ($r['approved_by_lastname'] ?? '')) ?: '-';
|
||||
}
|
||||
|
||||
$parentId = !empty($refunds) ? $refunds[0]['parent_id'] : null;
|
||||
|
||||
return view('refunds/list', [
|
||||
'refunds' => $refunds,
|
||||
'parentId' => $parentId
|
||||
]);
|
||||
}
|
||||
|
||||
/** Manual endpoint to recalculate/sync overpayments and optionally notify newly created entries. */
|
||||
public function recalculateOverpayments()
|
||||
{
|
||||
// Optional targeted invoice_number to handle cross-year cases
|
||||
$invoiceNumber = (string)($this->request->getPost('invoice_number') ?? '');
|
||||
if ($invoiceNumber !== '') {
|
||||
try {
|
||||
$inv = $this->invoiceModel->where('invoice_number', $invoiceNumber)->first();
|
||||
if ($inv) {
|
||||
$pid = (int)$inv['parent_id'];
|
||||
$iid = (int)$inv['id'];
|
||||
$year = (string)$inv['school_year'];
|
||||
|
||||
// Compute per-invoice overpayment now (independent of current term)
|
||||
$paid = (float)($this->paymentModel
|
||||
->select('COALESCE(SUM(paid_amount),0) AS tot')
|
||||
->where('invoice_id', $iid)
|
||||
->first()['tot'] ?? 0);
|
||||
$disc = (float)($this->db->table('discount_usages')
|
||||
->select('COALESCE(SUM(discount_amount),0) AS tot')
|
||||
->where('invoice_id', $iid)
|
||||
->get()->getRowArray()['tot'] ?? 0);
|
||||
$rfnd = (float)($this->refundModel
|
||||
->select('COALESCE(SUM(refund_paid_amount),0) AS tot')
|
||||
->where('invoice_id', $iid)
|
||||
->whereIn('status', ['Partial','Paid'])
|
||||
->first()['tot'] ?? 0);
|
||||
$total = (float)($inv['total_amount'] ?? 0);
|
||||
$raw = round($total - $disc - $paid - $rfnd, 2);
|
||||
if ($raw < -0.0001) {
|
||||
$over = abs($raw);
|
||||
// If there is ANY open refund for this invoice (any request), update it rather than create
|
||||
$openRow = $this->refundModel
|
||||
->where('invoice_id', $iid)
|
||||
->whereIn('status', ['Pending','Approved','Partial'])
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
if ($openRow) {
|
||||
$this->refundModel->update((int)$openRow['id'], [
|
||||
'refund_amount' => $over,
|
||||
'request' => 'overpayment',
|
||||
'updated_at' => utc_now(),
|
||||
'updated_by' => session()->get('user_id'),
|
||||
]);
|
||||
return redirect()->to(site_url('refunds/list'))->with('success', 'Overpayment updated for invoice ' . esc($invoiceNumber));
|
||||
}
|
||||
|
||||
// Else, if no open rows exist, either update existing overpayment (Paid) — skip creating new
|
||||
$existing = $this->refundModel
|
||||
->where('invoice_id', $iid)
|
||||
->where('request', 'overpayment')
|
||||
->orderBy('id', 'DESC')
|
||||
->first();
|
||||
if ($existing) {
|
||||
// If it's Paid, do not create a new duplicate line
|
||||
return redirect()->to(site_url('refunds/list'))->with('info', 'Overpayment was already resolved for this invoice.');
|
||||
}
|
||||
// As a last resort, create a single overpayment row
|
||||
$this->refundModel->insert([
|
||||
'parent_id' => $pid,
|
||||
'school_year' => $year,
|
||||
'semester' => $inv['semester'] ?? null,
|
||||
'invoice_id' => $iid,
|
||||
'refund_amount' => $over,
|
||||
'refund_paid_amount' => 0.00,
|
||||
'status' => 'Pending',
|
||||
'request' => 'overpayment',
|
||||
'reason' => 'Auto-detected per-invoice overpayment (manual)',
|
||||
'updated_by' => session()->get('user_id'),
|
||||
'created_at' => utc_now(),
|
||||
'updated_at' => utc_now(),
|
||||
]);
|
||||
$user = $this->userModel->select('id, email, firstname, lastname')->find($pid) ?: [];
|
||||
$eventData = [
|
||||
'user_id' => (int)($user['id'] ?? $pid),
|
||||
'email' => $user['email'] ?? null,
|
||||
'firstname' => $user['firstname'] ?? null,
|
||||
'lastname' => $user['lastname'] ?? null,
|
||||
'amount' => $over,
|
||||
'portalLink'=> base_url('/login'),
|
||||
];
|
||||
\CodeIgniter\Events\Events::trigger('refundPending', $eventData, []);
|
||||
return redirect()->to(site_url('refunds/list'))->with('success', 'Overpayment detected and refund created for invoice ' . esc($invoiceNumber));
|
||||
}
|
||||
return redirect()->to(site_url('refunds/list'))->with('info', 'No overpayment detected for invoice ' . esc($invoiceNumber));
|
||||
}
|
||||
return redirect()->to(site_url('refunds/list'))->with('error', 'Invoice not found: ' . esc($invoiceNumber));
|
||||
} catch (\Throwable $e) {
|
||||
return redirect()->to(site_url('refunds/list'))->with('error', 'Failed to recalc for invoice: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$res = $this->recalcOverpayments(true);
|
||||
$nNew = count($res['created_ids'] ?? []);
|
||||
$nUpd = count($res['updated_ids'] ?? []);
|
||||
return redirect()->to(site_url('refunds/list'))
|
||||
->with('success', "Recalculated overpayments: created {$nNew}, updated {$nUpd}.");
|
||||
}
|
||||
|
||||
/** Decision endpoint (Approved/Rejected) with reason */
|
||||
public function updateStatus()
|
||||
{
|
||||
log_message('debug', 'POST data: ' . print_r($_POST, true));
|
||||
|
||||
$refundId = (int)$this->request->getPost('refund_id');
|
||||
$status = $this->request->getPost('status');
|
||||
$reason = $this->request->getPost('reason');
|
||||
|
||||
if ($refundId <= 0 || !in_array($status, self::DECISIONS, true)) {
|
||||
return $this->response->setJSON(['error' => 'Invalid status update request.']);
|
||||
}
|
||||
if (empty($reason)) {
|
||||
return $this->response->setJSON(['error' => 'Reason is required for approval or rejection.']);
|
||||
}
|
||||
|
||||
$refund = $this->refundModel->find($refundId);
|
||||
if (!$refund) {
|
||||
return $this->response->setJSON(['error' => 'Refund not found.']);
|
||||
}
|
||||
|
||||
$ok = $this->refundModel->update($refundId, [
|
||||
'status' => $status,
|
||||
'reason' => $reason,
|
||||
'approved_at' => utc_now(),
|
||||
'approved_by' => session()->get('user_id'),
|
||||
'updated_at' => utc_now(),
|
||||
'updated_by' => session()->get('user_id'),
|
||||
]);
|
||||
|
||||
return $this->response->setJSON($ok ? ['success' => 'Refund status updated successfully.']
|
||||
: ['error' => 'Failed to update refund status.']);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,723 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Models\RoleModel;
|
||||
use App\Models\PermissionModel;
|
||||
use App\Models\RolePermissionModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\UserRoleModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\StaffModel;
|
||||
use CodeIgniter\Controller;
|
||||
|
||||
class RolePermissionController extends Controller
|
||||
{
|
||||
protected $roleModel;
|
||||
protected $userModel;
|
||||
protected $userRoleModel;
|
||||
protected $staffModel;
|
||||
protected $configModel;
|
||||
protected $permissionModel;
|
||||
protected $rolePermissionModel;
|
||||
protected $request;
|
||||
protected $db;
|
||||
protected $schoolYear;
|
||||
protected $semester;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->roleModel = new RoleModel();
|
||||
$this->userModel = new UserModel();
|
||||
$this->userRoleModel = new UserRoleModel();
|
||||
$this->staffModel = new StaffModel();
|
||||
$this->configModel = new ConfigurationModel();
|
||||
$this->permissionModel = new PermissionModel();
|
||||
$this->rolePermissionModel = new RolePermissionModel();
|
||||
|
||||
$this->request = \Config\Services::request();
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||
$this->semester = $this->configModel->getConfig('semester');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
helper('url');
|
||||
return view('rolepermission/list_roles', [
|
||||
'rolesEndpoint' => site_url('api/rolepermission/roles'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function assignRole()
|
||||
{
|
||||
helper('url');
|
||||
return view('rolepermission/assign_role', [
|
||||
'assignRoleEndpoint' => site_url('api/rolepermission/users'),
|
||||
'rolesEndpoint' => site_url('api/rolepermission/roles'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveRole()
|
||||
{
|
||||
if ($this->request->getMethod(true) === 'POST') {
|
||||
$userId = (int) $this->request->getPost('user_id');
|
||||
$roleIdsRaw = $this->request->getPost('role_ids');
|
||||
$roleIdsArray = is_array($roleIdsRaw) ? $roleIdsRaw : ($roleIdsRaw !== null ? [$roleIdsRaw] : []);
|
||||
$roleIds = array_values(array_unique(array_filter(array_map('intval', $roleIdsArray), fn($id) => $id > 0)));
|
||||
$adminId = session()->get('user_id');
|
||||
|
||||
if ($userId <= 0) {
|
||||
return redirect()->to('rolepermission/assign_role')->with('error', 'Invalid user selection.');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->db->transStart();
|
||||
|
||||
// Remove all existing roles for the user
|
||||
$this->userRoleModel->where('user_id', $userId)->delete();
|
||||
|
||||
// Assign new roles
|
||||
if (!empty($roleIds)) {
|
||||
foreach ($roleIds as $roleId) {
|
||||
$this->userRoleModel->insert([
|
||||
'user_id' => $userId,
|
||||
'role_id' => $roleId,
|
||||
'created_at' => utc_now(),
|
||||
'updated_by' => $adminId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->transComplete();
|
||||
|
||||
if ($this->db->transStatus() === false) {
|
||||
throw new \RuntimeException('Transaction failed.');
|
||||
}
|
||||
|
||||
// ✅ Load role names after saving
|
||||
$assignedRoles = !empty($roleIds)
|
||||
? $this->roleModel->whereIn('id', $roleIds)->findAll()
|
||||
: [];
|
||||
$roleNames = array_map(fn($r) => strtolower($r['name']), $assignedRoles ?? []);
|
||||
|
||||
// ✅ Update staff record if needed
|
||||
$this->updateStaffRecord($userId, $roleNames);
|
||||
|
||||
return redirect()->to('rolepermission/assign_role')->with('success', 'Roles updated successfully.');
|
||||
} catch (\Exception $e) {
|
||||
$this->db->transRollback();
|
||||
log_message('error', 'Error updating roles: ' . $e->getMessage());
|
||||
return redirect()->to('rolepermission/assign_role')->with('error', 'Failed to update roles.');
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->to('rolepermission/assign_role')->with('error', 'Invalid request.');
|
||||
}
|
||||
|
||||
public function usersData()
|
||||
{
|
||||
return $this->response->setJSON($this->buildUsersWithRolesPayload());
|
||||
}
|
||||
|
||||
public function rolesData()
|
||||
{
|
||||
$rows = $this->roleModel
|
||||
->select('roles.id, roles.name, roles.description, COUNT(DISTINCT user_roles.user_id) AS user_count')
|
||||
->join('user_roles', 'user_roles.role_id = roles.id', 'left')
|
||||
->groupBy('roles.id, roles.name, roles.description')
|
||||
->orderBy('roles.name', 'ASC')
|
||||
->findAll();
|
||||
|
||||
$normalized = array_map(static function ($row) {
|
||||
if (!is_array($row)) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'name' => (string) ($row['name'] ?? ''),
|
||||
'description' => $row['description'] ?? null,
|
||||
'user_count' => (int) ($row['user_count'] ?? 0),
|
||||
];
|
||||
}, $rows ?? []);
|
||||
|
||||
return $this->response->setJSON(['roles' => $normalized]);
|
||||
}
|
||||
|
||||
private function buildUsersWithRolesPayload(): array
|
||||
{
|
||||
$rows = $this->userModel
|
||||
->select('users.id, users.firstname, users.lastname, users.email, users.account_id, roles.name AS role_name')
|
||||
->join('user_roles', 'users.id = user_roles.user_id', 'left')
|
||||
->join('roles', 'user_roles.role_id = roles.id', 'left')
|
||||
->orderBy('users.id', 'ASC')
|
||||
->findAll();
|
||||
|
||||
$users = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($users[$id])) {
|
||||
$users[$id] = [
|
||||
'id' => $id,
|
||||
'firstname' => $row['firstname'] ?? '',
|
||||
'lastname' => $row['lastname'] ?? '',
|
||||
'email' => $row['email'] ?? '',
|
||||
'account_id' => $row['account_id'] ?? '',
|
||||
'roles' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$roleName = $row['role_name'] ?? null;
|
||||
if ($roleName) {
|
||||
$users[$id]['roles'][] = $roleName;
|
||||
}
|
||||
}
|
||||
|
||||
$normalized = array_map(static function ($user) {
|
||||
$roles = array_values(array_unique(array_filter($user['roles'] ?? [], 'strlen')));
|
||||
return [
|
||||
'id' => $user['id'],
|
||||
'firstname' => $user['firstname'] ?? '',
|
||||
'lastname' => $user['lastname'] ?? '',
|
||||
'email' => $user['email'] ?? '',
|
||||
'account_id' => $user['account_id'] ?? '',
|
||||
'roles' => $roles,
|
||||
];
|
||||
}, array_values($users));
|
||||
|
||||
return ['users' => $normalized];
|
||||
}
|
||||
|
||||
private function updateStaffRecord(int $userId, array $newRoleNames): void
|
||||
{
|
||||
$excludedRoles = ['parent', 'student', 'guest'];
|
||||
$loweredNewRoles = array_map('strtolower', $newRoleNames);
|
||||
|
||||
$user = $this->userModel->find($userId);
|
||||
if (!$user) {
|
||||
log_message('error', "updateStaffRecord: user_id {$userId} not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch existing staff record if it exists
|
||||
$existingStaff = $this->staffModel->where('user_id', $userId)->first();
|
||||
|
||||
// Extract existing roles from DB if any
|
||||
$existingRoles = [];
|
||||
if (!empty($existingStaff['role_name'])) {
|
||||
$existingRoles = array_map('strtolower', array_map('trim', explode(',', $existingStaff['role_name'])));
|
||||
}
|
||||
|
||||
// Merge and deduplicate roles
|
||||
$allRoles = array_unique(array_merge($existingRoles, $loweredNewRoles));
|
||||
|
||||
// Determine staff roles (excluding parent/student/guest)
|
||||
$staffRoles = array_filter($newRoleNames, fn($role) => !in_array($role, $excludedRoles));
|
||||
|
||||
// Determine active role
|
||||
$activeRole = !empty($staffRoles) ? $staffRoles[0] : 'inactive';
|
||||
|
||||
$email = $this->generateUniqueStaffEmail($user['firstname'], $user['lastname'], $userId);
|
||||
$now = utc_now();
|
||||
|
||||
$row = [
|
||||
'user_id' => $userId,
|
||||
'firstname' => $user['firstname'],
|
||||
'lastname' => $user['lastname'],
|
||||
'email' => $email,
|
||||
'phone' => $user['cellphone'],
|
||||
'role_name' => implode(', ', $allRoles), // historical roles
|
||||
'active_role' => $activeRole,
|
||||
'school_year' => $this->schoolYear,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
$this->staffModel->upsert($row);
|
||||
}
|
||||
|
||||
private function generateUniqueStaffEmail(string $firstname, string $lastname, int $userId): string
|
||||
{
|
||||
$domain = '@alrahmaisgl.org';
|
||||
|
||||
// Sanitize names: lowercase, letters only
|
||||
$firstname = strtolower(preg_replace('/[^a-z]/i', '', $firstname));
|
||||
$lastname = strtolower(preg_replace('/[^a-z]/i', '', $lastname));
|
||||
|
||||
// Use at least 1 letter from firstname
|
||||
$min = 1;
|
||||
$max = strlen($firstname);
|
||||
$base = '';
|
||||
$email = '';
|
||||
|
||||
for ($i = $min; $i <= $max; $i++) {
|
||||
$base = substr($firstname, 0, $i) . $lastname;
|
||||
$email = $base . $domain;
|
||||
|
||||
// If email not taken or belongs to same user, use it
|
||||
$exists = $this->staffModel->where('email', $email)
|
||||
->where('user_id !=', $userId)
|
||||
->first();
|
||||
if (!$exists) {
|
||||
return $email;
|
||||
}
|
||||
}
|
||||
|
||||
// If all combinations used, fallback to adding user ID
|
||||
return $base . $userId . $domain;
|
||||
}
|
||||
|
||||
public function listRoles()
|
||||
{
|
||||
helper('url');
|
||||
return view('rolepermission/list_roles', [
|
||||
'rolesEndpoint' => site_url('api/rolepermission/roles'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function editRolePermissions($roleId)
|
||||
{
|
||||
// Fetch roles, permissions, and role permissions data
|
||||
$roles = $this->roleModel->findAll();
|
||||
$permissions = $this->permissionModel->findAll();
|
||||
$rolePermissions = $this->rolePermissionModel->where('role_id', $roleId)->findAll();
|
||||
|
||||
// Convert existing role permissions to a simple array for comparison
|
||||
$existingPermissions = [];
|
||||
foreach ($rolePermissions as $rolePermission) {
|
||||
$existingPermissions[$rolePermission['permission_id']] = [
|
||||
'can_create' => $rolePermission['can_create'],
|
||||
'can_read' => $rolePermission['can_read'],
|
||||
'can_update' => $rolePermission['can_update'],
|
||||
'can_delete' => $rolePermission['can_delete'],
|
||||
];
|
||||
}
|
||||
|
||||
// Handle POST request
|
||||
if (strtolower($this->request->getMethod()) === 'post') {
|
||||
// Get the posted permissions data
|
||||
$postPermissions = $this->request->getPost('permissions');
|
||||
|
||||
// Call saveRolePermissions with the correct data
|
||||
return $this->saveRolePermissions($roleId, $postPermissions);
|
||||
}
|
||||
|
||||
return view('rolepermission/edit_role_permissions', [
|
||||
'roles' => $roles,
|
||||
'roleId' => $roleId,
|
||||
'permissions' => $permissions,
|
||||
'rolePermissions' => $rolePermissions,
|
||||
'existingPermissions' => $existingPermissions
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveRolePermissions($roleId = null, $permissions = null)
|
||||
{
|
||||
// If $roleId and $permissions are not passed, retrieve them from POST
|
||||
if (!$roleId) {
|
||||
$roleId = $this->request->getPost('role_id');
|
||||
}
|
||||
|
||||
if (!$permissions) {
|
||||
$permissions = $this->request->getPost('permissions');
|
||||
}
|
||||
|
||||
if (!$roleId) {
|
||||
return redirect()->back()->with('error', 'Role ID is missing.');
|
||||
}
|
||||
|
||||
// Fetch existing permissions for the role
|
||||
$rolePermissions = $this->rolePermissionModel->where('role_id', $roleId)->findAll();
|
||||
|
||||
$existingPermissions = [];
|
||||
foreach ($rolePermissions as $rolePermission) {
|
||||
$existingPermissions[$rolePermission['permission_id']] = $rolePermission;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->rolePermissionModel->transStart();
|
||||
|
||||
// Process the new permissions if provided
|
||||
if ($permissions && is_array($permissions)) {
|
||||
foreach ($permissions as $permissionId => $crudValues) {
|
||||
// Ensure CRUD values are correctly set, defaulting to 0 if not provided
|
||||
$permissionData = [
|
||||
'can_create' => isset($crudValues['create']) ? 1 : 0,
|
||||
'can_read' => isset($crudValues['read']) ? 1 : 0,
|
||||
'can_update' => isset($crudValues['update']) ? 1 : 0,
|
||||
'can_delete' => isset($crudValues['delete']) ? 1 : 0,
|
||||
'updated_at' => utc_now(),
|
||||
];
|
||||
|
||||
if (isset($existingPermissions[$permissionId])) {
|
||||
// Update existing permission
|
||||
$this->rolePermissionModel->where('role_id', $roleId)
|
||||
->where('permission_id', $permissionId)
|
||||
->set($permissionData)
|
||||
->update();
|
||||
} else {
|
||||
// Insert new permission
|
||||
$permissionData['role_id'] = $roleId;
|
||||
$permissionData['permission_id'] = $permissionId;
|
||||
$permissionData['created_at'] = utc_now();
|
||||
$this->rolePermissionModel->insert($permissionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove permissions that are no longer present
|
||||
foreach ($existingPermissions as $permissionId => $permissionData) {
|
||||
if (!isset($permissions[$permissionId])) {
|
||||
$this->rolePermissionModel->where('role_id', $roleId)
|
||||
->where('permission_id', $permissionId)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
||||
$this->rolePermissionModel->transComplete();
|
||||
|
||||
if ($this->rolePermissionModel->transStatus() === false) {
|
||||
throw new \RuntimeException('Transaction failed.');
|
||||
}
|
||||
|
||||
return redirect()->to('/rolepermission/edit_role_permissions/' . $roleId)
|
||||
->with('status', 'Permissions saved successfully.');
|
||||
} catch (\Exception $e) {
|
||||
$this->rolePermissionModel->transRollback();
|
||||
log_message('error', 'Failed to save permissions: ' . $e->getMessage());
|
||||
return redirect()->back()->with('error', 'Failed to save permissions: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Role management functions
|
||||
public function rolesList()
|
||||
{
|
||||
return $this->listRoles();
|
||||
}
|
||||
|
||||
|
||||
public function addRole()
|
||||
{
|
||||
return view('rolepermission/add_role');
|
||||
}
|
||||
|
||||
public function storeRole()
|
||||
{
|
||||
$validation = \Config\Services::validation();
|
||||
|
||||
// Validation rules
|
||||
$validation->setRules([
|
||||
'name' => 'required|min_length[3]|max_length[255]|is_unique[roles.name]',
|
||||
'slug' => 'permit_empty|max_length[64]|is_unique[roles.slug]',
|
||||
'description' => 'permit_empty|max_length[500]',
|
||||
'dashboard_route' => [
|
||||
'rules' => 'required|min_length[1]|max_length[255]|regex_match[/^[a-z0-9_\\/\\-]+$/]',
|
||||
'errors' => [
|
||||
'regex_match' => 'Route may contain lowercase letters, numbers, /, -, and _.'
|
||||
]
|
||||
],
|
||||
'priority' => 'required|integer|greater_than_equal_to[0]|less_than_equal_to[100000]',
|
||||
'is_active' => 'required|in_list[0,1]',
|
||||
]);
|
||||
|
||||
if (!$validation->withRequest($this->request)->run()) {
|
||||
return redirect()->back()->withInput()->with('errors', $validation->getErrors());
|
||||
}
|
||||
|
||||
// Normalize + sanitize inputs
|
||||
$rawName = trim((string) $this->request->getPost('name'));
|
||||
$rawSlug = trim((string) $this->request->getPost('slug'));
|
||||
$route = trim((string) $this->request->getPost('dashboard_route'));
|
||||
$desc = trim((string) $this->request->getPost('description'));
|
||||
$priority = (int) $this->request->getPost('priority');
|
||||
$isActive = (string) $this->request->getPost('is_active') === '1' ? 1 : 0;
|
||||
|
||||
// Force lowercase for consistency
|
||||
$name = strtolower($rawName);
|
||||
$dashboard_route = strtolower($route);
|
||||
$description = strtolower($desc);
|
||||
|
||||
// Build slug: from input if provided, else from name
|
||||
$slug = $rawSlug !== '' ? $rawSlug : $name;
|
||||
// convert spaces / invalid chars to underscores and lowercase
|
||||
$slug = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $slug));
|
||||
$slug = trim($slug, '_');
|
||||
|
||||
// Ensure slug uniqueness (in case collision after normalization)
|
||||
$slug = $this->ensureUniqueSlug($slug);
|
||||
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'slug' => $slug,
|
||||
'description' => $description,
|
||||
'dashboard_route' => $dashboard_route,
|
||||
'priority' => $priority,
|
||||
'is_active' => $isActive,
|
||||
'created_at' => utc_now(),
|
||||
'updated_at' => utc_now(),
|
||||
];
|
||||
|
||||
if ($this->roleModel->insert($data)) {
|
||||
return redirect()->to('rolepermission/roles')->with('success', 'Role created successfully!');
|
||||
}
|
||||
|
||||
return redirect()->back()->withInput()->with('error', 'Failed to create role.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure slug is unique in roles.slug; if exists, append -2, -3, ...
|
||||
*/
|
||||
/**
|
||||
* Ensure slug is unique in the roles table.
|
||||
* - If $ignoreId is provided, that record is excluded (for updates).
|
||||
* - Appends _2, _3, ... if duplicates exist.
|
||||
*/
|
||||
private function ensureUniqueSlug(string $baseSlug, ?int $ignoreId = null): string
|
||||
{
|
||||
$slug = $baseSlug;
|
||||
$i = 2;
|
||||
|
||||
while (true) {
|
||||
$builder = $this->roleModel->where('slug', $slug);
|
||||
if ($ignoreId !== null) {
|
||||
$builder->where('id !=', $ignoreId);
|
||||
}
|
||||
|
||||
$exists = $builder->countAllResults() > 0;
|
||||
|
||||
if (!$exists) {
|
||||
break;
|
||||
}
|
||||
|
||||
$slug = $baseSlug . '_' . $i;
|
||||
$i++;
|
||||
|
||||
if ($i > 100000) { // safety stop
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function editRole($id)
|
||||
{
|
||||
if (!$role = $this->roleModel->find($id)) {
|
||||
return redirect()->to('rolepermission/roles')->with('error', 'Role not found.');
|
||||
}
|
||||
|
||||
return view('rolepermission/edit_role', ['role' => $role]);
|
||||
}
|
||||
|
||||
public function updateRole($id)
|
||||
{
|
||||
$role = $this->roleModel->find($id);
|
||||
if (!$role) {
|
||||
return redirect()->to('rolepermission/roles')->with('error', 'Role not found.');
|
||||
}
|
||||
|
||||
$validation = \Config\Services::validation();
|
||||
// CI4 is_unique format: is_unique[table.field,ignore_field,ignore_value]
|
||||
$validation->setRules([
|
||||
'name' => "required|min_length[3]|max_length[255]|is_unique[roles.name,id,{$id}]",
|
||||
'slug' => "permit_empty|max_length[64]|is_unique[roles.slug,id,{$id}]",
|
||||
'description' => 'permit_empty|max_length[500]',
|
||||
'dashboard_route' => [
|
||||
'rules' => 'required|min_length[1]|max_length[255]|regex_match[/^[a-z0-9_\\/\\-]+$/]',
|
||||
'errors' => [
|
||||
'regex_match' => 'Route may contain lowercase letters, numbers, /, -, and _.'
|
||||
]
|
||||
],
|
||||
'priority' => 'required|integer|greater_than_equal_to[0]|less_than_equal_to[100000]',
|
||||
'is_active' => 'required|in_list[0,1]',
|
||||
]);
|
||||
|
||||
if (!$validation->withRequest($this->request)->run()) {
|
||||
return redirect()->back()->withInput()->with('errors', $validation->getErrors());
|
||||
}
|
||||
|
||||
// Normalize inputs (lowercase storage)
|
||||
$rawName = trim((string) $this->request->getPost('name'));
|
||||
$rawSlug = trim((string) $this->request->getPost('slug'));
|
||||
$route = trim((string) $this->request->getPost('dashboard_route'));
|
||||
$desc = trim((string) $this->request->getPost('description'));
|
||||
$priority = (int) $this->request->getPost('priority');
|
||||
$isActive = (string) $this->request->getPost('is_active') === '1' ? 1 : 0;
|
||||
|
||||
$name = strtolower($rawName);
|
||||
$dashboard_route = strtolower($route);
|
||||
$description = strtolower($desc);
|
||||
|
||||
// slug: if blank, derive from name; else normalize
|
||||
$slug = $rawSlug !== '' ? $rawSlug : $name;
|
||||
$slug = strtolower(preg_replace('/[^a-z0-9]+/i', '_', $slug));
|
||||
$slug = trim($slug, '_');
|
||||
|
||||
// Ensure unique slug if changed or blank turned into new value
|
||||
if ($slug !== $role['slug']) {
|
||||
$slug = $this->ensureUniqueSlug($slug, (int) $id);
|
||||
}
|
||||
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'slug' => $slug,
|
||||
'description' => $description,
|
||||
'dashboard_route' => $dashboard_route,
|
||||
'priority' => $priority,
|
||||
'is_active' => $isActive,
|
||||
'updated_at' => utc_now(),
|
||||
];
|
||||
|
||||
if ($this->roleModel->update($id, $data)) {
|
||||
return redirect()->to('rolepermission/roles')->with('success', 'Role updated successfully!');
|
||||
}
|
||||
|
||||
return redirect()->back()->withInput()->with('error', 'Failed to update role.');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public function deleteRole($id)
|
||||
{
|
||||
// Check if role exists
|
||||
$role = $this->roleModel->find($id);
|
||||
if (!$role) {
|
||||
return redirect()->to(site_url('rolepermission/roles'))
|
||||
->with('error', 'Role not found.');
|
||||
}
|
||||
|
||||
// Try deleting
|
||||
if ($this->roleModel->delete($id)) {
|
||||
return redirect()->to(site_url('rolepermission/roles'))
|
||||
->with('success', 'Role deleted successfully.');
|
||||
} else {
|
||||
return redirect()->to(site_url('rolepermission/roles'))
|
||||
->with('error', 'Failed to delete role.');
|
||||
}
|
||||
}
|
||||
|
||||
// ADD: show form + handle submit
|
||||
public function addPermission()
|
||||
{
|
||||
if (strtolower($this->request->getMethod()) === 'post') {
|
||||
$name = trim((string) $this->request->getPost('name'));
|
||||
$desc = trim((string) $this->request->getPost('description'));
|
||||
|
||||
// Basic guard
|
||||
if ($name === '') {
|
||||
return redirect()->back()
|
||||
->with('error', 'Permission name is required.')
|
||||
->withInput();
|
||||
}
|
||||
|
||||
// Check existence (collation is case-insensitive: utf8mb4_unicode_ci)
|
||||
$existing = $this->permissionModel->where('name', $name)->first();
|
||||
|
||||
if ($existing) {
|
||||
// Idempotent: do nothing if already exists
|
||||
return redirect()->to(site_url('rolepermission/list_permissions'))
|
||||
->with('success', 'Permission already exists. No changes made.');
|
||||
}
|
||||
|
||||
// Create new
|
||||
$data = [
|
||||
'name' => $name,
|
||||
'description' => $desc,
|
||||
];
|
||||
|
||||
if (!$this->permissionModel->save($data)) {
|
||||
return redirect()->back()
|
||||
->with('error', implode(' ', $this->permissionModel->errors()))
|
||||
->withInput();
|
||||
}
|
||||
|
||||
return redirect()->to(site_url('rolepermission/list_permissions'))
|
||||
->with('success', 'Permission added successfully.');
|
||||
}
|
||||
|
||||
// GET -> form
|
||||
return view('rolepermission/add_permission');
|
||||
}
|
||||
|
||||
|
||||
// EDIT: show form + handle submit
|
||||
public function editPermission($id = null)
|
||||
{
|
||||
$permission = $this->permissionModel->find($id);
|
||||
if (!$permission) {
|
||||
return redirect()->to(site_url('roles'))->with('error', 'Permission not found.');
|
||||
}
|
||||
|
||||
if (strtolower($this->request->getMethod()) === 'post') {
|
||||
$data = [
|
||||
'id' => $id, // needed for is_unique ignore
|
||||
'name' => trim($this->request->getPost('name')),
|
||||
'description' => trim($this->request->getPost('description')),
|
||||
];
|
||||
|
||||
if (!$this->permissionModel->save($data)) {
|
||||
return redirect()->back()
|
||||
->with('error', implode(' ', $this->permissionModel->errors()))
|
||||
->withInput();
|
||||
}
|
||||
|
||||
return redirect()->to(site_url('rolepermission/list_permissions'))
|
||||
->with('success', 'Permission updated successfully.');
|
||||
}
|
||||
|
||||
return view('rolepermission/edit_permission', ['permission' => $permission]);
|
||||
}
|
||||
|
||||
// Optional delete
|
||||
public function deletePermission($id = null)
|
||||
{
|
||||
if (!$this->permissionModel->find($id)) {
|
||||
return redirect()->to(site_url('rolepermission/list_permissions'))->with('error', 'Permission not found.');
|
||||
}
|
||||
|
||||
$this->permissionModel->delete($id);
|
||||
return redirect()->to(site_url('rolepermission/list_permissions'))->with('success', 'Permission deleted.');
|
||||
}
|
||||
|
||||
public function listPermissions()
|
||||
{
|
||||
helper('url');
|
||||
return view('rolepermission/permissionList', [
|
||||
'permissionsEndpoint' => site_url('api/rolepermission/permissions'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function permissionsData()
|
||||
{
|
||||
$rows = $this->permissionModel
|
||||
->select('id, name, description, created_at, updated_at')
|
||||
->orderBy('id', 'ASC')
|
||||
->findAll();
|
||||
|
||||
$normalized = array_map(static function ($row) {
|
||||
if (!is_array($row)) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'name' => (string) ($row['name'] ?? ''),
|
||||
'description' => $row['description'] ?? null,
|
||||
'created_at' => $row['created_at'] ?? null,
|
||||
'updated_at' => $row['updated_at'] ?? null,
|
||||
];
|
||||
}, $rows ?? []);
|
||||
|
||||
return $this->response->setJSON(['permissions' => $normalized]);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\RoleModel;
|
||||
|
||||
class RoleService
|
||||
{
|
||||
public function __construct(private RoleModel $roles = new RoleModel()) {}
|
||||
|
||||
/** @param string[] $roleKeys names or slugs */
|
||||
public function bestDashboardRouteFor(array $roleKeys): string
|
||||
{
|
||||
$rows = $this->roles->findByNamesOrSlugs($roleKeys);
|
||||
if (!empty($rows)) {
|
||||
// first row is highest priority due to orderBy('priority','ASC')
|
||||
return $rows[0]['dashboard_route'] ?? '/landing_page/guest_dashboard';
|
||||
}
|
||||
return '/landing_page/guest_dashboard';
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\UserRoleModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use CodeIgniter\Controller;
|
||||
use App\Models\RoleModel;
|
||||
|
||||
class RoleSwitcherController extends BaseController
|
||||
{
|
||||
protected $db;
|
||||
protected $configModel;
|
||||
protected $semester;
|
||||
protected $schoolYear;
|
||||
protected $userRoleModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Load the database service
|
||||
$this->db = \Config\Database::connect();
|
||||
// Check if the database connection is established
|
||||
if (!$this->db->connect()) {
|
||||
log_message('error', 'Database connection failed.');
|
||||
throw new \Exception('Database connection failed.');
|
||||
} else {
|
||||
log_message('info', 'Database connection successful.');
|
||||
}
|
||||
$this->configModel = new ConfigurationModel();
|
||||
$this->userRoleModel = new UserRoleModel();
|
||||
$this->semester = $this->configModel->getConfig('semester');
|
||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$userId = session()->get('user_id');
|
||||
|
||||
if (!$userId) {
|
||||
return redirect()->to('/login')->with('error', 'Please log in first.');
|
||||
}
|
||||
$roles = $this->userRoleModel
|
||||
->select('roles.name')
|
||||
->join('roles', 'roles.id = user_roles.role_id')
|
||||
->where('user_roles.user_id', $userId)
|
||||
->findAll();
|
||||
|
||||
if (empty($roles)) {
|
||||
return redirect()->back()->with('error', 'No roles assigned to this user.');
|
||||
}
|
||||
|
||||
$roleNames = array_column($roles, 'name');
|
||||
|
||||
if (count($roleNames) === 1) {
|
||||
return $this->redirectToDashboard($roleNames[0]);
|
||||
}
|
||||
|
||||
return view('account/choose_account', ['roles' => $roleNames]);
|
||||
}
|
||||
|
||||
public function switchTo($role)
|
||||
{
|
||||
session()->set('active_role', $role);
|
||||
return $this->redirectToDashboard($role);
|
||||
}
|
||||
|
||||
/*
|
||||
private function redirectToDashboard($role)
|
||||
{
|
||||
return match ($role) {
|
||||
'administrator' => redirect()->to('administrator/administratordashboard'),
|
||||
'administrative staff' => redirect()->to('/landing_page/admin_dashboard'),
|
||||
'teacher' => redirect()->to('/teacher_dashboard'),
|
||||
'student' => redirect()->to('/landing_page/student_dashboard'),
|
||||
'parent' => redirect()->to('/parent_dashboard'),
|
||||
'head_of_department' => redirect()->to('/hod/dashboard'),
|
||||
default => redirect()->to('/')->with('error', 'Unknown role.'),
|
||||
};
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
private function redirectToDashboard($role)
|
||||
{
|
||||
$key = is_string($role) ? $role : (string) $role;
|
||||
|
||||
$route = (new RoleModel())->getRouteByNameOrSlug($key)
|
||||
?? '/landing_page/guest_dashboard';
|
||||
|
||||
log_message('debug', 'Redirecting user to: ' . $route);
|
||||
return redirect()->to($route);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,658 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
use App\Controllers\BaseController;
|
||||
|
||||
use App\Models\CalendarModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
use App\Models\TeacherModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\ParentMeetingScheduleModel;
|
||||
use App\Services\EmailService;
|
||||
|
||||
|
||||
class SchoolCalendarController extends BaseController
|
||||
{
|
||||
protected $calendarModel;
|
||||
protected $configModel;
|
||||
protected $semester;
|
||||
protected $schoolYear;
|
||||
protected $db;
|
||||
protected $eventTypes = [
|
||||
'1st Semester Scores',
|
||||
'2nd Semester Scores',
|
||||
'Break',
|
||||
'Event',
|
||||
'Final',
|
||||
'Final Exam Draft',
|
||||
'Midterm',
|
||||
'Midterm Exam Draft',
|
||||
];
|
||||
|
||||
protected EmailService $emailService;
|
||||
protected UserModel $userModel;
|
||||
protected TeacherModel $teacherModel;
|
||||
protected ParentMeetingScheduleModel $meetingModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Load the models
|
||||
$this->calendarModel = new CalendarModel();
|
||||
$this->configModel = new ConfigurationModel();
|
||||
$this->userModel = new UserModel();
|
||||
$this->teacherModel = new TeacherModel();
|
||||
$this->emailService = new EmailService();
|
||||
$this->meetingModel = new ParentMeetingScheduleModel();
|
||||
$this->db = \Config\Database::connect();
|
||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||
$this->semester = $this->configModel->getConfig('semester');
|
||||
|
||||
// Load helpers
|
||||
helper(['form', 'url']);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
// Get school_year / semester from query string or defaults
|
||||
$syParam = $this->request->getGet('school_year');
|
||||
$semParam = $this->request->getGet('semester');
|
||||
$schoolYear = $syParam ?: $this->schoolYear;
|
||||
$semester = $semParam ?: '';
|
||||
|
||||
if ($semester !== '') {
|
||||
$events = $this->calendarModel->getEventsBySchoolYearAndSemester($schoolYear, $semester);
|
||||
} else {
|
||||
$events = $this->calendarModel->getEventsBySchoolYear($schoolYear);
|
||||
}
|
||||
|
||||
return view('administrator/calendar_view', [
|
||||
'events' => $events,
|
||||
'schoolYear' => $schoolYear,
|
||||
'semester' => $semester,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$event = $this->calendarModel->find($id);
|
||||
|
||||
if (!$event) {
|
||||
return redirect()->to('/administrator/calendar_view')->with('error', 'Event not found');
|
||||
}
|
||||
|
||||
return view('administrator/calendar_edit', [
|
||||
'event' => $event,
|
||||
'eventTypes' => $this->eventTypes,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
// Fetch the event by ID
|
||||
$event = $this->calendarModel->find($id);
|
||||
|
||||
if (!$event) {
|
||||
return redirect()->to('/administrator/calendar_view')->with('error', 'Event not found.');
|
||||
}
|
||||
|
||||
if (strtolower($this->request->getMethod()) === 'post') {
|
||||
$validation = $this->validate([
|
||||
'title' => 'required|min_length[3]',
|
||||
'start' => 'required|valid_date[Y-m-d]',
|
||||
]);
|
||||
|
||||
if (!$validation) {
|
||||
return view('administrator/calendar_edit', [
|
||||
'validation' => $this->validator,
|
||||
'event' => $event,
|
||||
'eventTypes' => $this->eventTypes,
|
||||
]);
|
||||
}
|
||||
|
||||
$emailTargets = $this->emailNotificationTargetsFromRequest();
|
||||
|
||||
$data = [
|
||||
'title' => $this->request->getPost('title'),
|
||||
'description' => $this->request->getPost('description'),
|
||||
'event_type' => $this->request->getPost('event_type') ?: null,
|
||||
'date' => $this->request->getPost('start'),
|
||||
'notify_parent' => $this->request->getPost('notify_parent') ? 1 : 0,
|
||||
'notify_teacher' => $this->request->getPost('notify_teacher') ? 1 : 0,
|
||||
'notify_admin' => $this->request->getPost('notify_admin') ? 1 : 0,
|
||||
'no_school' => $this->request->getPost('no_school') ? 1 : 0,
|
||||
'school_year' => $this->request->getPost('school_year') ?: $this->schoolYear,
|
||||
'semester' => $this->request->getPost('semester') ?: $this->semester,
|
||||
];
|
||||
if (!$this->calendarModel->supportsEventType()) {
|
||||
unset($data['event_type']);
|
||||
}
|
||||
|
||||
if ($this->calendarModel->update($id, $data)) {
|
||||
$updatedEvent = $this->calendarModel->find($id);
|
||||
$emailStatus = $this->dispatchCalendarNotificationEmails($updatedEvent, $emailTargets);
|
||||
$filters = [
|
||||
'school_year' => $data['school_year'],
|
||||
'semester' => $data['semester'],
|
||||
];
|
||||
$redirect = redirect()->to($this->buildCalendarViewUrl($filters))->with('success', 'Event updated successfully');
|
||||
if ($emailFlash = $this->buildEmailFlash($emailStatus)) {
|
||||
$redirect = $redirect
|
||||
->with('email_status', $emailFlash['message'])
|
||||
->with('email_status_class', $emailFlash['class']);
|
||||
}
|
||||
return $redirect;
|
||||
} else {
|
||||
return redirect()->back()->with('error', 'Failed to update the event.');
|
||||
}
|
||||
}
|
||||
|
||||
return view('administrator/calendar_edit', [
|
||||
'event' => $event,
|
||||
'eventTypes' => $this->eventTypes,
|
||||
]);
|
||||
}
|
||||
|
||||
public function addEvent()
|
||||
{
|
||||
if (strtolower($this->request->getMethod()) === 'post') {
|
||||
$validation = $this->validate([
|
||||
'title' => 'required|min_length[3]',
|
||||
'start' => 'required|valid_date[Y-m-d]',
|
||||
]);
|
||||
|
||||
if (!$validation) {
|
||||
return view('administrator/calendar_add_event', [
|
||||
'validation' => $this->validator,
|
||||
'eventTypes' => $this->eventTypes,
|
||||
]);
|
||||
}
|
||||
|
||||
$emailTargets = $this->emailNotificationTargetsFromRequest();
|
||||
|
||||
// Prepare data
|
||||
$data = [
|
||||
'title' => $this->request->getPost('title'),
|
||||
'description' => $this->request->getPost('description'),
|
||||
'event_type' => $this->request->getPost('event_type') ?: null,
|
||||
'date' => $this->request->getPost('start'),
|
||||
'notify_parent' => $this->request->getPost('notify_parent') ? 1 : 0,
|
||||
'notify_teacher' => $this->request->getPost('notify_teacher') ? 1 : 0,
|
||||
'notify_admin' => $this->request->getPost('notify_admin') ? 1 : 0,
|
||||
'no_school' => $this->request->getPost('no_school') ? 1 : 0,
|
||||
'school_year' => $this->request->getPost('school_year') ?: $this->schoolYear,
|
||||
'semester' => $this->request->getPost('semester') ?: $this->semester,
|
||||
];
|
||||
if (!$this->calendarModel->supportsEventType()) {
|
||||
unset($data['event_type']);
|
||||
}
|
||||
|
||||
if ($this->calendarModel->save($data)) {
|
||||
$insertedId = $this->calendarModel->getInsertID();
|
||||
$savedEvent = $this->calendarModel->find($insertedId);
|
||||
$emailStatus = $this->dispatchCalendarNotificationEmails($savedEvent, $emailTargets);
|
||||
log_message('info', 'Event saved successfully: ' . print_r($data, true));
|
||||
$filters = [
|
||||
'school_year' => $data['school_year'],
|
||||
'semester' => $data['semester'],
|
||||
];
|
||||
$redirect = redirect()->to($this->buildCalendarViewUrl($filters))->with('success', 'Event added successfully');
|
||||
if ($emailFlash = $this->buildEmailFlash($emailStatus)) {
|
||||
$redirect = $redirect
|
||||
->with('email_status', $emailFlash['message'])
|
||||
->with('email_status_class', $emailFlash['class']);
|
||||
}
|
||||
return $redirect;
|
||||
} else {
|
||||
log_message('error', 'Failed to save event: ' . print_r($data, true));
|
||||
return redirect()->back()->with('error', 'Failed to save the event.');
|
||||
}
|
||||
}
|
||||
|
||||
return view('administrator/calendar_add_event', [
|
||||
'eventTypes' => $this->eventTypes,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function emailNotificationTargetsFromRequest(): array
|
||||
{
|
||||
return [
|
||||
'parents' => (bool) $this->request->getPost('send_email_parent'),
|
||||
'teachers' => (bool) $this->request->getPost('send_email_teacher'),
|
||||
'admins' => (bool) $this->request->getPost('send_email_admin'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function dispatchCalendarNotificationEmails(array $event = null, array $targets = []): array
|
||||
{
|
||||
if (empty($event) || empty(array_filter($targets))) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$subject = $this->buildCalendarEmailSubject($event);
|
||||
$innerBody = view('emails/calendar_notification', [
|
||||
'event' => $event,
|
||||
'calendarUrl' => base_url('administrator/calendar_view'),
|
||||
]);
|
||||
$body = view('emails/_wrap_layout', [
|
||||
'title' => $subject,
|
||||
'subject' => $subject,
|
||||
'body_html' => $innerBody,
|
||||
]);
|
||||
|
||||
$results = [];
|
||||
foreach ($targets as $group => $enabled) {
|
||||
if (!$enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$emails = $this->collectEmailsForGroup($group);
|
||||
if (empty($emails)) {
|
||||
log_message('warning', "SchoolCalendarController: no recipient emails for {$group}.");
|
||||
$results[$group] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
$ok = $this->emailService->sendBcc($emails, $subject, $body, 'general');
|
||||
if (!$ok) {
|
||||
log_message('warning', "SchoolCalendarController: failed to send calendar notification to {$group}.");
|
||||
}
|
||||
$results[$group] = $ok;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
protected function collectEmailsForGroup(string $group): array
|
||||
{
|
||||
switch ($group) {
|
||||
case 'parents':
|
||||
return $this->normalizeEmailAddresses($this->userModel->getParents());
|
||||
case 'teachers':
|
||||
return $this->normalizeEmailAddresses($this->teacherModel->getAllTeachers());
|
||||
case 'admins':
|
||||
return $this->normalizeEmailAddresses($this->fetchAdminRows());
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected function normalizeEmailAddresses(array $rows): array
|
||||
{
|
||||
$emails = [];
|
||||
foreach ($rows as $row) {
|
||||
$email = trim((string) ($row['email'] ?? ''));
|
||||
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
continue;
|
||||
}
|
||||
$emails[$email] = true;
|
||||
}
|
||||
return array_values(array_keys($emails));
|
||||
}
|
||||
|
||||
protected function fetchAdminRows(): array
|
||||
{
|
||||
$excluded = ['guest', 'teacher', 'teacher_assistant', 'parent'];
|
||||
$escaped = array_map(static fn($value) => "'" . addslashes($value) . "'", $excluded);
|
||||
$excludedList = implode(', ', $escaped);
|
||||
|
||||
$builder = $this->db->table('users u')
|
||||
->select("u.id, u.firstname, u.lastname, u.email, MAX(CASE WHEN LOWER(COALESCE(r.slug, r.name)) NOT IN({$excludedList}) THEN 1 ELSE 0 END) AS is_admin", false)
|
||||
->join('user_roles ur', 'ur.user_id = u.id')
|
||||
->join('roles r', 'r.id = ur.role_id')
|
||||
->where('r.is_active', 1)
|
||||
->where('u.email IS NOT NULL')
|
||||
->where('u.email !=', '')
|
||||
->groupBy('u.id, u.firstname, u.lastname, u.email')
|
||||
->having('is_admin', 1);
|
||||
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
protected function buildCalendarEmailSubject(array $event): string
|
||||
{
|
||||
$title = trim((string) ($event['title'] ?? 'School Calendar Update'));
|
||||
$typeSegment = !empty($event['event_type']) ? ' (' . $event['event_type'] . ')' : '';
|
||||
return 'School Calendar: ' . $title . $typeSegment;
|
||||
}
|
||||
|
||||
protected function buildEmailFlash(array $status): ?array
|
||||
{
|
||||
if (empty($status)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sent = [];
|
||||
$failed = [];
|
||||
$missing = [];
|
||||
|
||||
foreach ($status as $group => $result) {
|
||||
if ($result === true) {
|
||||
$sent[] = $group;
|
||||
} elseif ($result === false) {
|
||||
$failed[] = $group;
|
||||
} elseif ($result === null) {
|
||||
$missing[] = $group;
|
||||
}
|
||||
}
|
||||
|
||||
$parts = [];
|
||||
if (!empty($sent)) {
|
||||
$parts[] = 'Email sent to ' . $this->humanizeGroupList($sent) . '.';
|
||||
}
|
||||
if (!empty($failed)) {
|
||||
$parts[] = 'Sending failed for ' . $this->humanizeGroupList($failed) . '.';
|
||||
}
|
||||
if (!empty($missing)) {
|
||||
$parts[] = 'No recipient emails for ' . $this->humanizeGroupList($missing) . '.';
|
||||
}
|
||||
|
||||
if (empty($parts)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'message' => implode(' ', $parts),
|
||||
'class' => empty($failed) && empty($missing) ? 'info' : 'warning',
|
||||
];
|
||||
}
|
||||
|
||||
protected function humanizeGroupList(array $groups): string
|
||||
{
|
||||
$groups = array_map('strval', $groups);
|
||||
if (empty($groups)) {
|
||||
return '';
|
||||
}
|
||||
if (count($groups) === 1) {
|
||||
return $groups[0];
|
||||
}
|
||||
$last = array_pop($groups);
|
||||
return implode(', ', $groups) . ' and ' . $last;
|
||||
}
|
||||
|
||||
protected function buildCalendarViewUrl(array $filters = []): string
|
||||
{
|
||||
$allowed = ['school_year', 'semester'];
|
||||
$params = [];
|
||||
foreach ($allowed as $key) {
|
||||
$value = $filters[$key] ?? '';
|
||||
if ($value !== '') {
|
||||
$params[$key] = $value;
|
||||
}
|
||||
}
|
||||
$query = http_build_query($params);
|
||||
return '/administrator/calendar_view' . ($query !== '' ? ('?' . $query) : '');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
|
||||
// Check if the event exists
|
||||
$event = $this->calendarModel->find($id);
|
||||
if (!$event) {
|
||||
return redirect()->to('/administrator/calendar_view')->with('error', 'Event not found.');
|
||||
}
|
||||
|
||||
// Delete the event
|
||||
if ($this->calendarModel->delete($id)) {
|
||||
return redirect()->to('/administrator/calendar_view')->with('success', 'Event deleted successfully');
|
||||
} else {
|
||||
return redirect()->to('/administrator/calendar_view')->with('error', 'Failed to delete the event.');
|
||||
}
|
||||
}
|
||||
|
||||
public function calendarParentView()
|
||||
{
|
||||
// Transform the events into the format FullCalendar expects
|
||||
$formattedEvents = $this->calendarView('parent');
|
||||
|
||||
// Pass the formatted events to the view
|
||||
return view('/parent/calendar', ['events' => $formattedEvents]);
|
||||
}
|
||||
|
||||
public function calendarTeacherView()
|
||||
{
|
||||
// Transform the events into the format FullCalendar expects
|
||||
$formattedEvents = $this->calendarView('teacher');
|
||||
|
||||
// Pass the formatted events to the view
|
||||
return view('/teacher/calendar', ['events' => $formattedEvents]);
|
||||
}
|
||||
|
||||
public function calendarAdminView()
|
||||
{
|
||||
// Transform the events into the format FullCalendar expects
|
||||
$formattedEvents = $this->calendarView('admin');
|
||||
|
||||
// Pass the formatted events to the view
|
||||
return view('administrator/calendar', ['events' => $formattedEvents]);
|
||||
}
|
||||
|
||||
private function calendarView(?string $audience = null)
|
||||
{
|
||||
// Filter events by school year
|
||||
$events = $this->calendarModel->getEventsBySchoolYear($this->schoolYear);
|
||||
|
||||
// Apply audience visibility rules:
|
||||
// - If no checkboxes selected (all notify_* = 0) => visible to everyone
|
||||
// - Otherwise visible only to the selected audience
|
||||
if (in_array($audience, ['parent', 'teacher', 'admin'], true)) {
|
||||
$events = array_values(array_filter($events, function ($event) use ($audience) {
|
||||
$p = (int) ($event['notify_parent'] ?? 0);
|
||||
$t = (int) ($event['notify_teacher'] ?? 0);
|
||||
$a = (int) ($event['notify_admin'] ?? 0);
|
||||
$ns = (int) ($event['no_school'] ?? 0);
|
||||
$hasAny = ($p + $t + $a) > 0;
|
||||
|
||||
// If marked as no_school, it should be visible to all audiences
|
||||
if ($ns === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$hasAny) {
|
||||
// No audience explicitly selected: show to everyone
|
||||
return true;
|
||||
}
|
||||
|
||||
// Audience explicitly selected: restrict to matching audience
|
||||
if ($audience === 'parent') { return $p === 1; }
|
||||
if ($audience === 'teacher') { return $t === 1; }
|
||||
if ($audience === 'admin') { return $a === 1; }
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
|
||||
// Format for FullCalendar
|
||||
$aud = $audience; // capture for closure
|
||||
$formattedEvents = array_map(function ($event) use ($aud) {
|
||||
$p = (int) ($event['notify_parent'] ?? 0);
|
||||
$t = (int) ($event['notify_teacher'] ?? 0);
|
||||
$a = (int) ($event['notify_admin'] ?? 0);
|
||||
$ns = (int) ($event['no_school'] ?? 0);
|
||||
|
||||
$backgroundColor = null;
|
||||
// Color no-school events yellow on teacher and parent calendars
|
||||
if (in_array($aud, ['teacher', 'parent'], true) && $ns === 1) {
|
||||
$backgroundColor = '#ffc107'; // yellow
|
||||
}
|
||||
|
||||
// Highlight events dedicated to staff (teachers/admins) only in green
|
||||
// Definition: visible to staff but not to parents, and not a no-school marker
|
||||
if (in_array($aud, ['teacher', 'admin'], true) && $backgroundColor === null) {
|
||||
$isStaffOnly = ($p === 0 && ($t === 1 || $a === 1) && $ns === 0);
|
||||
if ($isStaffOnly) {
|
||||
// Use a distinct green to differentiate from the default blue
|
||||
$backgroundColor = '#28a745';
|
||||
}
|
||||
}
|
||||
|
||||
// Title-casing for "no school" phrase when marked as no_school
|
||||
$title = $event['title'] ?? 'Untitled';
|
||||
if ($ns === 1 && is_string($title)) {
|
||||
// Replace common variants with Title Case
|
||||
$title = preg_replace('/\bno\s*[- ]\s*school\b/i', 'No School', $title);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $event['id'],
|
||||
'title' => $title,
|
||||
'start' => $event['date'] ?? '', // FullCalendar expects 'start'
|
||||
'description' => $event['description'] ?? '',
|
||||
'extendedProps' => [
|
||||
'semester' => $event['semester'] ?? '',
|
||||
'school_year' => $event['school_year'] ?? '',
|
||||
'notify_parent' => $p,
|
||||
'notify_teacher' => $t,
|
||||
'notify_admin' => $a,
|
||||
'no_school' => $ns,
|
||||
'backgroundColor' => $backgroundColor,
|
||||
]
|
||||
];
|
||||
}, $events);
|
||||
|
||||
// Append parent meeting schedules
|
||||
$meetingEvents = $this->formatMeetingEvents($audience);
|
||||
if (!empty($meetingEvents)) {
|
||||
$formattedEvents = array_values(array_merge($formattedEvents, $meetingEvents));
|
||||
}
|
||||
|
||||
return $formattedEvents;
|
||||
}
|
||||
|
||||
private function formatMeetingEvents(?string $audience = null): array
|
||||
{
|
||||
$aud = $audience ?? '';
|
||||
$builder = $this->meetingModel
|
||||
->where('school_year', $this->schoolYear)
|
||||
->groupStart()
|
||||
->where('status', 'scheduled')
|
||||
->orWhere('status', 'Scheduled')
|
||||
->orWhere('status', '')
|
||||
->orWhere('status', null)
|
||||
->groupEnd();
|
||||
|
||||
if ($aud === 'parent') {
|
||||
$parentUserId = (int) (session()->get('user_id') ?? 0);
|
||||
if ($parentUserId <= 0) {
|
||||
return [];
|
||||
}
|
||||
$studentIds = $this->getStudentIdsForParent($parentUserId);
|
||||
if (!empty($studentIds)) {
|
||||
$builder = $builder->groupStart()
|
||||
->where('parent_user_id', $parentUserId)
|
||||
->orWhereIn('student_id', $studentIds)
|
||||
->groupEnd();
|
||||
} else {
|
||||
$builder = $builder->where('parent_user_id', $parentUserId);
|
||||
}
|
||||
}
|
||||
|
||||
$rows = $builder->orderBy('date', 'ASC')->findAll();
|
||||
if (empty($rows)) {
|
||||
// Fallback: show any meetings if school_year was missing/mismatched
|
||||
$fallback = $this->meetingModel
|
||||
->groupStart()
|
||||
->where('status', 'scheduled')
|
||||
->orWhere('status', 'Scheduled')
|
||||
->orWhere('status', '')
|
||||
->orWhere('status', null)
|
||||
->groupEnd();
|
||||
|
||||
if ($aud === 'parent') {
|
||||
$parentUserId = (int) (session()->get('user_id') ?? 0);
|
||||
if ($parentUserId > 0) {
|
||||
$studentIds = $this->getStudentIdsForParent($parentUserId);
|
||||
if (!empty($studentIds)) {
|
||||
$fallback = $fallback->groupStart()
|
||||
->where('parent_user_id', $parentUserId)
|
||||
->orWhereIn('student_id', $studentIds)
|
||||
->groupEnd();
|
||||
} else {
|
||||
$fallback = $fallback->where('parent_user_id', $parentUserId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rows = $fallback->orderBy('date', 'ASC')->findAll();
|
||||
}
|
||||
if (empty($rows)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(function ($row) use ($aud) {
|
||||
$parentName = (string)($row['parent_name'] ?? 'Parent');
|
||||
$studentName = (string)($row['student_name'] ?? 'Student');
|
||||
$time = trim((string)($row['time'] ?? ''));
|
||||
$timeLabel = $time !== '' ? (' ' . $time) : '';
|
||||
|
||||
if ($aud === 'admin') {
|
||||
$title = 'Admin Meeting';
|
||||
} else {
|
||||
$title = 'Parent Meeting';
|
||||
}
|
||||
|
||||
$descParts = [];
|
||||
$descParts[] = 'Student: ' . $studentName;
|
||||
$descParts[] = 'Parent: ' . $parentName;
|
||||
if (!empty($row['class_section_name'])) {
|
||||
$descParts[] = 'Section: ' . $row['class_section_name'];
|
||||
}
|
||||
$descParts[] = 'Date/Time: ' . ($row['date'] ?? '') . $timeLabel;
|
||||
if (!empty($row['notes'])) {
|
||||
$descParts[] = 'Notes: ' . $row['notes'];
|
||||
}
|
||||
|
||||
$start = (string)($row['date'] ?? '');
|
||||
if ($start !== '' && $time !== '') {
|
||||
$start = $start . 'T' . $time;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => 'pm-' . (int)($row['id'] ?? 0),
|
||||
'title' => $title,
|
||||
'start' => $start,
|
||||
'description' => implode("\n", $descParts),
|
||||
'extendedProps' => [
|
||||
'semester' => $row['semester'] ?? '',
|
||||
'school_year' => $row['school_year'] ?? '',
|
||||
'notify_parent' => 1,
|
||||
'notify_admin' => 1,
|
||||
'notify_teacher' => 0,
|
||||
'no_school' => 0,
|
||||
'backgroundColor' => '#6f42c1',
|
||||
],
|
||||
];
|
||||
}, $rows);
|
||||
}
|
||||
|
||||
private function getStudentIdsForParent(int $parentUserId): array
|
||||
{
|
||||
try {
|
||||
$rows = $this->db->query(
|
||||
"SELECT DISTINCT fs.student_id
|
||||
FROM family_guardians fg
|
||||
JOIN family_students fs ON fs.family_id = fg.family_id
|
||||
WHERE fg.user_id = ?",
|
||||
[$parentUserId]
|
||||
)->getResultArray();
|
||||
$ids = array_map(static fn($r) => (int)($r['student_id'] ?? 0), $rows);
|
||||
|
||||
// Legacy fallback: students.parent_id
|
||||
$rows2 = $this->db->query(
|
||||
"SELECT id AS student_id
|
||||
FROM students
|
||||
WHERE parent_id = ?",
|
||||
[$parentUserId]
|
||||
)->getResultArray();
|
||||
foreach ($rows2 as $r) {
|
||||
$ids[] = (int)($r['student_id'] ?? 0);
|
||||
}
|
||||
|
||||
return array_values(array_filter($ids, static fn($id) => $id > 0));
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,54 +0,0 @@
|
||||
Major U.S. Providers:
|
||||
|
||||
AT&T
|
||||
SMS: <10-digit-number>@txt.att.net
|
||||
MMS: <10-digit-number>@mms.att.net
|
||||
|
||||
Verizon
|
||||
SMS: <10-digit-number>@vtext.com
|
||||
MMS: <10-digit-number>@vzwpix.com
|
||||
|
||||
T-Mobile
|
||||
SMS: <10-digit-number>@tmomail.net
|
||||
MMS: <10-digit-number>@tmomail.net
|
||||
|
||||
Sprint (now merged with T-Mobile, but some legacy users may still use Sprint domains)
|
||||
SMS: <10-digit-number>@messaging.sprintpcs.com
|
||||
MMS: <10-digit-number>@pm.sprint.com
|
||||
|
||||
Boost Mobile
|
||||
SMS: <10-digit-number>@sms.myboostmobile.com
|
||||
MMS: <10-digit-number>@myboostmobile.com
|
||||
|
||||
U.S. Cellular
|
||||
SMS: <10-digit-number>@email.uscc.net
|
||||
MMS: <10-digit-number>@mms.uscc.net
|
||||
|
||||
Metro by T-Mobile (formerly MetroPCS)
|
||||
SMS: <10-digit-number>@mymetropcs.com
|
||||
MMS: <10-digit-number>@mymetropcs.com
|
||||
|
||||
Cricket Wireless
|
||||
SMS: <10-digit-number>@sms.cricketwireless.net
|
||||
MMS: <10-digit-number>@mms.cricketwireless.net
|
||||
|
||||
Google Fi
|
||||
SMS/MMS: <10-digit-number>@msg.fi.google.com
|
||||
|
||||
Virgin Mobile USA
|
||||
SMS: <10-digit-number>@vmobl.com
|
||||
MMS: <10-digit-number>@vmpix.com
|
||||
|
||||
|
||||
|
||||
Carrier SMS Gateway MMS Gateway
|
||||
AT&T number@txt.att.net number@mms.att.net
|
||||
Verizon number@vtext.com number@vzwpix.com
|
||||
T-Mobile number@tmomail.net number@tmomail.net
|
||||
Sprint number@messaging.sprintpcs.com number@pm.sprint.com
|
||||
Boost Mobile number@sms.myboostmobile.com number@myboostmobile.com
|
||||
Cricket number@sms.cricketwireless.net number@mms.cricketwireless.net
|
||||
US Cellular number@email.uscc.net number@mms.uscc.net
|
||||
Google Fi number@msg.fi.google.com number@msg.fi.google.com
|
||||
Virgin Mobile number@vmobl.com number@vmpix.com
|
||||
Metro by T-Mobile number@mymetropcs.com number@mymetropcs.com
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
use App\Controllers\BaseController;
|
||||
use Config\SessionTimeout;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
|
||||
class SessionTimeoutController extends BaseController
|
||||
{
|
||||
use ResponseTrait;
|
||||
|
||||
public function getTimeoutConfig()
|
||||
{
|
||||
return $this->response->setJSON([
|
||||
'success' => true,
|
||||
'timeout' => SessionTimeout::TIMEOUT_DURATION,
|
||||
'warning_time' => SessionTimeout::WARNING_THRESHOLD,
|
||||
'check_interval' => SessionTimeout::CHECK_INTERVAL,
|
||||
'logout_url' => site_url('auth/logout'),
|
||||
'keep_alive_url' => site_url('session/pingActivity'),
|
||||
'check_url' => site_url('session/checkTimeout')
|
||||
]);
|
||||
}
|
||||
|
||||
public function checkTimeout()
|
||||
{
|
||||
$session = session();
|
||||
|
||||
// Verify session exists and has last_activity
|
||||
if (!$session->has('last_activity')) {
|
||||
return $this->expireSession();
|
||||
}
|
||||
|
||||
$lastActivity = $session->get('last_activity');
|
||||
$elapsed = time() - $lastActivity;
|
||||
|
||||
if ($elapsed > SessionTimeout::TIMEOUT_DURATION) {
|
||||
return $this->expireSession();
|
||||
} elseif ($elapsed > SessionTimeout::WARNING_THRESHOLD) {
|
||||
return $this->response->setJSON([
|
||||
'status' => 'warning',
|
||||
'time_remaining' => SessionTimeout::TIMEOUT_DURATION - $elapsed
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 'active',
|
||||
'time_remaining' => SessionTimeout::TIMEOUT_DURATION - $elapsed
|
||||
]);
|
||||
}
|
||||
|
||||
public function pingActivity()
|
||||
{
|
||||
$session = session();
|
||||
|
||||
// Only update if session is still valid
|
||||
if (!$session->has('last_activity') ||
|
||||
(time() - $session->get('last_activity') > SessionTimeout::TIMEOUT_DURATION)) {
|
||||
return $this->expireSession();
|
||||
}
|
||||
|
||||
$session->set('last_activity', time());
|
||||
return $this->response->setJSON([
|
||||
'status' => 'active',
|
||||
'time_remaining' => SessionTimeout::TIMEOUT_DURATION
|
||||
]);
|
||||
}
|
||||
|
||||
private function expireSession()
|
||||
{
|
||||
$this->destroySession();
|
||||
return $this->response->setJSON([
|
||||
'status' => 'expired',
|
||||
'redirect' => site_url('login'),
|
||||
'message' => 'Your session has expired due to inactivity.'
|
||||
]);
|
||||
}
|
||||
|
||||
private function destroySession()
|
||||
{
|
||||
$session = session();
|
||||
$session->setFlashdata('error', 'Your session has expired due to inactivity.');
|
||||
|
||||
// Clear session data
|
||||
$session->remove('last_activity');
|
||||
$session->destroy();
|
||||
|
||||
// Regenerate session ID for security
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\SettingsModel;
|
||||
|
||||
class SettingsController extends BaseController
|
||||
{
|
||||
protected $settingsModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->settingsModel = new SettingsModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
// Retrieve current settings from the database
|
||||
$settings = $this->settingsModel->getSettings();
|
||||
$timezones = timezone_identifiers_list();
|
||||
|
||||
// Pass the settings and timezones to the view
|
||||
return view('administrator/settings', ['settings' => $settings, 'timezones' => $timezones]);
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
// Handle the form submission to update settings
|
||||
$data = [
|
||||
'site_name' => $this->request->getPost('site_name'),
|
||||
'administrator_email' => strtolower($this->request->getPost('administrator_email')),
|
||||
'timezone' => $this->request->getPost('timezone'),
|
||||
];
|
||||
$this->settingsModel->updateSettings($data);
|
||||
return redirect()->to('/administrator/settings');
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\StaffModel;
|
||||
use App\Models\UserModel;
|
||||
use App\Models\TeacherClassModel;
|
||||
use App\Models\ConfigurationModel;
|
||||
|
||||
class StaffController extends BaseController
|
||||
{
|
||||
protected $configModel;
|
||||
protected $semester;
|
||||
protected $schoolYear;
|
||||
protected $teacherClassModel;
|
||||
protected $userModel;
|
||||
protected $staffModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Initialize the config model
|
||||
$this->configModel = new ConfigurationModel(); // Assuming ConfigModel is the model handling configurations
|
||||
$this->teacherClassModel = new TeacherClassModel();
|
||||
$this->userModel = new UserModel();
|
||||
$this->staffModel = new StaffModel();
|
||||
|
||||
// Retrieve the configuration values
|
||||
$this->semester = $this->configModel->getConfig('semester');
|
||||
$this->schoolYear = $this->configModel->getConfig('school_year');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
// roles we never show
|
||||
$excludedRoles = ['student', 'parent', 'guest', 'inactive']; // ← add inactive here
|
||||
|
||||
$staffList = $this->staffModel
|
||||
->whereNotIn('LOWER(active_role)', array_map('strtolower', $excludedRoles))
|
||||
->orderBy('created_at', 'DESC')
|
||||
->findAll();
|
||||
|
||||
// Preload assignments for the selected school year (across all semesters),
|
||||
// mirroring the logic used in teacher_class_assignment.
|
||||
$assignRows = $this->teacherClassModel
|
||||
->select('teacher_class.teacher_id, teacher_class.position, classSection.class_section_name')
|
||||
->join('classSection', 'classSection.class_section_id = teacher_class.class_section_id', 'left')
|
||||
->where('teacher_class.school_year', (string)$this->schoolYear)
|
||||
->findAll();
|
||||
|
||||
$assignByTeacher = [];
|
||||
foreach ($assignRows as $r) {
|
||||
$tid = (int)($r['teacher_id'] ?? 0);
|
||||
if ($tid <= 0) { continue; }
|
||||
$name = trim((string)($r['class_section_name'] ?? ''));
|
||||
if ($name === '') { continue; }
|
||||
$pos = strtolower((string)($r['position'] ?? ''));
|
||||
if (!in_array($pos, ['main','ta'], true)) { continue; }
|
||||
$assignByTeacher[$tid][] = $name . ' (' . $pos . ')';
|
||||
}
|
||||
|
||||
$issuesCount = 0;
|
||||
foreach ($staffList as &$staff) {
|
||||
// attach school_id
|
||||
$staff['school_id'] = $this->userModel->getSchoolIdByUserId($staff['user_id'] ?? null);
|
||||
|
||||
// Verify and attach class assignments for teacher/TA roles for the selected school year
|
||||
$role = strtolower((string)($staff['active_role'] ?? ''));
|
||||
if (in_array($role, ['teacher', 'teacher_assistant'], true)) {
|
||||
$tid = (int)($staff['user_id'] ?? 0);
|
||||
$labels = $assignByTeacher[$tid] ?? [];
|
||||
if (!empty($labels)) {
|
||||
$staff['class_section'] = implode(', ', array_unique($labels));
|
||||
$staff['verification_issue'] = false;
|
||||
} else {
|
||||
$staff['class_section'] = 'No class assigned';
|
||||
$staff['verification_issue'] = true;
|
||||
$issuesCount++;
|
||||
}
|
||||
} else {
|
||||
$staff['class_section'] = '—';
|
||||
$staff['verification_issue'] = false;
|
||||
}
|
||||
}
|
||||
unset($staff); // break reference
|
||||
|
||||
return view('staff/index', [
|
||||
'staff' => $staffList,
|
||||
'issues_count' => $issuesCount,
|
||||
'semester' => $this->semester,
|
||||
'school_year' => $this->schoolYear,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('staff/create');
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$now = utc_now();
|
||||
$payload = [
|
||||
'firstname' => $this->request->getPost('firstname'),
|
||||
'lastname' => $this->request->getPost('lastname'),
|
||||
'email' => $this->request->getPost('email'),
|
||||
'phone' => $this->request->getPost('phone'),
|
||||
'role_name' => $this->request->getPost('role_name'),
|
||||
'school_year' => $this->request->getPost('school_year') ?: $this->schoolYear,
|
||||
'status' => $this->request->getPost('status') ?? 'Active',
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
|
||||
if (!$this->staffModel->insert($payload)) {
|
||||
return redirect()->back()->withInput()->with('error', 'Failed to create staff.');
|
||||
}
|
||||
|
||||
return redirect()->to('/staff')->with('success', 'Staff member added.');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$user = $this->staffModel->find($id);
|
||||
|
||||
if (!$user) {
|
||||
return redirect()->to('staff')->with('error', 'Staff member not found.');
|
||||
}
|
||||
|
||||
return view('staff/edit', ['user' => $user]);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
// Only update fields provided by the form to avoid wiping data
|
||||
$data = [];
|
||||
foreach (['firstname','lastname','email','phone','role_name','school_year','status'] as $field) {
|
||||
$val = $this->request->getPost($field);
|
||||
if ($val !== null && $val !== '') {
|
||||
$data[$field] = $val;
|
||||
}
|
||||
}
|
||||
// Always bump updated_at
|
||||
$data['updated_at'] = utc_now();
|
||||
|
||||
if (empty($data)) {
|
||||
return redirect()->back()->with('error', 'No changes detected.');
|
||||
}
|
||||
|
||||
if (!$this->staffModel->update($id, $data)) {
|
||||
return redirect()->back()->withInput()->with('error', 'Failed to update staff.');
|
||||
}
|
||||
|
||||
return redirect()->to('/staff')->with('success', 'Staff member updated.');
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Models\StatsModel;
|
||||
use App\Controllers\BaseController;
|
||||
|
||||
class StatsController extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$model = new StatsModel();
|
||||
$data['stats'] = $model->getStats();
|
||||
|
||||
return view('stats_view', $data);
|
||||
}
|
||||
|
||||
public function calendar()
|
||||
{
|
||||
return view('/calendars');
|
||||
}
|
||||
|
||||
public function support()
|
||||
{
|
||||
return view('/support');
|
||||
}
|
||||
|
||||
public function contactUs()
|
||||
{
|
||||
return view('/contact_us');
|
||||
}
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
class StickersController extends PrintablesBaseController
|
||||
{
|
||||
public function stickerForm()
|
||||
{
|
||||
$data['students'] = $this->studentModel->findAll();
|
||||
$data['classes'] = $this->classSectionModel->findAll();
|
||||
$data['stickers'] = $this->stickerPresets();
|
||||
return view('printables_reports/sticker_form', $data);
|
||||
}
|
||||
|
||||
public function getByClass($classId)
|
||||
{
|
||||
$builder = $this->db->table('student_class sc')
|
||||
->select('s.id, s.firstname, s.lastname, s.registration_grade, s.gender')
|
||||
->join('students s', 's.id = sc.student_id')
|
||||
->where('sc.class_section_id', $classId)
|
||||
->where('sc.school_year', $this->schoolYear)
|
||||
->orderBy('s.lastname', 'asc');
|
||||
|
||||
$students = $builder->get()->getResultArray();
|
||||
|
||||
return $this->response->setJSON($students);
|
||||
}
|
||||
|
||||
/** Preset sticker sizes for the view (robust keys) */
|
||||
private function stickerPresets(): array
|
||||
{
|
||||
$rows = [
|
||||
['value' => '100x24.5', 'label' => 'Xsmall - 100x24.5 mm', 'ppg' => 20],
|
||||
['value' => '60x30', 'label' => 'Small - 60x30 mm', 'ppg' => 24],
|
||||
['value' => '102x34', 'label' => 'Medium - 102x34 mm', 'ppg' => 14],
|
||||
['value' => '90x45', 'label' => 'Large - 90x45 mm', 'ppg' => 10],
|
||||
];
|
||||
|
||||
// Add aliases so different views will not break (title/text/per_page)
|
||||
return array_map(static function ($r) {
|
||||
$r['title'] = $r['title'] ?? ($r['label'] ?? ($r['value'] . ' mm'));
|
||||
$r['text'] = $r['text'] ?? $r['title'];
|
||||
$r['per_page'] = $r['per_page'] ?? ($r['ppg'] ?? null);
|
||||
return $r;
|
||||
}, $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate name stickers as a PDF.
|
||||
*
|
||||
* Can be used for different sticker sizes/sheets by passing parameters
|
||||
* via function args (from routes) or via GET/POST form fields.
|
||||
*
|
||||
* Accepted request params (POST takes precedence over GET):
|
||||
* - class_id : int
|
||||
* - student_id : int
|
||||
* - print_all : 0|1
|
||||
* - sticker_size : string like "102x34" (accepts upper/lower case X or the unicode multiply sign)
|
||||
* - sticker_width : number (mm)
|
||||
* - sticker_height : number (mm)
|
||||
* - stickers_per_page : int (optional cap; grid still derived from size/margins)
|
||||
* - page_size : "A4" (default) | "Letter" | any FPDF size token
|
||||
* - orientation : "P" (default) | "L"
|
||||
* - margin_left : number (mm)
|
||||
* - margin_top : number (mm)
|
||||
* - margin_right : number (mm; defaults to margin_left)
|
||||
* - margin_bottom : number (mm; default 10)
|
||||
* - gap_x : number (mm; horizontal gap between stickers)
|
||||
* - gap_y : number (mm; vertical gap between stickers)
|
||||
* - copies[<student_id>] : int per-student copies in batch mode
|
||||
* - single_copies : int copies for single student mode
|
||||
*/
|
||||
public function generateStickers(
|
||||
?int $studentId = null,
|
||||
?float $stickerWidthArg = null,
|
||||
?float $stickerHeightArg = null,
|
||||
?int $stickersPerPageArg = null,
|
||||
?string $pageSizeArg = null,
|
||||
?string $orientationArg = null,
|
||||
?float $marginLeftArg = null,
|
||||
?float $marginTopArg = null,
|
||||
?float $marginRightArg = null,
|
||||
?float $marginBottomArg = null,
|
||||
?float $gapXArg = null,
|
||||
?float $gapYArg = null
|
||||
) {
|
||||
$request = service('request');
|
||||
$method = strtolower($request->getMethod());
|
||||
$classId = $request->getPost('class_id') ?: $request->getGet('class_id');
|
||||
$studentId = $studentId ?? ($request->getPost('student_id') ?: $request->getGet('student_id'));
|
||||
|
||||
// Safety cast
|
||||
$classId = $classId !== null ? (int) $classId : null;
|
||||
$studentId = $studentId !== null ? (int) $studentId : null;
|
||||
|
||||
// Sanity log for school year
|
||||
if (empty($this->schoolYear)) {
|
||||
log_message('error', 'generateStickers(): $this->schoolYear is empty. Load it from ConfigurationModel.');
|
||||
} else {
|
||||
log_message('debug', 'generateStickers(): schoolYear=' . $this->schoolYear);
|
||||
}
|
||||
|
||||
// Load classes that actually have students in this school year
|
||||
$classes = $this->classSectionModel
|
||||
->select('classSection.class_section_id, classSection.class_section_name')
|
||||
->join('student_class', 'student_class.class_section_id = classSection.class_section_id', 'inner')
|
||||
->where('student_class.school_year', $this->schoolYear)
|
||||
->groupBy('classSection.class_section_id, classSection.class_section_name')
|
||||
->orderBy('classSection.class_section_name', 'ASC')
|
||||
->findAll();
|
||||
|
||||
// ---------- GET: render form ----------
|
||||
if ($method === 'get') {
|
||||
$students = [];
|
||||
|
||||
if (!empty($classId)) {
|
||||
$students = $this->fetchStudentsByClass((int) $classId, $this->schoolYear);
|
||||
} else {
|
||||
$students = $this->studentModel
|
||||
->select('students.id, students.firstname, students.lastname')
|
||||
->join('student_class', 'student_class.student_id = students.id', 'inner')
|
||||
->where('student_class.school_year', $this->schoolYear)
|
||||
->groupBy('students.id, students.firstname, students.lastname')
|
||||
->orderBy('students.firstname', 'ASC')
|
||||
->orderBy('students.lastname', 'ASC')
|
||||
->findAll(500);
|
||||
}
|
||||
|
||||
// Presets (now with robust keys expected by the view)
|
||||
$presetRows = [
|
||||
['value' => '60x30', 'label' => 'Small - 60x30 mm', 'ppg' => 24],
|
||||
['value' => '102x34', 'label' => 'Medium - 102x34 mm', 'ppg' => 14],
|
||||
['value' => '90x45', 'label' => 'Large - 90x45 mm', 'ppg' => 10],
|
||||
];
|
||||
// Normalize to include 'title', 'text', and 'per_page' so views using any of those will not error.
|
||||
$stickers = array_map(static function (array $r): array {
|
||||
$r['title'] = $r['title'] ?? ($r['label'] ?? ($r['value'] . ' mm'));
|
||||
$r['text'] = $r['text'] ?? $r['title'];
|
||||
$r['per_page'] = $r['per_page'] ?? ($r['ppg'] ?? null);
|
||||
return $r;
|
||||
}, $presetRows);
|
||||
|
||||
return view('printables_reports/sticker_form', [
|
||||
'classes' => $classes,
|
||||
'students' => $students,
|
||||
'stickers' => $stickers,
|
||||
]);
|
||||
}
|
||||
|
||||
// ---------- POST: build student list ----------
|
||||
$printAll = (int) ($request->getPost('print_all') ?? $request->getGet('print_all') ?? 0) === 1;
|
||||
|
||||
$students = [];
|
||||
if (!empty($studentId)) {
|
||||
$student = $this->studentModel->find((int) $studentId);
|
||||
if (!$student) {
|
||||
return 'Student not found.';
|
||||
}
|
||||
$students = [$student];
|
||||
} elseif (!empty($classId)) {
|
||||
$students = $this->fetchStudentsByClass((int) $classId, $this->schoolYear);
|
||||
if (empty($students)) {
|
||||
return 'No students found in this class.';
|
||||
}
|
||||
} elseif ($printAll) {
|
||||
$students = $this->studentModel
|
||||
->select('students.id, students.firstname, students.lastname, sc.class_section_id, cs.class_section_name, c.id AS class_id')
|
||||
->join('student_class sc', 'sc.student_id = students.id', 'inner')
|
||||
->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'inner')
|
||||
->join('classes c', 'c.id = cs.class_id', 'inner')
|
||||
->where('sc.school_year', $this->schoolYear)
|
||||
->groupBy('students.id, students.firstname, students.lastname, sc.class_section_id, cs.class_section_name, c.id')
|
||||
->orderBy('c.id', 'ASC')
|
||||
->orderBy('cs.class_section_name', 'ASC')
|
||||
->orderBy('students.firstname', 'ASC')
|
||||
->orderBy('students.lastname', 'ASC')
|
||||
->findAll(5000);
|
||||
|
||||
// Exclude Youth and KG
|
||||
$students = array_values(array_filter($students, static function ($s) {
|
||||
$g = trim((string) ($s['class_section_name'] ?? ''));
|
||||
return $g !== '' && !preg_match('/^(youth|kg)(?:\b|-)/i', $g);
|
||||
}));
|
||||
|
||||
if (empty($students)) {
|
||||
return 'No students found for this school year.';
|
||||
}
|
||||
} else {
|
||||
return 'Please select a student or class.';
|
||||
}
|
||||
|
||||
// ---- Expand list according to per-student copy counts ----
|
||||
$copiesPosted = $request->getPost('copies'); // array|null
|
||||
$singleCopies = (int) ($request->getPost('single_copies') ?? $request->getGet('single_copies') ?? 1);
|
||||
if ($singleCopies < 0) $singleCopies = 0;
|
||||
|
||||
$printList = [];
|
||||
if (!empty($studentId)) {
|
||||
$qty = max(0, $singleCopies);
|
||||
for ($k = 0; $k < $qty; $k++) {
|
||||
$printList[] = $students[0];
|
||||
}
|
||||
} else {
|
||||
$hasCopies = is_array($copiesPosted);
|
||||
$defaultQty = $hasCopies ? 0 : 1; // if copies[] present, only print IDs provided
|
||||
foreach ($students as $stu) {
|
||||
$id = (int) ($stu['id'] ?? 0);
|
||||
$qty = $defaultQty;
|
||||
if ($hasCopies && array_key_exists((string)$id, $copiesPosted)) {
|
||||
$qty = (int) $copiesPosted[(string)$id];
|
||||
}
|
||||
$qty = max(0, $qty);
|
||||
for ($k = 0; $k < $qty; $k++) {
|
||||
$printList[] = $stu;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (empty($printList)) {
|
||||
return 'Nothing to print. All quantities are zero.';
|
||||
}
|
||||
|
||||
// ---------- Layout parameters (request/args/defaults) ----------
|
||||
$sizeStrReq = trim((string) ($request->getPost('sticker_size') ?? $request->getGet('sticker_size') ?? ''));
|
||||
$stickerWidth = $stickerWidthArg ?? (float) ($request->getPost('sticker_width') ?? $request->getGet('sticker_width') ?? 0);
|
||||
$stickerHeight = $stickerHeightArg ?? (float) ($request->getPost('sticker_height') ?? $request->getGet('sticker_height') ?? 0);
|
||||
|
||||
// parse size string if provided (supports decimals; accepts x/X/unicode multiply)
|
||||
if (($stickerWidth <= 0 || $stickerHeight <= 0) && $sizeStrReq !== '') {
|
||||
if (preg_match('/^\\s*(\\d+(?:\\.\\d+)?)\\s*[xX\\x{00D7}]\\s*(\\d+(?:\\.\\d+)?)\\s*$/u', $sizeStrReq, $m)) {
|
||||
$stickerWidth = (float) $m[1];
|
||||
$stickerHeight = (float) $m[2];
|
||||
}
|
||||
}
|
||||
|
||||
// fallbacks to controller properties or sane defaults (mm)
|
||||
if ($stickerWidth <= 0) {
|
||||
$stickerWidth = isset($this->stickerWidth) ? (float)$this->stickerWidth : 102.0;
|
||||
}
|
||||
if ($stickerHeight <= 0) {
|
||||
$stickerHeight = isset($this->stickerHeight) ? (float)$this->stickerHeight : 34.0;
|
||||
}
|
||||
|
||||
$stickersPerPageReq = $stickersPerPageArg ?? (int) ($request->getPost('stickers_per_page') ?? $request->getGet('stickers_per_page') ?? 0);
|
||||
$pageSize = $pageSizeArg ?? (string) ($request->getPost('page_size') ?? $request->getGet('page_size') ?? 'A4');
|
||||
$orientation = $orientationArg ?? (string) ($request->getPost('orientation') ?? $request->getGet('orientation') ?? 'P');
|
||||
|
||||
$marginLeftProvided = $marginLeftArg !== null || $request->getPost('margin_left') !== null || $request->getGet('margin_left') !== null;
|
||||
$marginTopProvided = $marginTopArg !== null || $request->getPost('margin_top') !== null || $request->getGet('margin_top') !== null;
|
||||
$marginRightProvided = $marginRightArg !== null || $request->getPost('margin_right') !== null || $request->getGet('margin_right') !== null;
|
||||
$marginBottomProvided = $marginBottomArg !== null || $request->getPost('margin_bottom') !== null || $request->getGet('margin_bottom') !== null;
|
||||
$gapXProvided = $gapXArg !== null || $request->getPost('gap_x') !== null || $request->getGet('gap_x') !== null;
|
||||
$gapYProvided = $gapYArg !== null || $request->getPost('gap_y') !== null || $request->getGet('gap_y') !== null;
|
||||
|
||||
$marginLeft = $marginLeftArg ?? (float) ($request->getPost('margin_left') ?? $request->getGet('margin_left') ?? ($this->marginL ?? 6));
|
||||
$marginTop = $marginTopArg ?? (float) ($request->getPost('margin_top') ?? $request->getGet('margin_top') ?? ($this->marginT ?? 6));
|
||||
$marginRight = $marginRightArg ?? (float) ($request->getPost('margin_right') ?? $request->getGet('margin_right') ?? $marginLeft);
|
||||
$marginBottom = $marginBottomArg ?? (float) ($request->getPost('margin_bottom') ?? $request->getGet('margin_bottom') ?? 10);
|
||||
|
||||
$gapX = $gapXArg ?? (float) ($request->getPost('gap_x') ?? $request->getGet('gap_x') ?? ($this->gapX ?? 0));
|
||||
$gapY = $gapYArg ?? (float) ($request->getPost('gap_y') ?? $request->getGet('gap_y') ?? ($this->gapY ?? 0));
|
||||
|
||||
// If using the 100x24.5 preset (small labels) and nothing custom was provided,
|
||||
// default to tight margins and zero gaps to fit 20 per page (2 columns * 10+ rows).
|
||||
$normalizedSize = strtolower(str_replace(['×', ' '], ['x', ''], $sizeStrReq));
|
||||
$isXSmallPreset = $normalizedSize === '100x24.5' || (abs($stickerWidth - 100.0) < 0.6 && abs($stickerHeight - 24.5) < 0.6);
|
||||
if ($isXSmallPreset) {
|
||||
if (!$marginLeftProvided) {
|
||||
$marginLeft = 4.0;
|
||||
if (!$marginRightProvided) {
|
||||
$marginRight = 4.0;
|
||||
}
|
||||
}
|
||||
if (!$marginRightProvided && $marginRight < 4.0) {
|
||||
$marginRight = 4.0;
|
||||
}
|
||||
if (!$gapXProvided) {
|
||||
$gapX = 0.0;
|
||||
}
|
||||
if (!$gapYProvided) {
|
||||
$gapY = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- FPDF setup ----------
|
||||
$pdf = new \FPDF($orientation, 'mm', $pageSize);
|
||||
$pdf->SetMargins($marginLeft, $marginTop, $marginRight);
|
||||
$pdf->SetAutoPageBreak(true, $marginBottom);
|
||||
$pdf->AddPage();
|
||||
$pdf->SetFont('Arial', '', 12);
|
||||
|
||||
// Printable area
|
||||
$pageW = (float) $pdf->GetPageWidth();
|
||||
$pageH = (float) $pdf->GetPageHeight();
|
||||
|
||||
$usableW = max(0.0, $pageW - ($marginLeft + $marginRight));
|
||||
$usableH = max(0.0, $pageH - ($marginTop + $marginBottom));
|
||||
|
||||
// Compute columns & rows from physical dimensions and gaps.
|
||||
$cols = (int) floor(($usableW + max(0.0, $gapX)) / (max(0.1, $stickerWidth) + max(0.0, $gapX)));
|
||||
$rows = (int) floor(($usableH + max(0.0, $gapY)) / (max(0.1, $stickerHeight) + max(0.0, $gapY)));
|
||||
|
||||
$cols = max(1, $cols);
|
||||
$rows = max(1, $rows);
|
||||
|
||||
// Optional cap: stickers_per_page
|
||||
$capacity = $cols * $rows;
|
||||
if ($stickersPerPageReq > 0 && $stickersPerPageReq < $capacity) {
|
||||
$rows = (int) ceil($stickersPerPageReq / $cols);
|
||||
$maxRowsFit = (int) floor(($usableH + max(0.0, $gapY)) / (max(0.1, $stickerHeight) + max(0.0, $gapY)));
|
||||
$rows = max(1, min($rows, max(1, $maxRowsFit)));
|
||||
$capacity = $cols * $rows;
|
||||
}
|
||||
|
||||
// Starting positions
|
||||
$xStart = $marginLeft;
|
||||
$yStart = $marginTop;
|
||||
$x = $xStart;
|
||||
$y = $yStart;
|
||||
|
||||
$pt2mm = static function (float $pt): float {
|
||||
return $pt * 0.352778;
|
||||
};
|
||||
|
||||
// Draw loop
|
||||
$i = 0;
|
||||
$perPage = $capacity;
|
||||
|
||||
foreach ($printList as $stu) {
|
||||
if ($i > 0 && $i % $perPage === 0) {
|
||||
$pdf->AddPage();
|
||||
$x = $xStart;
|
||||
$y = $yStart;
|
||||
}
|
||||
|
||||
// Background sticker rectangle (optional; fill white)
|
||||
$pdf->SetFillColor(255, 255, 255);
|
||||
$pdf->Rect($x, $y, $stickerWidth, $stickerHeight, 'F');
|
||||
|
||||
// Centered name with dynamic font size
|
||||
$fontFamily = 'Arial';
|
||||
$fontStyle = 'B';
|
||||
$maxPt = 16;
|
||||
$minPt = 8;
|
||||
$hPad = 2;
|
||||
|
||||
$fullName = trim(($stu['firstname'] ?? '') . ' ' . ($stu['lastname'] ?? '')) ?: 'Student';
|
||||
|
||||
$bestPt = $maxPt;
|
||||
$textW = 0.0;
|
||||
while ($bestPt >= $minPt) {
|
||||
$pdf->SetFont($fontFamily, $fontStyle, $bestPt);
|
||||
$textW = (float) $pdf->GetStringWidth($fullName);
|
||||
if ($textW <= ($stickerWidth - 2 * $hPad)) break;
|
||||
$bestPt -= 0.5;
|
||||
}
|
||||
if ($bestPt < $minPt) {
|
||||
$bestPt = $minPt;
|
||||
$pdf->SetFont($fontFamily, $fontStyle, $bestPt);
|
||||
$textW = (float) $pdf->GetStringWidth($fullName);
|
||||
}
|
||||
$textH = $pt2mm($bestPt);
|
||||
|
||||
$cellX = $x + (($stickerWidth - $textW) / 2);
|
||||
$cellY = $y + (($stickerHeight - $textH) / 2);
|
||||
|
||||
$pdf->SetXY($cellX, $cellY);
|
||||
$pdf->Cell($textW, $textH, $fullName, 0, 0, 'L');
|
||||
|
||||
// Advance cell position within the grid
|
||||
$i++;
|
||||
$posInRow = ($i - 1) % $cols;
|
||||
if ($posInRow === ($cols - 1)) {
|
||||
$x = $xStart;
|
||||
$y += $stickerHeight + $gapY;
|
||||
} else {
|
||||
$x += $stickerWidth + $gapX;
|
||||
}
|
||||
}
|
||||
|
||||
// Output
|
||||
$pdfContent = $pdf->Output('S');
|
||||
return $this->response
|
||||
->setHeader('Content-Type', 'application/pdf')
|
||||
->setHeader('Content-Disposition', 'inline; filename="Student_Stickers.pdf"')
|
||||
->setHeader('Cache-Control', 'private, max-age=0, must-revalidate')
|
||||
->setHeader('Pragma', 'public')
|
||||
->setHeader('Content-Length', (string) strlen($pdfContent))
|
||||
->setBody($pdfContent);
|
||||
}
|
||||
}
|
||||
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]',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\SupplierModel;
|
||||
|
||||
class SupplierController extends BaseController
|
||||
{
|
||||
protected $model;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new SupplierModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$q = trim($this->request->getGet('q') ?? '');
|
||||
$builder = $this->model;
|
||||
if ($q !== '') {
|
||||
$builder = $builder->groupStart()
|
||||
->like('name', $q)->orLike('email', $q)->orLike('phone', $q)
|
||||
->groupEnd();
|
||||
}
|
||||
return view('inventory/suppliers_index', [
|
||||
'suppliers' => $builder->orderBy('name')->paginate(20),
|
||||
'pager' => $this->model->pager,
|
||||
'q' => $q,
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('inventory/suppliers_form', [
|
||||
'title' => 'Add Supplier',
|
||||
'action' => site_url('inventory/suppliers/store'),
|
||||
'supplier' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$data = $this->request->getPost(['name','email','phone','address','notes']);
|
||||
if (!$this->model->save($data)) {
|
||||
return redirect()->back()->withInput()->with('error', implode("\n", $this->model->errors()));
|
||||
}
|
||||
return redirect()->to(site_url('inventory/suppliers'))->with('success', 'Supplier saved.');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$s = $this->model->find($id);
|
||||
if (!$s) return redirect()->to('inventory/suppliers')->with('error', 'Not found.');
|
||||
return view('inventory/suppliers_form', [
|
||||
'title' => 'Edit Supplier',
|
||||
'action' => site_url('inventory/suppliers/update/'.$id),
|
||||
'supplier' => $s,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
$data = $this->request->getPost(['name','email','phone','address','notes']);
|
||||
$data['id'] = $id;
|
||||
if (!$this->model->save($data)) {
|
||||
return redirect()->back()->withInput()->with('error', implode("\n", $this->model->errors()));
|
||||
}
|
||||
return redirect()->to(site_url('inventory/suppliers'))->with('success', 'Supplier updated.');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$this->model->delete($id);
|
||||
return redirect()->to(site_url('inventory/suppliers'))->with('success', 'Supplier deleted.');
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Models\SupplyCategoryModel;
|
||||
|
||||
|
||||
class SupplyCategoryController extends BaseController
|
||||
{
|
||||
protected $model;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new SupplyCategoryModel();
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return view('inventory/categories_index', [
|
||||
'categories' => $this->model->orderBy('name')->findAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('inventory/categories_form', [
|
||||
'title' => 'Add Category',
|
||||
'action' => site_url('inventory/categories/store'),
|
||||
'category' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$data = $this->request->getPost(['name']);
|
||||
if (!$this->model->save($data)) {
|
||||
return redirect()->back()->withInput()->with('error', implode("\n", $this->model->errors()));
|
||||
}
|
||||
return redirect()->to(site_url('inventory/categories'))->with('success', 'Category saved.');
|
||||
}
|
||||
|
||||
public function edit($id)
|
||||
{
|
||||
$cat = $this->model->find($id);
|
||||
if (!$cat) return redirect()->to('inventory/categories')->with('error', 'Not found.');
|
||||
return view('inventory/categories_form', [
|
||||
'title' => 'Edit Category',
|
||||
'action' => site_url('inventory/categories/update/'.$id),
|
||||
'category' => $cat,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update($id)
|
||||
{
|
||||
$data = $this->request->getPost(['name']);
|
||||
$data['id'] = (int) $id;
|
||||
if (!$this->model->save($data)) {
|
||||
return redirect()->back()->withInput()->with('error', implode("\n", $this->model->errors()));
|
||||
}
|
||||
return redirect()->to(site_url('inventory/categories'))->with('success', 'Category updated.');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$this->model->delete($id);
|
||||
return redirect()->to(site_url('inventory/categories'))->with('success', 'Category deleted.');
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use App\Controllers\BaseController;
|
||||
use App\Controllers\View\EmailController;
|
||||
use App\Models\SupportRequestModel;
|
||||
use App\Models\UserModel;
|
||||
|
||||
class SupportController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
helper(['form']);
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return view('/support');
|
||||
}
|
||||
|
||||
|
||||
public function submit()
|
||||
{
|
||||
// Retrieve user_id from session
|
||||
$session = session();
|
||||
$user_id = $session->get('user_id');
|
||||
|
||||
// If no user_id is found in the session, redirect with an error
|
||||
if (!$user_id) {
|
||||
return redirect()->back()->with('error', 'You must be logged in to submit a support request.');
|
||||
}
|
||||
|
||||
// Load the UserModel to retrieve user information
|
||||
$userModel = new UserModel();
|
||||
$user = $userModel->find($user_id);
|
||||
|
||||
// If the user does not exist, redirect with an error
|
||||
if (!$user) {
|
||||
return redirect()->back()->with('error', 'User not found. Please contact support.');
|
||||
}
|
||||
|
||||
// Retrieve form data
|
||||
$subject = $this->request->getPost('subject');
|
||||
$message = $this->request->getPost('message');
|
||||
|
||||
// Get the user's full name using firstname and lastname
|
||||
$full_name = $user['firstname'] . ' ' . $user['lastname'];
|
||||
|
||||
// Call the function to send an email using SMTP (configured via Email.php or .env)
|
||||
$sendStatus = $this->sendSupportEmail($user['email'], $full_name, $subject, $message);
|
||||
|
||||
if ($sendStatus === true) {
|
||||
return redirect()->back()->with('success', 'Your support request has been submitted successfully.');
|
||||
} else {
|
||||
// If email fails, log the error and display a message
|
||||
log_message('error', $sendStatus);
|
||||
return redirect()->back()->with('error', 'Failed to send your support request. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to send an email via SMTP from user to support@domain.org
|
||||
*
|
||||
* @param string $user_email - The email address of the user sending the request.
|
||||
* @param string $user_name - The name of the user sending the request.
|
||||
* @param string $subject - The subject of the support request.
|
||||
* @param string $message - The body of the support request.
|
||||
* @return bool|string - Returns true if email sent successfully, otherwise error message.
|
||||
*/
|
||||
public function sendSupportEmail($user_email, $full_name, $subject, $message)
|
||||
{
|
||||
$mailer = new EmailController();
|
||||
$ok = $mailer->sendEmail(
|
||||
'support@alrahmaisgl.org',
|
||||
$subject,
|
||||
nl2br($message),
|
||||
null,
|
||||
$user_email,
|
||||
$full_name
|
||||
);
|
||||
|
||||
return $ok === true ? true : 'Failed to send support email.';
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function requests()
|
||||
{
|
||||
$supportModel = new SupportRequestModel();
|
||||
$userId = session()->get('user_id');
|
||||
$data['requests'] = $supportModel->where('user_id', $userId)->findAll();
|
||||
|
||||
return view('/support_requests', $data);
|
||||
}
|
||||
|
||||
public function supportTeacher()
|
||||
{
|
||||
return view('/teacher/teacher_support');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
public function submit()
|
||||
{
|
||||
$validation = \Config\Services::validation();
|
||||
|
||||
// Define validation rules
|
||||
$validation->setRules([
|
||||
'subject' => 'required|min_length[3]',
|
||||
'message' => 'required|min_length[10]',
|
||||
]);
|
||||
|
||||
if (!$validation->withRequest($this->request)->run()) {
|
||||
return view('/support', [
|
||||
'validation' => $validation
|
||||
]);
|
||||
}
|
||||
|
||||
$supportModel = new SupportRequestModel();
|
||||
$supportModel->save([
|
||||
'user_id' => session()->get('user_id'),
|
||||
'subject' => $this->request->getPost('subject'),
|
||||
'message' => $this->request->getPost('message'),
|
||||
'status' => 'open'
|
||||
]);
|
||||
|
||||
session()->setFlashdata('success', 'Your support request has been submitted.');
|
||||
|
||||
return redirect()->to('/support');
|
||||
}
|
||||
*/
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
class TestDBController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
try {
|
||||
$query = $db->query('SELECT 1');
|
||||
$result = $query->getResult();
|
||||
|
||||
if ($result) {
|
||||
echo "Database connection is successful.";
|
||||
} else {
|
||||
echo "Database connection failed.";
|
||||
}
|
||||
} catch (DatabaseException $e) {
|
||||
echo "Database connection failed: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\PreferencesModel;
|
||||
use App\Models\SettingsModel;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
class TimeService
|
||||
{
|
||||
private string $defaultUserTimezone = 'America/New_York';
|
||||
private string $serverTimezone = 'UTC';
|
||||
|
||||
// Cached per-request user timezone
|
||||
private ?string $cachedUserTz = null;
|
||||
|
||||
/**
|
||||
* Prime detection cache from the Request (optional call, lazy by default).
|
||||
*/
|
||||
public function primeFromRequest(?RequestInterface $request = null): void
|
||||
{
|
||||
$this->cachedUserTz = $this->detectUserTimezone($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved user timezone (detect lazily if needed).
|
||||
*/
|
||||
public function userTimezone(?RequestInterface $request = null): string
|
||||
{
|
||||
if ($this->cachedUserTz !== null) {
|
||||
return $this->cachedUserTz;
|
||||
}
|
||||
|
||||
$this->cachedUserTz = $this->detectUserTimezone($request);
|
||||
return $this->cachedUserTz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core detection logic (headers > user pref > settings > school config > default).
|
||||
*/
|
||||
public function detectUserTimezone(?RequestInterface $request = null): string
|
||||
{
|
||||
$request = $request ?: service('request');
|
||||
|
||||
// 1) Explicit client header
|
||||
try {
|
||||
$tzHeader = $request->getHeaderLine('X-Timezone')
|
||||
?: $request->getHeaderLine('Timezone')
|
||||
?: $request->getHeaderLine('Accept-Timezone')
|
||||
?: null;
|
||||
|
||||
if ($tzHeader && in_array($tzHeader, timezone_identifiers_list(), true)) {
|
||||
return $tzHeader;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// 2) User preference (if logged-in)
|
||||
try {
|
||||
$userId = (int) (session('user_id') ?: 0);
|
||||
if ($userId > 0) {
|
||||
// Do not select a specific column; some DBs may not have it yet
|
||||
$pref = (new PreferencesModel())
|
||||
->where('user_id', $userId)
|
||||
->first();
|
||||
$tz = $pref['timezone'] ?? null;
|
||||
if ($tz && in_array($tz, timezone_identifiers_list(), true)) {
|
||||
return $tz;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// 3) Global settings timezone (if available)
|
||||
try {
|
||||
$settings = (new SettingsModel())->getSettings();
|
||||
$tz = $settings['timezone'] ?? null;
|
||||
if ($tz && in_array($tz, timezone_identifiers_list(), true)) {
|
||||
return $tz;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// 4) School attendance timezone (if defined)
|
||||
try {
|
||||
$tz = (string) (config('School')->attendance['timezone'] ?? '');
|
||||
if ($tz && in_array($tz, timezone_identifiers_list(), true)) {
|
||||
return $tz;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// 5) Default
|
||||
return $this->defaultUserTimezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Server timezone (for storage/UTC operations).
|
||||
*/
|
||||
public function serverTimezone(): string
|
||||
{
|
||||
return $this->serverTimezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current time in user timezone as CI Time.
|
||||
*/
|
||||
public function nowLocal(?string $tz = null): Time
|
||||
{
|
||||
$tz = $tz ?: $this->userTimezone();
|
||||
return Time::now($tz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current time in UTC as CI Time.
|
||||
*/
|
||||
public function nowUTC(): Time
|
||||
{
|
||||
return Time::now($this->serverTimezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert any supported input to UTC string (Y-m-d H:i:s by default).
|
||||
*
|
||||
* @param string|Time|\DateTimeInterface|null $value
|
||||
*/
|
||||
public function toUTC($value, ?string $fromTz = null, string $format = 'Y-m-d H:i:s'): ?string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fromTz = $fromTz ?: $this->userTimezone();
|
||||
|
||||
try {
|
||||
if ($value instanceof Time) {
|
||||
return $value->setTimezone($this->serverTimezone)->toDateTimeString();
|
||||
}
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return Time::createFromInstance($value)
|
||||
->setTimezone($this->serverTimezone)
|
||||
->format($format);
|
||||
}
|
||||
|
||||
// assume string
|
||||
return Time::parse((string)$value, $fromTz)
|
||||
->setTimezone($this->serverTimezone)
|
||||
->format($format);
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert UTC (or provided timezone) to user-local string.
|
||||
*
|
||||
* @param string|Time|\DateTimeInterface|null $value
|
||||
*/
|
||||
public function toLocal($value, ?string $sourceTz = null, ?string $targetTz = null, string $format = 'Y-m-d H:i:s'): ?string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$targetTz = $targetTz ?: $this->userTimezone();
|
||||
if ($sourceTz === null && $this->isDateOnlyString($value)) {
|
||||
// Date-only strings should not shift across timezones.
|
||||
$sourceTz = $targetTz;
|
||||
} else {
|
||||
$sourceTz = $sourceTz ?: $this->serverTimezone;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($value instanceof Time) {
|
||||
return $value->setTimezone($targetTz)->format($format);
|
||||
}
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return Time::createFromInstance($value)
|
||||
->setTimezone($targetTz)
|
||||
->format($format);
|
||||
}
|
||||
|
||||
// assume string
|
||||
return Time::parse((string)$value, $sourceTz)
|
||||
->setTimezone($targetTz)
|
||||
->format($format);
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience formatter for user-local.
|
||||
*/
|
||||
public function formatLocal($value, string $format = 'Y-m-d H:i', ?string $sourceTz = null): string
|
||||
{
|
||||
return (string) ($this->toLocal($value, $sourceTz, null, $format) ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience formatter for UTC.
|
||||
*/
|
||||
public function formatUTC($value, string $format = 'Y-m-d H:i:s', ?string $fromTz = null): string
|
||||
{
|
||||
return (string) ($this->toUTC($value, $fromTz, $format) ?? '');
|
||||
}
|
||||
|
||||
private function isDateOnlyString($value): bool
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$value = trim($value);
|
||||
return (bool) preg_match('/^\d{4}-\d{2}-\d{2}$/', $value);
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers\View;
|
||||
|
||||
use CodeIgniter\Controller;
|
||||
use App\Models\PreferencesModel;
|
||||
|
||||
class UiController extends Controller
|
||||
{
|
||||
public function style()
|
||||
{
|
||||
$styleCfg = config('Style');
|
||||
$accent = (string) $this->request->getGet('accent');
|
||||
$menu = (string) $this->request->getGet('menu');
|
||||
$menuBg = (string) $this->request->getGet('menu_bg');
|
||||
$menuTx = (string) $this->request->getGet('menu_text');
|
||||
$menuMd = (string) $this->request->getGet('menu_mode'); // optional: 'light'|'dark'|'auto'
|
||||
|
||||
$updates = [];
|
||||
if ($accent && isset(($styleCfg->stylePalettes ?? [])[$accent])) {
|
||||
session()->set('style_color', $accent);
|
||||
$updates['style_color'] = $accent;
|
||||
}
|
||||
if ($menu && isset(($styleCfg->menuPalettes ?? [])[$menu])) {
|
||||
session()->set('menu_color', $menu);
|
||||
$updates['menu_color'] = $menu;
|
||||
$updates['menu_custom_bg'] = null;
|
||||
$updates['menu_custom_text'] = null;
|
||||
$updates['menu_custom_mode'] = null;
|
||||
} elseif (strtolower($menu) === 'custom' || ($menuBg !== '' || $menuTx !== '')) {
|
||||
// Allow custom menu colors via GET: menu=custom&menu_bg=#hex&menu_text=#hex&menu_mode=dark|light|auto
|
||||
$bg = $this->normalizeHex($menuBg);
|
||||
$tx = $this->normalizeHex($menuTx);
|
||||
if ($bg === '' && $tx === '') {
|
||||
// no valid values; ignore
|
||||
} else {
|
||||
if ($bg === '') $bg = '#0f172a';
|
||||
if ($tx === '') $tx = '#ffffff';
|
||||
$mode = in_array($menuMd, ['light','dark','auto'], true) ? $menuMd : 'auto';
|
||||
session()->set('menu_color', 'custom');
|
||||
session()->set('menu_custom_bg', $bg);
|
||||
session()->set('menu_custom_text', $tx);
|
||||
session()->set('menu_custom_mode', $mode);
|
||||
$updates['menu_color'] = 'custom';
|
||||
$updates['menu_custom_bg'] = $bg;
|
||||
$updates['menu_custom_text'] = $tx;
|
||||
$updates['menu_custom_mode'] = $mode;
|
||||
}
|
||||
}
|
||||
|
||||
$userId = (int) session()->get('user_id');
|
||||
if ($userId && !empty($updates)) {
|
||||
$prefsModel = new PreferencesModel();
|
||||
$existing = $prefsModel->where('user_id', $userId)->first();
|
||||
if ($existing) {
|
||||
$prefsModel->update($existing['id'], $updates);
|
||||
} else {
|
||||
$updates['user_id'] = $userId;
|
||||
$prefsModel->insert($updates);
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect back to referrer or home
|
||||
$back = (string) $this->request->getGet('back');
|
||||
if ($back) return redirect()->to($back);
|
||||
$ref = $this->request->getServer('HTTP_REFERER');
|
||||
if ($ref) return redirect()->to($ref);
|
||||
return redirect()->to(site_url('/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a CSS hex color (#RGB or #RRGGBB). Returns '' on failure.
|
||||
*/
|
||||
private function normalizeHex(string $hex): string
|
||||
{
|
||||
$h = trim($hex);
|
||||
if ($h === '') return '';
|
||||
if ($h[0] !== '#') $h = '#' . $h;
|
||||
if (preg_match('/^#([0-9a-fA-F]{3})$/', $h, $m)) {
|
||||
// Expand #RGB to #RRGGBB
|
||||
$r = $m[1][0]; $g = $m[1][1]; $b = $m[1][2];
|
||||
return '#' . $r . $r . $g . $g . $b . $b;
|
||||
}
|
||||
if (preg_match('/^#([0-9a-fA-F]{6})$/', $h)) return strtoupper($h);
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Choose navbar mode based on background luminance: 'dark' for dark BG, else 'light'.
|
||||
*/
|
||||
private function autoModeForBg(string $hexBg): string
|
||||
{
|
||||
$hex = ltrim($hexBg, '#');
|
||||
if (strlen($hex) !== 6) return 'dark';
|
||||
$r = hexdec(substr($hex,0,2));
|
||||
$g = hexdec(substr($hex,2,2));
|
||||
$b = hexdec(substr($hex,4,2));
|
||||
// Relative luminance approximation
|
||||
$lum = (0.2126*$r + 0.7152*$g + 0.0722*$b) / 255.0;
|
||||
return ($lum < 0.5) ? 'dark' : 'light';
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user