43 Commits

Author SHA1 Message Date
root 0f39dbee62 fix endpoints 2026-03-11 23:32:54 -04:00
root 863e330dd8 fix some responses 2026-03-11 18:44:16 -04:00
root 2ef71cc92b add all controllers logic 2026-03-11 17:53:15 -04:00
root 3e6c577085 add services logic 2026-03-11 01:38:20 -04:00
root 182036cc41 add notifications logic and add support of both JWT and Sanctum 2026-03-11 01:20:31 -04:00
root f6be51576c add refund and event logic 2026-03-10 23:12:49 -04:00
root ba1206e314 fix swagger docs 2026-03-10 18:29:09 -04:00
root 25e7b3c67c add inventory and supply logic 2026-03-10 18:21:08 -04:00
root 17d73e2f92 add family logic 2026-03-10 17:52:47 -04:00
root a82f7aedbc add report card logic 2026-03-10 17:33:03 -04:00
root c235fd2170 add role permission logic 2026-03-10 17:11:48 -04:00
root abebe0d9c0 add school calendar logic 2026-03-10 16:55:50 -04:00
root 311bb93977 add SessionTimeout logic 2026-03-10 16:45:08 -04:00
root 2e9a391280 add stickers logic 2026-03-10 16:40:11 -04:00
root 3bf4c687da add whatsup logic 2026-03-10 16:31:35 -04:00
root 20ee70d153 add more controller 2026-03-10 00:48:32 -04:00
root 5eeaec0257 migration 3 2026-03-09 18:36:58 -04:00
root a953f00675 migrations fix 2026-03-09 18:24:26 -04:00
root 99d399d36c update migration table 2026-03-09 18:09:35 -04:00
root b7bdee3af5 update 15 docker CI 2026-03-09 17:52:17 -04:00
root 51ab9cbdbb update 14 docker CI 2026-03-09 17:44:13 -04:00
root d0c8ac533b update 14 docker CI 2026-03-09 17:36:32 -04:00
root f236e5bfe4 update 13 docker CI 2026-03-09 17:31:28 -04:00
root 6a03e547cc update 12 docker CI 2026-03-09 17:25:12 -04:00
root 51d2504e99 update 11 docker CI 2026-03-09 17:17:59 -04:00
root d19d27bd88 update 10 docker CI 2026-03-09 17:13:41 -04:00
root cd73300bc6 update 9 docker CI 2026-03-09 17:10:14 -04:00
root 6e8c3f5466 update 8 docker CI 2026-03-09 17:06:56 -04:00
root e2c3e3cf85 update 7 docker CI 2026-03-09 17:05:24 -04:00
root d9567f925f update 6 docker CI 2026-03-09 17:01:24 -04:00
root d7d718c469 update 5 docker CI 2026-03-09 16:54:38 -04:00
root 380740e4c8 update 4 docker CI 2026-03-09 16:50:25 -04:00
root af250d9fd4 update 3 docker CI 2026-03-09 16:43:12 -04:00
root 52bd34e213 update 3 docker CI 2026-03-09 16:38:19 -04:00
root ad5e42c6c7 update 3 ci docker 2026-03-09 16:32:05 -04:00
root f8aa350bfd update ci docker 2026-03-09 16:26:39 -04:00
root adc4219766 docker ci file 2026-03-09 16:05:44 -04:00
root 79e44fe037 fix logic and tests, update docker CI file 2026-03-09 16:03:16 -04:00
root 1cb3573d4b add more controllers and fix tests 2026-03-09 16:02:30 -04:00
Administrator 0c3e9b16f7 Edit docker-compose.ci.yml 2026-03-09 19:29:52 +00:00
Administrator b3efb8c815 Edit docker-compose.ci.yml 2026-03-09 19:26:22 +00:00
Administrator 6fbca86b9b Edit docker-compose.ci.yml 2026-03-09 19:24:26 +00:00
root d76c871cb7 add controllers, servoices 2026-03-09 02:52:13 -04:00
1529 changed files with 89909 additions and 47555 deletions
+37 -4
View File
@@ -1,4 +1,9 @@
FROM php:8.2-cli
ARG PHP_IMAGE=php:8.2-cli
ARG COMPOSER_IMAGE=composer:2
FROM ${COMPOSER_IMAGE} AS composer
FROM ${PHP_IMAGE}
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
@@ -6,11 +11,39 @@ RUN apt-get update \
unzip \
sqlite3 \
libsqlite3-dev \
default-mysql-client \
libonig-dev \
libxml2-dev \
&& docker-php-ext-install pdo_sqlite mbstring xml \
&& docker-php-ext-install pdo_sqlite pdo_mysql mbstring xml \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
COPY --from=composer /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
ENV APP_DIR=/app
WORKDIR ${APP_DIR}
COPY . ${APP_DIR}
RUN printf '%s\n' \
'#!/bin/sh' \
'set -e' \
'if [ -z "${APP_KEY:-}" ]; then' \
' if command -v php >/dev/null 2>&1; then' \
' APP_KEY=$(php -r '"'"'echo "base64:" . base64_encode(random_bytes(32));'"'"')' \
' elif command -v openssl >/dev/null 2>&1; then' \
' APP_KEY=$(openssl rand -base64 32 | sed "s/^/base64:/")' \
' else' \
' echo "APP_KEY is required but php/openssl is unavailable." >&2' \
' exit 1' \
' fi' \
' export APP_KEY' \
'fi' \
'exec "$@"' \
> /usr/local/bin/ci-entrypoint \
&& chmod +x /usr/local/bin/ci-entrypoint
RUN if [ -f composer.json ]; then \
composer install --no-interaction --prefer-dist --no-progress; \
fi
ENTRYPOINT ["ci-entrypoint"]
Vendored
+4 -3
View File
@@ -22,7 +22,7 @@ pipeline {
stage('Prepare Test DB') {
steps {
sh 'docker compose -f docker-compose.ci.yml run --rm app sh -lc \"mkdir -p database && touch database/database.sqlite\"'
sh 'docker compose -f docker-compose.ci.yml run --rm app sh -lc "echo using mysql: $DB_HOST:$DB_PORT/$DB_DATABASE"'
}
}
@@ -31,16 +31,17 @@ pipeline {
script {
env.APP_KEY = sh(
returnStdout: true,
script: "php -r 'echo \"base64:\" . base64_encode(random_bytes(32));'"
script: "docker compose -f docker-compose.ci.yml run --rm keygen"
).trim()
}
sh 'APP_KEY=$APP_KEY docker compose -f docker-compose.ci.yml run --rm app php artisan test'
sh 'APP_KEY=$APP_KEY docker compose -f docker-compose.ci.yml run --rm app sh -lc "for i in \\$(seq 1 60); do mysqladmin ping -h \\${DB_HOST:-db} -u\\${DB_USERNAME:-school} -p\\${DB_PASSWORD:-school} >/dev/null 2>&1 && break; sleep 2; done; php artisan migrate --force && php artisan test"'
}
}
}
post {
always {
sh 'docker compose -f docker-compose.ci.yml rm -f -s -v || true'
sh 'docker compose -f docker-compose.ci.yml down -v --remove-orphans'
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
namespace Config;
use App\Services\EmailService;
use App\Services\SemesterScoreService;
use App\Services\Scores\SemesterScoreService;
class Services
{
@@ -0,0 +1,26 @@
<?php
namespace App\Console\Commands;
use App\Services\Attendance\AttendanceAutoPublishJobService;
use Illuminate\Console\Command;
class AttendanceAutoPublishCommand extends Command
{
protected $signature = 'attendance:auto-publish';
protected $description = 'Auto-publish attendance days per "second Sunday backward" rule.';
public function handle(AttendanceAutoPublishJobService $service): int
{
$result = $service->run();
$this->info(sprintf(
'Auto-publish checked at %s (TZ: %s). Published rows: %d.',
$result['checked_at'],
$result['timezone'],
$result['published']
));
return self::SUCCESS;
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Console\Commands;
use App\Services\Payments\PaymentMissedCheckService;
use Illuminate\Console\Command;
class CheckMissedPaymentsCommand extends Command
{
protected $signature = 'payments:check-missed';
protected $description = 'Checks for users who missed payments and sends reminders.';
public function handle(PaymentMissedCheckService $service): int
{
$users = $service->findUsersWithMissedPayments();
if (empty($users)) {
$this->info('No missed payments found.');
return self::SUCCESS;
}
$result = $service->sendReminders($users);
$this->info(sprintf('Finished checking missed payments. Sent: %d, Failed: %d.', $result['sent'], $result['failed']));
return self::SUCCESS;
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Console\Commands;
use App\Services\Notifications\NotificationCleanupService;
use Illuminate\Console\Command;
class CleanupExpiredNotificationsCommand extends Command
{
protected $signature = 'notifications:cleanup';
protected $description = 'Deletes expired notifications from the database.';
public function handle(NotificationCleanupService $service): int
{
$count = $service->cleanupExpired();
if ($count <= 0) {
$this->info('No expired notifications found to soft delete.');
return self::SUCCESS;
}
$this->info("Soft-deleted {$count} expired notifications.");
return self::SUCCESS;
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Console\Commands;
use App\Services\Auth\PasswordResetCleanupService;
use Illuminate\Console\Command;
class CleanupPasswordResetsCommand extends Command
{
protected $signature = 'cleanup:password-resets {--days=30}';
protected $description = 'Delete password reset requests older than N days.';
public function handle(PasswordResetCleanupService $service): int
{
$days = (int) $this->option('days');
$count = $service->cleanup($days > 0 ? $days : 30);
$this->info("Deleted {$count} old password reset request(s).");
return self::SUCCESS;
}
}
@@ -0,0 +1,37 @@
<?php
namespace App\Console\Commands;
use App\Services\System\ConfigUpdateService;
use DateTimeZone;
use Illuminate\Console\Command;
class ConfigUpdateCommand extends Command
{
protected $signature = 'config:update {task?} {--task=} {--dry} {--force} {--tz=}';
protected $description = 'Run a configuration update task (weekly cron).';
public function handle(ConfigUpdateService $service): int
{
$task = (string) ($this->option('task') ?: $this->argument('task') ?: '');
$dry = (bool) $this->option('dry');
$force = (bool) $this->option('force');
$tzName = (string) ($this->option('tz') ?: (config('School')->attendance['timezone'] ?? 'UTC'));
$tz = new DateTimeZone($tzName);
if ($task === '' || !in_array($task, $service->availableTasks(), true)) {
$this->error('Invalid or missing --task. Available: ' . implode(', ', $service->availableTasks()));
return self::FAILURE;
}
$result = $service->runTask($task, $dry, $force, $tz);
if (!$result['ok']) {
$this->error($result['message'] ?? "Task '{$task}' failed.");
return self::FAILURE;
}
$this->info("Task '{$task}' finished successfully." . ($dry ? ' [DRY RUN]' : ''));
return self::SUCCESS;
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Console\Commands;
use App\Services\Users\InactiveUserCleanupService;
use Illuminate\Console\Command;
class DeleteInactiveUsersCommand extends Command
{
protected $signature = 'users:delete-inactive-users {--minutes=15}';
protected $description = 'Delete users that are inactive and created more than N minutes ago.';
public function handle(InactiveUserCleanupService $service): int
{
$minutes = (int) $this->option('minutes');
$result = $service->cleanup($minutes > 0 ? $minutes : 15);
$this->info(sprintf(
'Deleted %d inactive users, %d parents rows, %d orphaned user_roles.',
$result['deleted_users'],
$result['deleted_parents'],
$result['deleted_roles']
));
return self::SUCCESS;
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Console\Commands;
use App\Events\DeleteUnverifiedUser;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Event;
class DeleteUnverifiedUsersCommand extends Command
{
protected $signature = 'users:delete-unverified {--timeout=86400}';
protected $description = 'Delete unverified users older than the configured timeout (seconds).';
public function handle(): int
{
$timeout = (int) $this->option('timeout');
if ($timeout <= 0) {
$timeout = 86400;
}
$cutoff = now()->subSeconds($timeout);
$users = User::query()
->where('is_verified', 0)
->where(function ($q) use ($cutoff) {
$q->where('updated_at', '<=', $cutoff)
->orWhere(function ($sub) use ($cutoff) {
$sub->whereNull('updated_at')->where('created_at', '<=', $cutoff);
});
})
->get(['id']);
$count = 0;
foreach ($users as $user) {
Event::dispatch(new DeleteUnverifiedUser((int) $user->id));
$count++;
}
$this->info("Dispatched delete events for {$count} unverified user(s).");
return self::SUCCESS;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class ImportUsers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:import-users';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
//
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Console\Commands;
use App\Services\Attendance\AttendanceSummaryRebuildService;
use Illuminate\Console\Command;
class RecalculateAttendanceCommand extends Command
{
protected $signature = 'attendance:recalculate-summary';
protected $description = 'Recalculates the attendance summary records from raw attendance data.';
public function handle(AttendanceSummaryRebuildService $service): int
{
$result = $service->rebuild();
$this->info('Attendance summary recalculation finished. Inserted: ' . $result['inserted']);
return self::SUCCESS;
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Console\Commands;
use App\Services\Attendance\AttendanceDailySummaryService;
use Illuminate\Console\Command;
class SendAbsenteesSummaryCommand extends Command
{
protected $signature = 'attendance:absentees-summary';
protected $description = 'Sends daily attendance summaries for absences to parents.';
public function handle(AttendanceDailySummaryService $service): int
{
$sent = $service->sendAbsenteesSummary();
$this->info("Attendance absentees summary completed. Sent: {$sent}.");
return self::SUCCESS;
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Console\Commands;
use App\Services\Attendance\AttendanceDailySummaryService;
use Illuminate\Console\Command;
class SendLatesSummaryCommand extends Command
{
protected $signature = 'attendance:lates-summary';
protected $description = 'Sends daily attendance summaries for lates to parents.';
public function handle(AttendanceDailySummaryService $service): int
{
$sent = $service->sendLatesSummary();
$this->info("Attendance lates summary completed. Sent: {$sent}.");
return self::SUCCESS;
}
}
@@ -0,0 +1,56 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use App\Services\Payments\PaymentNotificationService;
use Illuminate\Console\Command;
class SendMonthlyPaymentNotificationsCommand extends Command
{
protected $signature = 'payments:monthly-reminder {--force} {--email=} {--type=}';
protected $description = 'Send monthly payment reminders on the first Saturday of every month.';
public function handle(PaymentNotificationService $service): int
{
$force = (bool) $this->option('force');
$email = (string) ($this->option('email') ?? '');
$type = (string) ($this->option('type') ?? '');
if ($email !== '') {
$user = User::query()->where('email', $email)->first();
if (!$user) {
$this->error('Invalid --email provided.');
return self::FAILURE;
}
$result = $service->send([
'parent_id' => (int) $user->id,
'type' => $type,
'force' => true,
]);
$this->info('Test reminder sent for ' . $email . '.');
return $result['failed'] > 0 ? self::FAILURE : self::SUCCESS;
}
if (!$force && !$this->isFirstSaturday(now())) {
$this->info('Not the first Saturday of the month. Use --force to override.');
return self::SUCCESS;
}
$result = $service->send([
'type' => $type,
'force' => $force,
]);
$this->info(sprintf('Done. Sent: %d, Skipped: %d, Failed: %d.', $result['sent'], $result['skipped'], $result['failed']));
return $result['failed'] > 0 ? self::FAILURE : self::SUCCESS;
}
private function isFirstSaturday(\DateTimeInterface $dt): bool
{
return (int) $dt->format('w') === 6 && (int) $dt->format('j') <= 7;
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\Services\Payments\PaymentTestNotificationService;
use Illuminate\Console\Command;
class SendTestPaymentNotificationCommand extends Command
{
protected $signature = 'payments:send-test {--email=} {--type=no_payment}';
protected $description = 'Send a single test non-payment or installment reminder to a specific email.';
public function handle(PaymentTestNotificationService $service): int
{
$email = (string) $this->option('email');
$type = (string) $this->option('type');
if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->error('Usage: php artisan payments:send-test --email=addr@example.com [--type=no_payment|installment]');
return self::FAILURE;
}
$result = $service->send($email, $type);
if (!$result['ok']) {
$this->error('Failed to send test reminder to ' . $email . '.');
return self::FAILURE;
}
$this->info('Sent test reminder to ' . $email . '.');
return self::SUCCESS;
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Console\Commands;
use App\Services\Payments\PaypalPaymentSyncService;
use Illuminate\Console\Command;
class SyncPaypalPaymentsCommand extends Command
{
protected $signature = 'payments:sync-paypal {--dry-run} {--report-only}';
protected $description = 'Sync PayPal payments to internal payments table from paypal_payments.';
public function handle(PaypalPaymentSyncService $service): int
{
$dryRun = (bool) $this->option('dry-run');
$reportOnly = (bool) $this->option('report-only');
$result = $service->sync($dryRun, $reportOnly);
$this->info(sprintf('[%s] %d PayPal payments processed.', $result['mode'], $result['processed']));
if (!empty($result['failed'])) {
$this->error(sprintf('[%s] Failed transactions: %s', $result['mode'], implode(', ', $result['failed'])));
}
return self::SUCCESS;
}
}
+10
View File
@@ -0,0 +1,10 @@
<?php
namespace App\Events;
class DeleteUnverifiedUser
{
public function __construct(public int $userId)
{
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class WhatsappInvitesSend
{
use Dispatchable;
use SerializesModels;
public function __construct(public array $payload)
{
}
}
@@ -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'] ?? [])
);
@@ -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([
@@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers\Api\Administrator;
use App\Http\Controllers\Controller;
use App\Http\Requests\EmergencyContacts\EmergencyContactUpdateRequest;
use App\Http\Resources\EmergencyContacts\EmergencyContactGroupResource;
use App\Http\Resources\EmergencyContacts\EmergencyContactResource;
use App\Services\EmergencyContacts\EmergencyContactCrudService;
use App\Services\EmergencyContacts\EmergencyContactDirectoryService;
use Illuminate\Http\JsonResponse;
class EmergencyContactController extends Controller
{
public function __construct(
private EmergencyContactDirectoryService $directoryService,
private EmergencyContactCrudService $crudService
) {
}
public function index(): JsonResponse
{
$groups = $this->directoryService->groups();
return response()->json([
'ok' => true,
'groups' => EmergencyContactGroupResource::collection($groups),
]);
}
public function update(EmergencyContactUpdateRequest $request, int $contactId): JsonResponse
{
$contact = $this->crudService->update($contactId, $request->validated());
return response()->json([
'ok' => true,
'contact' => new EmergencyContactResource($contact),
]);
}
public function destroy(int $contactId): JsonResponse
{
$this->crudService->delete($contactId);
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Api\Administrator;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Teachers\TeacherAssignmentDeleteRequest;
use App\Http\Requests\Teachers\TeacherAssignmentListRequest;
use App\Http\Requests\Teachers\TeacherAssignmentStoreRequest;
use App\Http\Resources\Teachers\TeacherAssignmentResource;
use App\Http\Resources\Teachers\TeacherClassResource;
use App\Services\Teachers\TeacherAssignmentService;
use Illuminate\Http\JsonResponse;
class TeacherClassAssignmentController extends BaseApiController
{
public function __construct(private TeacherAssignmentService $assignmentService)
{
parent::__construct();
}
public function index(TeacherAssignmentListRequest $request): JsonResponse
{
$data = $this->assignmentService->listAssignments($request->validated()['school_year'] ?? null);
return response()->json([
'ok' => true,
'school_year' => $data['school_year'],
'teachers' => TeacherAssignmentResource::collection($data['teachers']),
'classes' => TeacherClassResource::collection($data['classes']),
]);
}
public function store(TeacherAssignmentStoreRequest $request): JsonResponse
{
$payload = $request->validated();
$payload['updated_by'] = (int) (auth()->id() ?? 0);
$result = $this->assignmentService->assign($payload);
$status = $result['ok'] ? 200 : 422;
return response()->json([
'ok' => (bool) $result['ok'],
'message' => $result['message'] ?? '',
'position' => $result['position'] ?? null,
], $status);
}
public function destroy(TeacherAssignmentDeleteRequest $request): JsonResponse
{
$result = $this->assignmentService->delete($request->validated());
$status = $result['ok'] ? 200 : 422;
return response()->json([
'ok' => (bool) $result['ok'],
'message' => $result['message'] ?? '',
], $status);
}
}
Binary file not shown.
@@ -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',
@@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers\Api\Attendance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Attendance\LateSlipLogIndexRequest;
use App\Http\Resources\Attendance\LateSlipLogCollection;
use App\Http\Resources\Attendance\LateSlipLogResource;
use App\Models\LateSlipLog;
use App\Services\Attendance\LateSlipLogCommandService;
use App\Services\Attendance\LateSlipLogQueryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class LateSlipLogsController extends BaseApiController
{
public function __construct(
private LateSlipLogQueryService $queryService,
private LateSlipLogCommandService $commandService
) {
parent::__construct();
}
public function index(LateSlipLogIndexRequest $request): JsonResponse
{
$this->authorize('viewAny', LateSlipLog::class);
$filters = $request->validated();
$page = (int) ($filters['page'] ?? 1);
$perPage = (int) ($filters['per_page'] ?? 50);
$logs = $this->queryService->paginate($filters, $page, $perPage);
$collection = new LateSlipLogCollection($logs);
return $this->success([
'logs' => $collection->toArray($request),
'meta' => $collection->with($request)['meta'],
]);
}
public function show(int $id): JsonResponse
{
$log = $this->queryService->find($id);
if (!$log) {
return $this->error('Late slip log not found.', Response::HTTP_NOT_FOUND);
}
$this->authorize('view', $log);
return $this->success([
'log' => new LateSlipLogResource($log),
]);
}
public function destroy(int $id): JsonResponse
{
$log = $this->queryService->find($id);
if (!$log) {
return $this->error('Late slip log not found.', Response::HTTP_NOT_FOUND);
}
$this->authorize('delete', $log);
try {
$deleted = $this->commandService->delete($log);
} catch (\Throwable $e) {
Log::error('Late slip log delete failed: ' . $e->getMessage());
return $this->error('Unable to delete late slip log.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!$deleted) {
return $this->error('Unable to delete late slip log.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success(null, 'Late slip log deleted.');
}
}
@@ -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\AttendanceTrackingService;
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,
@@ -0,0 +1,115 @@
<?php
namespace App\Http\Controllers\Api\Auth;
use App\Http\Controllers\Api\BaseApiController;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
class AuthController extends BaseApiController
{
public function login(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);
if ($validator->fails()) {
return $this->respondValidationError($validator->errors()->toArray());
}
$email = (string) $request->input('email');
$password = (string) $request->input('password');
$user = User::query()->where('email', $email)->first();
if (!$user || !ci_password_verify($password, (string) $user->password)) {
return $this->respondError('Invalid credentials.', 401);
}
if ((int) ($user->is_verified ?? 0) !== 1) {
return $this->respondError('Account not verified.', 403);
}
if (strcasecmp((string) ($user->status ?? ''), 'Active') !== 0) {
return $this->respondError('Account is inactive.', 403);
}
$jwtToken = JWTAuth::fromUser($user);
$sanctumToken = $user->createToken('api')->plainTextToken;
return $this->respondSuccess([
'user' => [
'id' => (int) $user->id,
'firstname' => $user->firstname,
'lastname' => $user->lastname,
'email' => $user->email,
],
'jwt' => [
'access_token' => $jwtToken,
'token_type' => 'bearer',
'expires_in' => (int) config('jwt.ttl') * 60,
],
'sanctum' => [
'access_token' => $sanctumToken,
'token_type' => 'bearer',
],
], 'Authenticated.');
}
public function refresh(Request $request): JsonResponse
{
try {
$newToken = JWTAuth::parseToken()->refresh();
} catch (\Throwable $e) {
return $this->respondError('Token refresh failed.', 401);
}
return $this->respondSuccess([
'access_token' => $newToken,
'token_type' => 'bearer',
'expires_in' => (int) config('jwt.ttl') * 60,
], 'Token refreshed.');
}
public function me(): JsonResponse
{
$user = Auth::user();
if (!$user) {
return $this->respondError('Unauthorized.', 401);
}
return $this->respondSuccess([
'id' => (int) $user->id,
'firstname' => $user->firstname,
'lastname' => $user->lastname,
'email' => $user->email,
], 'OK');
}
public function logout(Request $request): JsonResponse
{
$token = $request->bearerToken();
if ($token && substr_count($token, '.') === 2) {
try {
JWTAuth::setToken($token)->invalidate();
} catch (\Throwable $e) {
// ignore invalidation errors
}
}
$user = $request->user();
if ($user && method_exists($user, 'currentAccessToken')) {
$current = $user->currentAccessToken();
if ($current) {
$current->delete();
}
}
return $this->respondSuccess(null, 'Logged out.');
}
}
@@ -0,0 +1,115 @@
<?php
namespace App\Http\Controllers\Api\Auth;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Security\IpBanIndexRequest;
use App\Http\Requests\Security\IpBanRequest;
use App\Http\Requests\Security\IpUnbanRequest;
use App\Http\Resources\Security\IpBanCollection;
use App\Http\Resources\Security\IpBanResource;
use App\Models\IpAttempt;
use App\Services\Security\IpBanCommandService;
use App\Services\Security\IpBanQueryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class IpBanController extends BaseApiController
{
public function __construct(
private IpBanQueryService $queryService,
private IpBanCommandService $commandService
) {
parent::__construct();
}
public function index(IpBanIndexRequest $request): JsonResponse
{
$this->authorize('viewAny', IpAttempt::class);
$filters = $request->validated();
$page = (int) ($filters['page'] ?? 1);
$perPage = (int) ($filters['per_page'] ?? 20);
$bans = $this->queryService->paginate($filters, $page, $perPage);
$collection = new IpBanCollection($bans);
return $this->success([
'bans' => $collection->toArray($request),
'meta' => $collection->with($request)['meta'],
]);
}
public function show(int $id): JsonResponse
{
$ban = $this->queryService->find($id);
if (!$ban) {
return $this->error('IP record not found.', Response::HTTP_NOT_FOUND);
}
$this->authorize('view', $ban);
return $this->success([
'ban' => new IpBanResource($ban),
]);
}
public function ban(IpBanRequest $request): JsonResponse
{
$this->authorize('create', IpAttempt::class);
$validated = $request->validated();
$id = $validated['id'] ?? null;
$ip = $validated['ip'] ?? null;
$hours = (int) ($validated['hours'] ?? 24);
try {
$ban = $this->commandService->banNow($id ? (int) $id : null, $ip ? (string) $ip : null, $hours);
} catch (\Throwable $e) {
Log::error('IP ban failed: ' . $e->getMessage());
return $this->error('Unable to ban IP.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!$ban) {
return $this->error('IP record not found.', Response::HTTP_NOT_FOUND);
}
return $this->success([
'ban' => new IpBanResource($ban),
], 'IP banned.');
}
public function unban(IpUnbanRequest $request): JsonResponse
{
$this->authorize('update', IpAttempt::class);
$validated = $request->validated();
$all = filter_var($validated['all'] ?? false, FILTER_VALIDATE_BOOLEAN);
try {
if ($all) {
$count = $this->commandService->unbanAll();
return $this->success([
'count' => $count,
], $count . ' IP(s) unbanned.');
}
$ban = $this->commandService->unbanOne(
isset($validated['id']) ? (int) $validated['id'] : null,
$validated['ip'] ?? null
);
} catch (\Throwable $e) {
Log::error('IP unban failed: ' . $e->getMessage());
return $this->error('Unable to unban IP.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!$ban) {
return $this->error('IP record not found.', Response::HTTP_NOT_FOUND);
}
return $this->success([
'ban' => new IpBanResource($ban),
], 'IP unbanned.');
}
}
@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Api\Auth;
use App\Http\Controllers\Api\BaseApiController;
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
{
public function __construct(
private RegistrationService $service,
private RegistrationCaptchaService $captchaService
) {
parent::__construct();
}
public function captcha(): JsonResponse
{
$captcha = $this->captchaService->generate();
return response()->json([
'ok' => true,
'captcha' => $captcha,
]);
}
public function store(Request $request): JsonResponse
{
if (app()->runningUnitTests() && $request->header('X-Debug-Request') === '1') {
return response()->json([
'ok' => true,
'debug' => [
'content_type' => $request->header('Content-Type'),
'content_length' => strlen((string) $request->getContent()),
'request_all' => $request->all(),
'json_all' => $request->json()->all(),
'payload_data' => $this->payloadData(),
],
]);
}
$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';
$status = $code === 'pending_activation' || $code === 'email_exists' ? 409 : 422;
return response()->json([
'ok' => false,
'message' => $result['message'] ?? 'Registration failed.',
'code' => $code,
], $status);
}
return response()->json([
'ok' => true,
'registration' => new RegisterResource($result),
], 201);
}
}
@@ -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));
}
}
+15 -18
View File
@@ -229,7 +229,7 @@ class BaseApiController extends Controller
protected function getCurrentUserId(): ?int
{
$userId = (int) (session()->get('user_id') ?? 0);
$userId = (int) (auth()->id() ?? 0);
return $userId > 0 ? $userId : null;
}
@@ -251,23 +251,20 @@ class BaseApiController extends Controller
$name = $row['email'] ?? 'User #' . $userId;
}
$roles = (array) (session()->get('roles') ?? []);
if (empty($roles)) {
try {
$roles = DB::table('user_roles ur')
->select('LOWER(r.name) AS name')
->join('roles r', 'r.id', '=', 'ur.role_id')
->where('ur.user_id', $userId)
->whereNull('ur.deleted_at')
->pluck('name')
->map(fn($name) => strtolower((string) $name))
->unique()
->values()
->toArray();
} catch (\Throwable $e) {
Log::error('Unable to load user roles: ' . $e->getMessage());
$roles = [];
}
try {
$roles = DB::table('user_roles ur')
->select('LOWER(r.name) AS name')
->join('roles r', 'r.id', '=', 'ur.role_id')
->where('ur.user_id', $userId)
->whereNull('ur.deleted_at')
->pluck('name')
->map(fn($name) => strtolower((string) $name))
->unique()
->values()
->toArray();
} catch (\Throwable $e) {
Log::error('Unable to load user roles: ' . $e->getMessage());
$roles = [];
}
return (object) [
@@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Api\ClassPrep;
use App\Http\Controllers\Api\BaseApiController;
use App\Services\ClassPrep\ClassRosterService;
use App\Services\ClassPrep\StickerCountService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ClassPrepController extends BaseApiController
{
private StickerCountService $stickerCounts;
private ClassRosterService $roster;
public function __construct(StickerCountService $stickerCounts, ClassRosterService $roster)
{
parent::__construct();
$this->stickerCounts = $stickerCounts;
$this->roster = $roster;
}
public function stickerCounts(Request $request): JsonResponse
{
$schoolYear = (string) ($request->query('school_year') ?? '');
$semester = (string) ($request->query('semester') ?? '');
$classSectionId = $request->query('class_section_id') ?? $request->query('class_id');
$classSectionId = is_numeric($classSectionId) ? (int) $classSectionId : null;
if ($classSectionId !== null && $classSectionId > 0) {
$payload = $this->stickerCounts->listForClass($schoolYear, $semester, $classSectionId);
} else {
$payload = $this->stickerCounts->listAll($schoolYear, $semester);
}
return response()->json([
'ok' => true,
'data' => $payload,
]);
}
public function studentsByClass(Request $request, int $classSectionId): JsonResponse
{
$schoolYear = (string) ($request->query('school_year') ?? '');
$students = $this->roster->listStudentsByClass($classSectionId, $schoolYear !== '' ? $schoolYear : null);
return response()->json([
'ok' => true,
'students' => $students,
]);
}
}
@@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Api\ClassPreparation;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\ClassPreparation\ClassPreparationAdjustmentRequest;
use App\Http\Requests\ClassPreparation\ClassPreparationIndexRequest;
use App\Http\Requests\ClassPreparation\ClassPreparationMarkPrintedRequest;
use App\Http\Requests\ClassPreparation\ClassPreparationPrintRequest;
use App\Http\Resources\ClassPreparation\ClassPreparationPrintResource;
use App\Http\Resources\ClassPreparation\ClassPreparationResultResource;
use App\Services\ClassPreparation\ClassPreparationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class ClassPreparationController extends BaseApiController
{
private ClassPreparationService $service;
public function __construct(ClassPreparationService $service)
{
parent::__construct();
$this->service = $service;
}
public function index(ClassPreparationIndexRequest $request): JsonResponse
{
$schoolYear = (string) ($request->query('school_year') ?? '');
$semester = (string) ($request->query('semester') ?? '');
$payload = $this->service->listPrep(
$schoolYear !== '' ? $schoolYear : null,
$semester !== '' ? $semester : null
);
return $this->success([
'schoolYear' => $payload['schoolYear'],
'semester' => $payload['semester'],
'results' => ClassPreparationResultResource::collection($payload['results']),
'totals' => $payload['totals'],
'shortages' => $payload['shortages'],
]);
}
public function markPrinted(ClassPreparationMarkPrintedRequest $request): JsonResponse
{
$validated = $request->validated();
$schoolYear = (string) ($validated['school_year'] ?? '');
$semester = (string) ($validated['semester'] ?? '');
$ids = $validated['class_section_ids'] ?? [];
$count = $this->service->markPrinted($schoolYear, $semester, $ids);
return $this->success([
'updated' => $count,
], 'Class preparation logs updated.');
}
public function saveAdjustments(ClassPreparationAdjustmentRequest $request): JsonResponse
{
$validated = $request->validated();
$classSectionId = (string) $validated['class_section_id'];
$schoolYear = (string) ($validated['school_year'] ?? '');
try {
$count = $this->service->saveAdjustments($classSectionId, $schoolYear, $validated['adjustments']);
} catch (\Throwable $e) {
Log::error('Class prep adjustment save failed: ' . $e->getMessage());
return $this->error('Unable to save adjustments.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success([
'updated' => $count,
], 'Adjustments saved.');
}
public function print(ClassPreparationPrintRequest $request): JsonResponse
{
$validated = $request->validated();
$payload = $this->service->printPrep(
(string) $validated['class_section_id'],
(string) $validated['school_year'],
(string) ($validated['semester'] ?? '')
);
if (empty($payload['ok'])) {
return $this->error('Unable to log class preparation print.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success([
'print' => new ClassPreparationPrintResource($payload),
], 'Class preparation logged.');
}
}
@@ -0,0 +1,136 @@
<?php
namespace App\Http\Controllers\Api\Classes;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Classes\ClassSectionIndexRequest;
use App\Http\Requests\Classes\ClassSectionStoreRequest;
use App\Http\Requests\Classes\ClassSectionUpdateRequest;
use App\Http\Resources\Classes\ClassAttendanceResource;
use App\Http\Resources\Classes\ClassSectionCollection;
use App\Http\Resources\Classes\ClassSectionResource;
use App\Models\ClassSection;
use App\Services\ClassSections\ClassAttendanceService;
use App\Services\ClassSections\ClassSectionCommandService;
use App\Services\ClassSections\ClassSectionQueryService;
use App\Services\ClassSections\ClassSectionSeedService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class ClassController extends BaseApiController
{
public function __construct(
private ClassSectionQueryService $queryService,
private ClassSectionCommandService $commandService,
private ClassAttendanceService $attendanceService,
private ClassSectionSeedService $seedService
) {
parent::__construct();
}
public function index(ClassSectionIndexRequest $request): JsonResponse
{
$this->authorize('viewAny', ClassSection::class);
$filters = $request->validated();
$page = (int) ($filters['page'] ?? 1);
$perPage = (int) ($filters['per_page'] ?? 20);
$sections = $this->queryService->list($filters, $page, $perPage);
$collection = new ClassSectionCollection($sections);
return $this->success([
'sections' => $collection->toArray($request),
'meta' => $collection->with($request)['meta'],
]);
}
public function show(ClassSection $classSection): JsonResponse
{
$this->authorize('view', $classSection);
$section = $this->queryService->find((int) $classSection->id);
if (!$section) {
return $this->error('Class section not found.', Response::HTTP_NOT_FOUND);
}
return $this->success([
'section' => new ClassSectionResource($section),
]);
}
public function store(ClassSectionStoreRequest $request): JsonResponse
{
$this->authorize('create', ClassSection::class);
$section = $this->commandService->create($request->validated());
return $this->success([
'section' => new ClassSectionResource($section),
], 'Class section created.', Response::HTTP_CREATED);
}
public function update(ClassSectionUpdateRequest $request, ClassSection $classSection): JsonResponse
{
$this->authorize('update', $classSection);
$section = $this->commandService->update($classSection, $request->validated());
return $this->success([
'section' => new ClassSectionResource($section),
], 'Class section updated.');
}
public function destroy(ClassSection $classSection): JsonResponse
{
$this->authorize('delete', $classSection);
if (!$this->commandService->delete($classSection)) {
return $this->error('Class section not found.', Response::HTTP_NOT_FOUND);
}
return $this->success(null, 'Class section deleted.');
}
public function attendance(Request $request, int $classSectionId): JsonResponse
{
$teacherId = (int) ($request->user()?->id ?? 0);
if ($teacherId <= 0) {
return $this->error('Missing teacher authentication.', Response::HTTP_UNAUTHORIZED);
}
$payload = $this->attendanceService->getAttendancePayload(
$classSectionId,
$teacherId,
$request->query('school_year'),
$request->query('semester')
);
if (empty($payload['students'])) {
return $this->error('No data found for this class.', Response::HTTP_NOT_FOUND);
}
return $this->success([
'attendance' => new ClassAttendanceResource($payload),
]);
}
public function seedDefaults(): JsonResponse
{
$this->authorize('seedDefaults', ClassSection::class);
try {
$created = $this->seedService->seedDefaults();
} catch (\Throwable $e) {
Log::error('Seeding default classes failed: ' . $e->getMessage());
return $this->error('Unable to seed default classes.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success([
'created' => $created,
], 'Default classes seeded.');
}
}
@@ -0,0 +1,178 @@
<?php
namespace App\Http\Controllers\Api\Communication;
use App\Http\Controllers\Api\BaseApiController;
use App\Services\Communication\CommunicationFamilyService;
use App\Services\Communication\CommunicationPreviewService;
use App\Services\Communication\CommunicationSendService;
use App\Services\Communication\CommunicationStudentService;
use App\Services\Communication\CommunicationTemplateService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CommunicationController extends BaseApiController
{
private CommunicationStudentService $students;
private CommunicationTemplateService $templates;
private CommunicationFamilyService $families;
private CommunicationPreviewService $previewService;
private CommunicationSendService $sendService;
public function __construct(
CommunicationStudentService $students,
CommunicationTemplateService $templates,
CommunicationFamilyService $families,
CommunicationPreviewService $previewService,
CommunicationSendService $sendService
) {
parent::__construct();
$this->students = $students;
$this->templates = $templates;
$this->families = $families;
$this->previewService = $previewService;
$this->sendService = $sendService;
}
public function options(): JsonResponse
{
return response()->json([
'ok' => true,
'students' => $this->students->listStudents(),
'templates' => $this->templates->listActiveTemplates(),
]);
}
public function families(int $studentId): JsonResponse
{
return response()->json([
'ok' => true,
'data' => $this->families->familiesForStudent($studentId),
]);
}
public function guardians(int $familyId): JsonResponse
{
return response()->json([
'ok' => true,
'data' => $this->families->guardiansForFamily($familyId),
]);
}
public function preview(Request $request): JsonResponse
{
$templateKey = (string) $request->input('template_key', '');
$studentId = (int) $request->input('student_id', 0);
$familyId = (int) $request->input('family_id', 0);
$vars = $this->normalizeArray($request->input('vars', []));
if ($templateKey === '' || $studentId <= 0 || $familyId <= 0) {
return response()->json([
'ok' => false,
'message' => 'template_key, student_id, and family_id are required.',
], 422);
}
$teacherName = $this->resolveTeacherName();
$result = $this->previewService->buildPreview($templateKey, $studentId, $familyId, $vars, $teacherName);
if (!$result['ok']) {
return response()->json([
'ok' => false,
'message' => $result['message'] ?? 'Preview failed.',
], $result['status'] ?? 400);
}
return response()->json([
'ok' => true,
'subject' => $result['subject'],
'html' => $result['html'],
]);
}
public function send(Request $request): JsonResponse
{
$studentId = (int) $request->input('student_id', 0);
$familyId = (int) $request->input('family_id', 0);
$templateKey = (string) $request->input('template_key', '');
$subject = (string) $request->input('subject', '');
$body = (string) $request->input('body', '');
$recipients = $this->normalizeArray($request->input('recipients', []));
$cc = $this->normalizeArray($request->input('cc', []));
$bcc = $this->normalizeArray($request->input('bcc', []));
if ($studentId <= 0 || $familyId <= 0 || $templateKey === '' || $subject === '' || $body === '') {
return response()->json([
'ok' => false,
'message' => 'student_id, family_id, template_key, subject, and body are required.',
], 422);
}
if (empty($recipients)) {
return response()->json([
'ok' => false,
'message' => 'At least one recipient is required.',
], 422);
}
$student = $this->students->getStudentBasic($studentId);
if (!$student) {
return response()->json([
'ok' => false,
'message' => 'Student not found.',
], 404);
}
$result = $this->sendService->send([
'student_id' => $studentId,
'family_id' => $familyId,
'template_key' => $templateKey,
'subject' => $subject,
'body' => $body,
'recipients' => $recipients,
'cc' => $cc,
'bcc' => $bcc,
'student_name' => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')),
'sent_by' => (int) (auth()->id() ?? 0),
]);
if (!$result['ok']) {
return response()->json([
'ok' => false,
'message' => 'Failed to send email.',
'error' => $result['error'],
], 500);
}
return response()->json([
'ok' => true,
'message' => 'Email sent successfully.',
]);
}
private function normalizeArray($value): array
{
if (is_array($value)) {
return $value;
}
if (is_string($value) && $value !== '') {
$decoded = json_decode($value, true);
if (is_array($decoded)) {
return $decoded;
}
}
return [];
}
private function resolveTeacherName(): string
{
$user = auth()->user();
if ($user) {
$name = trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? ''));
return $name !== '' ? $name : 'Teacher';
}
return 'Teacher';
}
}
@@ -0,0 +1,212 @@
<?php
namespace App\Http\Controllers\Api\CompetitionScores;
use App\Http\Controllers\Api\BaseApiController;
use App\Models\Competition;
use App\Services\CompetitionScores\CompetitionScoresContextService;
use App\Services\CompetitionScores\CompetitionScoresQueryService;
use App\Services\CompetitionScores\CompetitionScoresRosterService;
use App\Services\CompetitionScores\CompetitionScoresSaveService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class CompetitionScoresController extends BaseApiController
{
private CompetitionScoresContextService $contextService;
private CompetitionScoresQueryService $queryService;
private CompetitionScoresRosterService $rosterService;
private CompetitionScoresSaveService $saveService;
public function __construct(
CompetitionScoresContextService $contextService,
CompetitionScoresQueryService $queryService,
CompetitionScoresRosterService $rosterService,
CompetitionScoresSaveService $saveService
) {
parent::__construct();
$this->contextService = $contextService;
$this->queryService = $queryService;
$this->rosterService = $rosterService;
$this->saveService = $saveService;
}
public function index(Request $request): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
[$assignments, $schoolYear, $semester] = $this->contextService->getTeacherContext($userId);
$activeClassId = $this->contextService->resolveActiveClassId(
$assignments,
(int) $request->query('class_section_id', 0)
);
$activeClassName = $this->contextService->getActiveClassName($assignments, $activeClassId);
if (empty($assignments) || $activeClassId <= 0) {
return response()->json([
'ok' => true,
'competitions' => [],
'activeClassId' => $activeClassId,
'activeClassName' => $activeClassName,
'questionCounts' => [],
'scoreCounts' => [],
'studentTotal' => 0,
'sectionMap' => $this->contextService->getClassSectionMap(),
'schoolYear' => $schoolYear,
'semester' => $semester,
'hasClasses' => false,
]);
}
$competitions = $this->queryService->getCompetitionsForClass($activeClassId, $schoolYear, $semester);
$competitionIds = array_values(array_filter(array_map(static function ($row) {
return (int) ($row['id'] ?? 0);
}, $competitions)));
return response()->json([
'ok' => true,
'competitions' => $competitions,
'activeClassId' => $activeClassId,
'activeClassName' => $activeClassName,
'questionCounts' => $this->queryService->getQuestionCounts($competitionIds, $activeClassId),
'scoreCounts' => $this->queryService->getScoreCounts($competitionIds, $activeClassId),
'studentTotal' => $this->rosterService->getClassStudentCount($activeClassId, $schoolYear),
'sectionMap' => $this->contextService->getClassSectionMap(),
'schoolYear' => $schoolYear,
'semester' => $semester,
'hasClasses' => true,
]);
}
public function edit(Request $request, int $id): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
[$assignments, $schoolYear, $semester] = $this->contextService->getTeacherContext($userId);
$allowedClassIds = $this->contextService->getAllowedClassIds($assignments);
if (empty($allowedClassIds)) {
return response()->json([
'ok' => false,
'message' => 'No class assignments found for your account.',
], 403);
}
$competition = Competition::query()->find($id);
if (!$competition) {
return response()->json([
'ok' => false,
'message' => 'Competition not found.',
], 404);
}
$isLocked = (bool) $competition->is_locked;
$lockedClassId = (int) ($competition->class_section_id ?? 0);
$requestedClassId = (int) $request->query('class_section_id', 0);
$activeClassId = $this->contextService->resolveActiveClassId($assignments, $requestedClassId);
$classSectionId = $lockedClassId > 0 ? $lockedClassId : $activeClassId;
if ($classSectionId <= 0) {
return response()->json([
'ok' => false,
'message' => 'Select a class section before entering scores.',
], 422);
}
if (!in_array($classSectionId, $allowedClassIds, true)) {
return response()->json([
'ok' => false,
'message' => 'You are not assigned to that class.',
], 403);
}
$competitionArray = $competition->toArray();
$students = $this->rosterService->getStudentsForCompetition($competitionArray, $classSectionId);
$scoreMap = $this->saveService->getScoreMap($id, $classSectionId);
$questionCount = $this->queryService->getQuestionCountForCompetition($id, $classSectionId);
$classStudentCount = $this->rosterService->getClassStudentCount(
$classSectionId,
$competitionArray['school_year'] ?? $schoolYear
);
return response()->json([
'ok' => true,
'competition' => $competitionArray,
'students' => $students,
'scoreMap' => $scoreMap,
'classSectionId' => $classSectionId,
'classSectionName' => $this->contextService->getActiveClassName($assignments, $classSectionId),
'classStudentCount' => $classStudentCount,
'questionCount' => $questionCount,
'classSelectionLocked' => $lockedClassId > 0,
'isLocked' => $isLocked,
'schoolYear' => $schoolYear,
'semester' => $semester,
]);
}
public function save(Request $request, int $id): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
[$assignments] = $this->contextService->getTeacherContext($userId);
$allowedClassIds = $this->contextService->getAllowedClassIds($assignments);
if (empty($allowedClassIds)) {
return response()->json([
'ok' => false,
'message' => 'No class assignments found for your account.',
], 403);
}
$competition = Competition::query()->find($id);
if (!$competition) {
return response()->json([
'ok' => false,
'message' => 'Competition not found.',
], 404);
}
if ((bool) $competition->is_locked) {
return response()->json([
'ok' => false,
'message' => 'Competition is locked. You cannot update scores.',
], 423);
}
$lockedClassId = (int) ($competition->class_section_id ?? 0);
$classSectionId = $lockedClassId > 0 ? $lockedClassId : (int) $request->input('class_section_id', 0);
if ($classSectionId <= 0) {
return response()->json([
'ok' => false,
'message' => 'Select a class section before saving scores.',
], 422);
}
if (!in_array($classSectionId, $allowedClassIds, true)) {
return response()->json([
'ok' => false,
'message' => 'You are not assigned to that class.',
], 403);
}
$scores = $request->input('scores', []);
$scores = is_array($scores) ? $scores : [];
[$cleanScores, $invalidScores] = $this->saveService->filterScores($scores);
if (!empty($invalidScores)) {
return response()->json([
'ok' => false,
'message' => 'Scores must be whole numbers (no decimals).',
'invalidScores' => $invalidScores,
], 422);
}
$this->saveService->saveScores($id, $classSectionId, $cleanScores);
return response()->json([
'ok' => true,
'message' => 'Scores saved.',
'savedCount' => count($cleanScores),
]);
}
}
@@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers\Api\Discounts;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Discounts\ApplyDiscountVoucherRequest;
use App\Http\Requests\Discounts\StoreDiscountVoucherRequest;
use App\Http\Requests\Discounts\UpdateDiscountVoucherRequest;
use App\Http\Resources\Discounts\DiscountParentResource;
use App\Http\Resources\Discounts\DiscountVoucherResource;
use App\Services\Discounts\DiscountApplyService;
use App\Services\Discounts\DiscountContextService;
use App\Services\Discounts\DiscountParentService;
use App\Services\Discounts\DiscountVoucherService;
use Illuminate\Http\JsonResponse;
class DiscountController extends BaseApiController
{
private DiscountContextService $context;
private DiscountVoucherService $voucherService;
private DiscountParentService $parentService;
private DiscountApplyService $applyService;
public function __construct(
DiscountContextService $context,
DiscountVoucherService $voucherService,
DiscountParentService $parentService,
DiscountApplyService $applyService
) {
parent::__construct();
$this->context = $context;
$this->voucherService = $voucherService;
$this->parentService = $parentService;
$this->applyService = $applyService;
}
public function options(): JsonResponse
{
$schoolYear = $this->context->getSchoolYear();
$vouchers = $this->voucherService->listActive();
$parents = $this->parentService->listParentsWithDiscounts($schoolYear);
return response()->json([
'ok' => true,
'vouchers' => DiscountVoucherResource::collection($vouchers),
'parents' => DiscountParentResource::collection($parents),
'schoolYear' => $schoolYear,
]);
}
public function apply(ApplyDiscountVoucherRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->applyService->applyVoucher(
(int) $payload['voucher_id'],
$payload['parent_ids'],
(bool) ($payload['allow_additional'] ?? false),
(int) (auth()->id() ?? 0)
);
return response()->json($result, $result['ok'] ? 200 : 422);
}
public function listVouchers(): JsonResponse
{
$vouchers = $this->voucherService->listAll();
return response()->json([
'ok' => true,
'vouchers' => DiscountVoucherResource::collection($vouchers),
]);
}
public function storeVoucher(StoreDiscountVoucherRequest $request): JsonResponse
{
$voucher = $this->voucherService->create($request->validated());
return response()->json([
'ok' => true,
'voucher' => new DiscountVoucherResource($voucher),
], 201);
}
public function updateVoucher(UpdateDiscountVoucherRequest $request, int $id): JsonResponse
{
$voucher = $this->voucherService->update($id, $request->validated());
return response()->json([
'ok' => true,
'voucher' => new DiscountVoucherResource($voucher),
]);
}
public function showVoucher(int $id): JsonResponse
{
$voucher = $this->voucherService->find($id);
return response()->json([
'ok' => true,
'voucher' => new DiscountVoucherResource($voucher),
]);
}
public function deleteVoucher(int $id): JsonResponse
{
$this->voucherService->delete($id);
return response()->json([
'ok' => true,
]);
}
}
@@ -0,0 +1,184 @@
<?php
namespace App\Http\Controllers\Api\Email;
use App\Http\Controllers\Api\BaseApiController;
use App\Services\BroadcastEmail\BroadcastEmailComposerService;
use App\Services\BroadcastEmail\BroadcastEmailDispatchService;
use App\Services\BroadcastEmail\BroadcastEmailImageService;
use App\Services\BroadcastEmail\BroadcastEmailRecipientService;
use App\Services\BroadcastEmail\BroadcastEmailSenderOptionsService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class BroadcastEmailController extends BaseApiController
{
private BroadcastEmailSenderOptionsService $senderOptions;
private BroadcastEmailRecipientService $recipients;
private BroadcastEmailComposerService $composer;
private BroadcastEmailDispatchService $dispatch;
private BroadcastEmailImageService $imageService;
public function __construct(
BroadcastEmailSenderOptionsService $senderOptions,
BroadcastEmailRecipientService $recipients,
BroadcastEmailComposerService $composer,
BroadcastEmailDispatchService $dispatch,
BroadcastEmailImageService $imageService
) {
parent::__construct();
$this->senderOptions = $senderOptions;
$this->recipients = $recipients;
$this->composer = $composer;
$this->dispatch = $dispatch;
$this->imageService = $imageService;
}
public function options(): JsonResponse
{
return response()->json([
'ok' => true,
'parents' => $this->recipients->parentsWithEmails(),
'fromOptions' => $this->senderOptions->listOptions(),
]);
}
public function send(Request $request): JsonResponse
{
$mode = (string) ($request->input('mode') ?? 'standard');
$subject = trim((string) ($request->input('subject') ?? ''));
$fromKey = trim((string) ($request->input('from_key') ?? 'general'));
$body = (string) ($request->input('body_html') ?? '');
$wrapLayout = (bool) $request->boolean('wrap_layout');
$preheader = (string) ($request->input('preheader') ?? '');
$ctaText = (string) ($request->input('cta_text') ?? '');
$ctaUrl = (string) ($request->input('cta_url') ?? '');
$testEmail = trim((string) ($request->input('test_email') ?? ''));
$isTestOnly = (bool) $request->boolean('send_test_only');
$body = $this->composer->sanitizeHtml($body);
if ($subject === '' || $body === '') {
return response()->json([
'ok' => false,
'message' => 'Subject and Body are required.',
], 422);
}
$personalize = $mode === 'personalized';
if ($isTestOnly) {
if ($testEmail === '') {
return response()->json([
'ok' => false,
'message' => 'Provide a test email address.',
], 422);
}
$result = $this->dispatch->sendTest([
'wrap_layout' => $wrapLayout,
'subject' => $subject,
'body_html' => $body,
'recipient_name' => 'Parent',
'preheader' => $preheader,
'cta_text' => $ctaText,
'cta_url' => $ctaUrl,
'personalize' => $personalize,
'test_email' => $testEmail,
'from_key' => $fromKey,
]);
return response()->json($result, $result['ok'] ? 200 : 500);
}
$rawIds = $request->input('parent_ids', []);
$ids = $this->normalizeIds($rawIds);
if (empty($ids)) {
return response()->json([
'ok' => false,
'message' => 'Please select at least one parent.',
], 422);
}
$recipients = $this->recipients->recipientsByIds($ids);
if (empty($recipients)) {
return response()->json([
'ok' => false,
'message' => 'No valid parent emails found.',
], 422);
}
$stats = $this->dispatch->sendBroadcast([
'wrap_layout' => $wrapLayout,
'subject' => $subject,
'body_html' => $body,
'preheader' => $preheader,
'cta_text' => $ctaText,
'cta_url' => $ctaUrl,
'personalize' => $personalize,
'from_key' => $fromKey,
'mode' => $mode,
], $recipients);
$message = sprintf(
'Broadcast finished. Mode: %s. Sent: %d/%d. Failures: %d.',
$stats['mode'],
$stats['sent'],
$stats['attempted'],
$stats['failed']
);
return response()->json([
'ok' => $stats['failed'] === 0,
'stats' => $stats,
'message' => $message,
]);
}
public function uploadImage(Request $request): JsonResponse
{
$file = $request->file('image');
if (!$file || !$file->isValid()) {
return response()->json([
'success' => false,
'error' => 'No image uploaded',
], 400);
}
$result = $this->imageService->store($file);
if (!$result['ok']) {
return response()->json([
'success' => false,
'error' => $result['error'] ?? 'Upload failed',
], $result['status'] ?? 400);
}
$newHash = function_exists('csrf_hash') ? csrf_hash() : csrf_token();
return response()->json([
'success' => true,
'url' => $result['url'],
'csrf_hash' => $newHash,
])->header('X-CSRF-HASH', (string) $newHash);
}
private function normalizeIds($rawIds): array
{
$ids = [];
$raw = is_array($rawIds) ? $rawIds : [$rawIds];
foreach ($raw as $value) {
if (is_string($value) && strpos($value, ',') !== false) {
foreach (explode(',', $value) as $piece) {
$ids[] = (int) $piece;
}
continue;
}
$ids[] = (int) $value;
}
return array_values(array_unique(array_filter($ids)));
}
}
@@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers\Api\Email;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Email\EmailSendRequest;
use App\Http\Resources\Email\EmailSenderResource;
use App\Services\Email\EmailAttachmentService;
use App\Services\Email\EmailDispatchService;
use App\Services\Email\EmailSenderOptionsService;
use Illuminate\Http\JsonResponse;
class EmailController extends BaseApiController
{
public function __construct(
private EmailDispatchService $dispatch,
private EmailSenderOptionsService $senderOptions,
private EmailAttachmentService $attachments
) {
parent::__construct();
}
public function senders(): JsonResponse
{
$senders = $this->senderOptions->listSenders();
return response()->json([
'ok' => true,
'senders' => EmailSenderResource::collection($senders),
]);
}
public function send(EmailSendRequest $request): JsonResponse
{
$payload = $request->validated();
$attachments = $this->attachments->normalize($request->file('attachments', []));
$ok = $this->dispatch->send(
$payload['recipient'],
$payload['subject'],
$payload['html_message'],
$payload['profile'] ?? null,
$payload['reply_to_email'] ?? null,
$payload['reply_to_name'] ?? null,
$attachments
);
if (!$ok) {
return response()->json([
'ok' => false,
'message' => 'Email failed to send.',
], 500);
}
return response()->json([
'ok' => true,
'message' => 'Email sent successfully.',
]);
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Api\Email;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Email\EmailExtractorCompareRequest;
use App\Http\Resources\Email\EmailExtractorCompareResource;
use App\Http\Resources\Email\EmailExtractorEmailsResource;
use App\Services\Email\EmailExtractorService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class EmailExtractorController extends BaseApiController
{
public function __construct(private EmailExtractorService $service)
{
parent::__construct();
}
public function emails(): JsonResponse
{
$emails = $this->service->listEmails();
return response()->json([
'ok' => true,
'emails' => new EmailExtractorEmailsResource($emails),
]);
}
public function compare(EmailExtractorCompareRequest $request): JsonResponse
{
$csvEmails = [];
$file = $request->file('file');
if ($file && $file->isValid()) {
$contents = file_get_contents($file->getRealPath() ?: $file->getPathname());
$csvEmails = $this->service->extractEmailsFromText($contents);
}
if (empty($csvEmails)) {
$csvEmails = $request->input('csv_emails', []);
}
if (empty($csvEmails)) {
return response()->json([
'ok' => false,
'message' => 'Provide a CSV file or csv_emails array.',
], 422);
}
$result = $this->service->compare($csvEmails);
return response()->json([
'ok' => true,
'comparison' => new EmailExtractorCompareResource($result),
]);
}
}
@@ -0,0 +1,115 @@
<?php
namespace App\Http\Controllers\Api\Exams;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Exams\ExamDraftAdminLegacyRequest;
use App\Http\Requests\Exams\ExamDraftAdminReviewRequest;
use App\Http\Requests\Exams\ExamDraftTeacherStoreRequest;
use App\Http\Resources\Exams\ExamDraftResource;
use App\Services\Exams\ExamDraftAdminService;
use App\Services\Exams\ExamDraftTeacherService;
use Illuminate\Http\JsonResponse;
class ExamDraftController extends BaseApiController
{
public function __construct(
private ExamDraftTeacherService $teacherService,
private ExamDraftAdminService $adminService
) {
parent::__construct();
}
public function teacherIndex(): JsonResponse
{
$teacherId = (int) (auth()->id() ?? 0);
if ($teacherId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$data = $this->teacherService->listForTeacher($teacherId);
return response()->json([
'ok' => true,
'assignments' => $data['assignments'],
'drafts' => ExamDraftResource::collection($data['drafts']),
'legacy_exams' => ExamDraftResource::collection($data['legacyExams']),
'exam_types' => $data['examTypes'],
'status_options' => $data['statusOptions'],
'school_year' => $data['schoolYear'],
'semester' => $data['semester'],
]);
}
public function teacherStore(ExamDraftTeacherStoreRequest $request): JsonResponse
{
$teacherId = (int) (auth()->id() ?? 0);
if ($teacherId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$result = $this->teacherService->store($teacherId, $request->validated(), $request->file('draft_file'));
if (!$result['ok']) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Unable to save.'], 422);
}
return response()->json([
'ok' => true,
'draft' => new ExamDraftResource($result['draft']),
], 201);
}
public function adminIndex(): JsonResponse
{
$data = $this->adminService->list();
return response()->json([
'ok' => true,
'drafts' => ExamDraftResource::collection($data['drafts']),
'class_sections' => $data['classSections'],
'legacy_by_class' => $data['legacyByClass'],
'exam_types' => $data['examTypes'],
'status_options' => $data['statusOptions'],
'school_year' => $data['schoolYear'],
'semester' => $data['semester'],
'allowed_extensions' => $data['allowedExtensions'],
'max_upload_bytes' => $data['maxUploadBytes'],
]);
}
public function adminUploadLegacy(ExamDraftAdminLegacyRequest $request): JsonResponse
{
$adminId = (int) (auth()->id() ?? 0);
if ($adminId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$result = $this->adminService->uploadLegacy($adminId, $request->validated(), $request->file('old_exam_file'));
if (!$result['ok']) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Unable to save.'], 422);
}
return response()->json([
'ok' => true,
'draft' => new ExamDraftResource($result['draft']),
], 201);
}
public function adminReview(ExamDraftAdminReviewRequest $request): JsonResponse
{
$adminId = (int) (auth()->id() ?? 0);
if ($adminId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$result = $this->adminService->review($adminId, $request->validated(), $request->file('final_file'));
if (!$result['ok']) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Unable to save.'], 422);
}
return response()->json([
'ok' => true,
'draft' => new ExamDraftResource($result['draft']),
]);
}
}
@@ -0,0 +1,180 @@
<?php
namespace App\Http\Controllers\Api\Expenses;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Expenses\StoreExpenseRequest;
use App\Http\Requests\Expenses\UpdateExpenseRequest;
use App\Http\Requests\Expenses\UpdateExpenseStatusRequest;
use App\Http\Resources\Expenses\ExpenseResource;
use App\Http\Resources\Expenses\ExpenseStaffResource;
use App\Models\Expense;
use App\Services\Expenses\ExpenseContextService;
use App\Services\Expenses\ExpenseQueryService;
use App\Services\Expenses\ExpenseReceiptService;
use App\Services\Expenses\ExpenseRetailorService;
use App\Services\Expenses\ExpenseStaffService;
use App\Services\Expenses\ExpenseStatusService;
use App\Services\Expenses\ExpenseWriteService;
use Illuminate\Http\JsonResponse;
class ExpenseController extends BaseApiController
{
public function __construct(
private ExpenseContextService $context,
private ExpenseRetailorService $retailors,
private ExpenseStaffService $staff,
private ExpenseQueryService $queryService,
private ExpenseReceiptService $receiptService,
private ExpenseWriteService $writeService,
private ExpenseStatusService $statusService
) {
parent::__construct();
}
public function options(): JsonResponse
{
$schoolYear = $this->context->getSchoolYear();
$semester = $this->context->getSemester();
return response()->json([
'ok' => true,
'schoolYear' => $schoolYear,
'semester' => $semester,
'retailors' => $this->retailors->listRetailors(),
'staff' => ExpenseStaffResource::collection($this->staff->listStaffUsers()),
]);
}
public function index(): JsonResponse
{
$rows = $this->queryService->listAll();
$rows = array_map(function ($row) {
$row['receipt_url'] = $this->receiptService->receiptUrl($row['receipt_path'] ?? null);
return $row;
}, $rows);
return response()->json([
'ok' => true,
'expenses' => ExpenseResource::collection($rows),
]);
}
public function show(int $id): JsonResponse
{
$row = $this->queryService->findById($id);
if (!$row) {
return response()->json(['ok' => false, 'message' => 'Expense not found.'], 404);
}
$row['receipt_url'] = $this->receiptService->receiptUrl($row['receipt_path'] ?? null);
return response()->json([
'ok' => true,
'expense' => new ExpenseResource($row),
]);
}
public function store(StoreExpenseRequest $request): JsonResponse
{
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
$receiptName = null;
if ($request->hasFile('receipt')) {
$receiptName = $this->receiptService->storeReceipt($request->file('receipt'));
}
$isDonation = $payload['category'] === 'Donation';
$expense = $this->writeService->store([
'category' => $payload['category'],
'amount' => $payload['amount'],
'receipt_path' => $receiptName,
'description' => $payload['description'] ?? null,
'retailor' => $payload['retailor'] ?? null,
'date_of_purchase' => $payload['date_of_purchase'],
'purchased_by' => (int) $payload['purchased_by'],
'added_by' => $userId,
'status' => $isDonation ? 'approved' : 'pending',
'status_reason' => $isDonation ? 'Marked as Donation (non-reimbursable).' : null,
'approved_by' => $isDonation ? $userId : null,
'school_year' => $payload['school_year'] ?? null,
'semester' => $payload['semester'] ?? null,
]);
return response()->json([
'ok' => true,
'expense' => new ExpenseResource($expense->toArray()),
], 201);
}
public function update(UpdateExpenseRequest $request, int $id): JsonResponse
{
$expense = Expense::query()->find($id);
if (!$expense) {
return response()->json(['ok' => false, 'message' => 'Expense not found.'], 404);
}
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
$receiptName = $expense->receipt_path;
if ($request->hasFile('receipt')) {
$receiptName = $this->receiptService->storeReceipt($request->file('receipt'));
}
if (!empty($payload['remove_receipt'])) {
$receiptName = null;
}
$category = $payload['category'];
$isDonation = $category === 'Donation';
$updateData = [
'category' => $category,
'amount' => $payload['amount'],
'description' => $payload['description'] ?? null,
'retailor' => $payload['retailor'] ?? null,
'date_of_purchase' => $payload['date_of_purchase'],
'purchased_by' => (int) $payload['purchased_by'],
'receipt_path' => $receiptName,
'updated_by' => $userId,
];
if ($isDonation) {
$updateData['status'] = 'approved';
$updateData['status_reason'] = 'Marked as Donation (non-reimbursable).';
$updateData['approved_by'] = $userId ?: null;
$updateData['reimbursement_id'] = null;
} elseif (($expense->category ?? '') === 'Donation') {
$updateData['status_reason'] = null;
$updateData['approved_by'] = $expense->approved_by;
$updateData['status'] = $expense->status ?? 'pending';
}
$updated = $this->writeService->update($expense, $updateData);
return response()->json([
'ok' => true,
'expense' => new ExpenseResource($updated->toArray()),
]);
}
public function updateStatus(UpdateExpenseStatusRequest $request, int $id): JsonResponse
{
$expense = Expense::query()->find($id);
if (!$expense) {
return response()->json(['ok' => false, 'message' => 'Expense not found.'], 404);
}
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
$updated = $this->statusService->updateStatus($expense, $payload['status'], $payload['reason'] ?? '', $userId);
return response()->json([
'ok' => true,
'expense' => new ExpenseResource($updated->toArray()),
]);
}
}
@@ -0,0 +1,143 @@
<?php
namespace App\Http\Controllers\Api\ExtraCharges;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\ExtraCharges\StoreExtraChargeRequest;
use App\Http\Requests\ExtraCharges\UpdateExtraChargeRequest;
use App\Http\Resources\ExtraCharges\ExtraChargeInvoiceOptionResource;
use App\Http\Resources\ExtraCharges\ExtraChargeParentOptionResource;
use App\Http\Resources\ExtraCharges\ExtraChargeResource;
use App\Models\AdditionalCharge;
use App\Services\ExtraCharges\ExtraChargesChargeService;
use App\Services\ExtraCharges\ExtraChargesContextService;
use App\Services\ExtraCharges\ExtraChargesInvoiceService;
use App\Services\ExtraCharges\ExtraChargesListService;
use App\Services\ExtraCharges\ExtraChargesMetaService;
use App\Services\ExtraCharges\ExtraChargesParentService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ExtraChargesController extends BaseApiController
{
public function __construct(
private ExtraChargesContextService $context,
private ExtraChargesMetaService $meta,
private ExtraChargesParentService $parents,
private ExtraChargesInvoiceService $invoices,
private ExtraChargesListService $listService,
private ExtraChargesChargeService $chargeService
) {
parent::__construct();
}
public function list(Request $request): JsonResponse
{
$year = (string) ($request->query('school_year') ?? $this->context->getSchoolYear());
$sem = (string) ($request->query('semester') ?? $this->context->getSemester());
$status = $request->query('status') ?: null;
$q = trim((string) ($request->query('q') ?? '')) ?: null;
$per = (int) ($request->query('per_page') ?? 50);
[$rows, $meta] = $this->listService->listForTerm($year, $sem, $status, $q, $per);
return response()->json([
'ok' => true,
'school_year' => $year,
'semester' => $sem,
'rows' => ExtraChargeResource::collection($rows),
'pager' => $meta,
]);
}
public function options(Request $request): JsonResponse
{
$q = trim((string) ($request->query('q') ?? ''));
$parentOptions = $this->parents->searchParents($q);
return response()->json([
'ok' => true,
'schoolYear' => $this->context->getSchoolYear(),
'semester' => $this->context->getSemester(),
'schoolYears' => $this->meta->getSchoolYears($this->context->getSchoolYear()),
'parents' => ExtraChargeParentOptionResource::collection($parentOptions),
]);
}
public function parentOptions(Request $request): JsonResponse
{
$q = trim((string) ($request->query('q') ?? ''));
$parentOptions = $this->parents->searchParents($q);
return response()->json([
'results' => ExtraChargeParentOptionResource::collection($parentOptions),
]);
}
public function invoicesForParent(Request $request): JsonResponse
{
$parentId = (int) ($request->query('parent_id') ?? 0);
$schoolYear = (string) ($request->query('school_year') ?? $this->context->getSchoolYear());
$rows = $this->invoices->listInvoicesForParent($parentId, $schoolYear);
return response()->json([
'results' => ExtraChargeInvoiceOptionResource::collection($rows),
]);
}
public function store(StoreExtraChargeRequest $request): JsonResponse
{
$payload = $request->validated();
$payload['created_by'] = (int) (auth()->id() ?? 0);
$result = $this->chargeService->createCharge($payload);
return response()->json($result, $result['ok'] ? 201 : 422);
}
public function update(UpdateExtraChargeRequest $request, int $id): JsonResponse
{
$charge = AdditionalCharge::query()->find($id);
if (!$charge) {
return response()->json(['ok' => false, 'error' => 'Charge not found'], 404);
}
$payload = $request->validated();
$payload['updated_by'] = (int) (auth()->id() ?? 0);
$ok = $this->chargeService->updateCharge($charge, $payload);
return response()->json([
'ok' => $ok,
'id' => $id,
], $ok ? 200 : 422);
}
public function void(int $id): JsonResponse
{
$charge = AdditionalCharge::query()->find($id);
if (!$charge) {
return response()->json(['ok' => false, 'error' => 'Charge not found'], 404);
}
$ok = $this->chargeService->voidCharge($charge);
return response()->json([
'ok' => $ok,
], $ok ? 200 : 422);
}
public function reverse(int $id): JsonResponse
{
$charge = AdditionalCharge::query()->find($id);
if (!$charge) {
return response()->json(['ok' => false, 'error' => 'Charge not found'], 404);
}
$ok = $this->chargeService->reverseCharge($charge);
return response()->json([
'ok' => $ok,
], $ok ? 200 : 422);
}
}
@@ -0,0 +1,102 @@
<?php
namespace App\Http\Controllers\Api\Family;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Families\FamilyAdminCardRequest;
use App\Http\Requests\Families\FamilyAdminIndexRequest;
use App\Http\Requests\Families\FamilyAdminSearchRequest;
use App\Http\Requests\Families\FamilyComposeEmailRequest;
use App\Http\Resources\Families\FamilyResource;
use App\Http\Resources\Families\FamilySearchItemResource;
use App\Models\Configuration;
use App\Services\Families\FamilyNotificationService;
use App\Services\Families\FamilyQueryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class FamilyAdminController extends BaseApiController
{
public function __construct(
private FamilyQueryService $queryService,
private FamilyNotificationService $notificationService
) {
parent::__construct();
}
public function index(FamilyAdminIndexRequest $request): JsonResponse
{
$payload = $request->validated();
$schoolYear = trim((string) (Configuration::getConfig('school_year') ?? ''));
$data = $this->queryService->adminIndexData(
$payload['student_id'] ?? null,
$payload['guardian_id'] ?? null,
$schoolYear
);
return $this->success([
'student' => $data['student'],
'families' => FamilyResource::collection($data['families']),
'guardians' => $data['guardians'],
'students' => $data['students'],
'searchStudents' => $data['searchStudents'],
'searchGuardians' => $data['searchGuardians'],
'resolved_student_id' => $data['resolved_student_id'],
]);
}
public function search(FamilyAdminSearchRequest $request): JsonResponse
{
$payload = $request->validated();
$items = $this->queryService->searchSuggestions(
(string) $payload['q'],
(int) ($payload['limit'] ?? 8)
);
return $this->success([
'items' => FamilySearchItemResource::collection($items),
]);
}
public function card(FamilyAdminCardRequest $request): JsonResponse
{
$payload = $request->validated();
$schoolYear = trim((string) (Configuration::getConfig('school_year') ?? ''));
$family = $this->queryService->familyCard(
$payload['student_id'] ?? null,
$payload['guardian_id'] ?? null,
$payload['family_id'] ?? null,
$schoolYear
);
if (!$family) {
return $this->error('Family not found.', Response::HTTP_NOT_FOUND);
}
return $this->success(new FamilyResource($family));
}
public function composeEmail(FamilyComposeEmailRequest $request): JsonResponse
{
$payload = $request->validated();
$ok = $this->notificationService->sendComposeEmail(
(string) $payload['recipient'],
(string) $payload['subject'],
(string) $payload['html_message'],
$payload['profile'] ?? null,
$payload['reply_to_email'] ?? null,
$payload['reply_to_name'] ?? null
);
if (!$ok) {
Log::warning('Compose email failed for family admin', ['recipient' => $payload['recipient']]);
return $this->error('Unable to send email.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success(['recipient' => $payload['recipient']], 'Email sent.');
}
}
@@ -0,0 +1,169 @@
<?php
namespace App\Http\Controllers\Api\Family;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Families\FamiliesByStudentRequest;
use App\Http\Requests\Families\FamilyAttachSecondByEmailRequest;
use App\Http\Requests\Families\FamilyAttachSecondByUserRequest;
use App\Http\Requests\Families\FamilyBootstrapRequest;
use App\Http\Requests\Families\FamilyImportLegacyRequest;
use App\Http\Requests\Families\FamilySetGuardianFlagsRequest;
use App\Http\Requests\Families\FamilySetPrimaryHomeRequest;
use App\Http\Requests\Families\FamilyUnlinkGuardianRequest;
use App\Http\Requests\Families\FamilyUnlinkStudentRequest;
use App\Http\Requests\Families\GuardiansByFamilyRequest;
use App\Http\Resources\Families\FamilyGuardianResource;
use App\Http\Resources\Families\FamilyResource;
use App\Services\Families\FamilyMutationService;
use App\Services\Families\FamilyQueryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class FamilyController extends BaseApiController
{
public function __construct(
private FamilyQueryService $queryService,
private FamilyMutationService $mutationService
) {
parent::__construct();
}
public function familiesByStudent(FamiliesByStudentRequest $request, int $studentId): JsonResponse
{
$rows = $this->queryService->listFamiliesByStudent($studentId);
return $this->success([
'families' => FamilyResource::collection($rows),
]);
}
public function guardiansByFamily(GuardiansByFamilyRequest $request, int $familyId): JsonResponse
{
$rows = $this->queryService->listGuardiansByFamily($familyId);
return $this->success([
'guardians' => FamilyGuardianResource::collection($rows),
]);
}
public function bootstrap(FamilyBootstrapRequest $request): JsonResponse
{
try {
$result = $this->mutationService->bootstrapFamilies();
} catch (\Throwable $e) {
Log::error('Family bootstrap failed: ' . $e->getMessage());
return $this->error('Family bootstrap failed.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success($result, 'Bootstrap completed.');
}
public function attachSecondByUser(FamilyAttachSecondByUserRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->mutationService->attachSecondByUser(
(int) $payload['student_id'],
(int) $payload['user_id'],
(string) ($payload['relation'] ?? 'secondary')
);
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Unable to attach guardian.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success($result, 'Guardian attached.');
}
public function attachSecondByEmail(FamilyAttachSecondByEmailRequest $request): JsonResponse
{
$payload = $request->validated();
try {
$result = $this->mutationService->attachSecondByEmail(
(int) $payload['student_id'],
(string) $payload['email'],
$payload['firstname'] ?? null,
$payload['lastname'] ?? null,
(string) ($payload['relation'] ?? 'secondary')
);
} catch (\Throwable $e) {
Log::error('Attach guardian by email failed: ' . $e->getMessage());
return $this->error('Unable to attach guardian.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Unable to attach guardian.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success($result, 'Guardian attached.');
}
public function setPrimaryHome(FamilySetPrimaryHomeRequest $request): JsonResponse
{
$payload = $request->validated();
$this->mutationService->setPrimaryHome(
(int) $payload['family_id'],
(int) $payload['student_id'],
(bool) $payload['is_primary_home']
);
return $this->success(null, 'Primary home updated.');
}
public function setGuardianFlags(FamilySetGuardianFlagsRequest $request): JsonResponse
{
$payload = $request->validated();
$flags = array_intersect_key($payload, array_flip(['receive_emails', 'is_primary', 'receive_sms', 'relation']));
if (empty($flags)) {
return $this->error('No flags provided.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$this->mutationService->setGuardianFlags(
(int) $payload['family_id'],
(int) $payload['user_id'],
$flags
);
return $this->success(null, 'Guardian flags updated.');
}
public function unlinkGuardian(FamilyUnlinkGuardianRequest $request): JsonResponse
{
$payload = $request->validated();
$this->mutationService->unlinkGuardian(
(int) $payload['family_id'],
(int) $payload['user_id']
);
return $this->success(null, 'Guardian unlinked.');
}
public function unlinkStudent(FamilyUnlinkStudentRequest $request): JsonResponse
{
$payload = $request->validated();
$this->mutationService->unlinkStudent(
(int) $payload['family_id'],
(int) $payload['student_id']
);
return $this->success(null, 'Student unlinked.');
}
public function importSecondParentsFromLegacy(FamilyImportLegacyRequest $request): JsonResponse
{
try {
$result = $this->mutationService->importSecondParentsFromLegacy();
} catch (\Throwable $e) {
Log::error('Legacy second parent import failed: ' . $e->getMessage());
return $this->error('Legacy import failed.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!($result['ok'] ?? true)) {
return $this->error($result['message'] ?? 'Legacy import failed.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success($result, 'Legacy import completed.');
}
}
@@ -0,0 +1,72 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Finance\FeeRefundRequest;
use App\Http\Requests\Finance\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\Support\Facades\Log;
class FeeCalculationController extends BaseApiController
{
public function __construct(
private FeeRefundCalculatorService $refunds,
private FeeStudentFeeService $tuition
) {
parent::__construct();
}
public function refund(FeeRefundRequest $request): JsonResponse
{
$payload = $request->validated();
try {
$result = $this->refunds->calculateRefund($payload['students'], (int) $payload['parent_id']);
} catch (\Throwable $e) {
Log::error('Fee refund calculation failed', [
'parent_id' => $payload['parent_id'] ?? null,
'exception' => $e,
]);
return response()->json([
'ok' => false,
'message' => 'Unable to calculate refund.',
], 500);
}
return response()->json([
'ok' => true,
'refund' => new FeeRefundResource($result),
]);
}
public function tuitionTotal(FeeTuitionTotalRequest $request): JsonResponse
{
$payload = $request->validated();
try {
$total = $this->tuition->totalTuitionFee($payload['students']);
} catch (\Throwable $e) {
Log::error('Fee tuition total calculation failed', [
'exception' => $e,
]);
return response()->json([
'ok' => false,
'message' => 'Unable to calculate tuition total.',
], 500);
}
return response()->json([
'ok' => true,
'tuition' => new FeeTuitionTotalResource([
'total_tuition' => $total,
]),
]);
}
}
@@ -0,0 +1,218 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Finance\FinancialReportRequest;
use App\Http\Requests\Finance\FinancialSummaryRequest;
use App\Http\Requests\Finance\FinancialUnpaidParentsRequest;
use App\Http\Resources\Finance\FinancialReportResource;
use App\Http\Resources\Finance\FinancialSummaryResource;
use App\Http\Resources\Finance\FinancialUnpaidParentResource;
use App\Services\Finance\FinancialPdfReportService;
use App\Services\Finance\FinancialReportService;
use App\Services\Finance\FinancialSchoolYearService;
use App\Services\Finance\FinancialSummaryService;
use App\Services\Finance\FinancialUnpaidParentsService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class FinancialController extends BaseApiController
{
public function __construct(
private FinancialReportService $reportService,
private FinancialSummaryService $summaryService,
private FinancialUnpaidParentsService $unpaidParentsService,
private FinancialSchoolYearService $schoolYears,
private FinancialPdfReportService $pdfService
) {
parent::__construct();
}
public function report(FinancialReportRequest $request): JsonResponse
{
$payload = $request->validated();
$report = $this->reportService->getReport(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null
);
return response()->json([
'ok' => true,
'report' => new FinancialReportResource($report),
]);
}
public function summary(FinancialSummaryRequest $request): JsonResponse
{
$payload = $request->validated();
$summary = $this->summaryService->getSummary(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null
);
$schoolYears = $this->schoolYears->listYears($summary['schoolYear'] ?? null);
return response()->json([
'ok' => true,
'summary' => new FinancialSummaryResource($summary),
'schoolYears' => $schoolYears,
]);
}
public function unpaidParents(FinancialUnpaidParentsRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->unpaidParentsService->getUnpaidParents($payload['school_year'] ?? null);
return response()->json([
'ok' => true,
'schoolYear' => $data['schoolYear'] ?? null,
'schoolYears' => $data['schoolYears'] ?? [],
'results' => FinancialUnpaidParentResource::collection($data['results'] ?? []),
]);
}
public function downloadCsv(FinancialReportRequest $request): StreamedResponse
{
$payload = $request->validated();
$report = $this->reportService->getReport(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null
);
$paymentsMap = [];
foreach ($report['payments'] as $payment) {
$invoiceId = (int) ($payment['invoice_id'] ?? 0);
$paymentsMap[$invoiceId] = (float) ($payment['paid_amount'] ?? 0);
}
$refundsMap = [];
foreach ($report['refunds'] as $refund) {
$invoiceId = (int) ($refund['invoice_id'] ?? 0);
$refundsMap[$invoiceId] = (float) ($refund['total_refunded'] ?? 0);
}
$discountsMap = [];
foreach ($report['discounts'] as $discount) {
$invoiceId = (int) ($discount['invoice_id'] ?? 0);
$discountsMap[$invoiceId] = (float) ($discount['discount_amount'] ?? 0);
}
$breakdown = $report['paymentBreakdown'] ?? [];
$filename = 'financial_report_' . date('Ymd_His') . '.csv';
return response()->streamDownload(function () use ($report, $paymentsMap, $refundsMap, $discountsMap, $breakdown) {
$out = fopen('php://output', 'w');
fputcsv($out, ['Invoice #', 'Parent', 'School Year', 'Total', 'Paid', 'Cash', 'Credit', 'Check', 'Balance', 'Refund', 'Discount', 'Status']);
$sumTotal = 0.0;
$sumPaid = 0.0;
$sumCash = 0.0;
$sumCredit = 0.0;
$sumCheck = 0.0;
$sumBalance = 0.0;
$sumRefund = 0.0;
$sumDiscount = 0.0;
foreach ($report['invoices'] as $inv) {
$invoiceId = (int) ($inv['id'] ?? 0);
$paid = (float) ($paymentsMap[$invoiceId] ?? 0);
$bd = $breakdown[$invoiceId] ?? [];
$cash = (float) ($bd['cash'] ?? 0);
$credit = (float) ($bd['credit'] ?? 0);
$check = (float) ($bd['check'] ?? 0);
$refunded = (float) ($refundsMap[$invoiceId] ?? 0);
$discount = (float) ($discountsMap[$invoiceId] ?? 0);
$total = (float) ($inv['total_amount'] ?? 0);
$balance = $total - $paid - $discount - $refunded;
if ($balance < 0) {
$balance = 0.0;
}
$status = $balance === 0.0 ? 'Paid' : 'Unpaid';
fputcsv($out, [
$inv['invoice_number'] ?? '',
$inv['parent_name'] ?? '',
$inv['school_year'] ?? '',
$total,
$paid,
$cash,
$credit,
$check,
$balance,
$refunded,
$discount,
$status,
]);
$sumTotal += $total;
$sumPaid += $paid;
$sumCash += $cash;
$sumCredit += $credit;
$sumCheck += $check;
$sumBalance += $balance;
$sumRefund += $refunded;
$sumDiscount += $discount;
}
fputcsv($out, [
'TOTALS',
'',
'',
$sumTotal,
$sumPaid,
$sumCash,
$sumCredit,
$sumCheck,
$sumBalance,
$sumRefund,
$sumDiscount,
'',
]);
fputcsv($out, []);
fputcsv($out, ['Expenses Summary']);
fputcsv($out, ['Category', 'Total Amount']);
foreach ($report['expenses'] as $exp) {
fputcsv($out, [
$exp['category'] ?? '',
$exp['total_amount'] ?? 0,
]);
}
fputcsv($out, []);
fputcsv($out, ['Reimbursements Summary']);
fputcsv($out, ['Status', 'Total Amount']);
foreach ($report['reimbursements'] as $row) {
fputcsv($out, [
$row['status'] ?? '',
$row['total_amount'] ?? 0,
]);
}
fclose($out);
}, $filename, ['Content-Type' => 'text/csv']);
}
public function downloadPdf(FinancialReportRequest $request): StreamedResponse
{
$payload = $request->validated();
$summary = $this->summaryService->getSummary(
$payload['date_from'] ?? null,
$payload['date_to'] ?? null,
$payload['school_year'] ?? null
);
$pdfBytes = $this->pdfService->buildPdf($summary);
$schoolYear = $summary['schoolYear'] ?? 'report';
return response()->streamDownload(function () use ($pdfBytes) {
echo $pdfBytes;
}, 'Financial_Report_' . $schoolYear . '.pdf', ['Content-Type' => 'application/pdf']);
}
}
@@ -0,0 +1,139 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Invoices\InvoiceManagementRequest;
use App\Http\Requests\Invoices\InvoiceParentRequest;
use App\Http\Requests\Invoices\InvoiceStatusRequest;
use App\Http\Resources\Invoices\InvoiceManagementParentResource;
use App\Http\Resources\Invoices\InvoiceResource;
use App\Models\Invoice;
use App\Services\Invoices\InvoiceGenerationService;
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
{
public function __construct(
private InvoiceManagementService $management,
private InvoiceGenerationService $generation,
private InvoicePaymentService $paymentService,
private InvoicePdfService $pdfService
) {
parent::__construct();
}
public function management(InvoiceManagementRequest $request): JsonResponse
{
$schoolYear = $request->validated()['school_year'] ?? null;
$schoolYear = $schoolYear ?: $request->input('schoolYear');
$schoolYear = $schoolYear ?: $request->input('year');
$data = $this->management->getManagementData($schoolYear);
return response()->json([
'ok' => true,
'schoolYear' => $data['schoolYear'],
'schoolYears' => $data['schoolYears'],
'invoices' => InvoiceManagementParentResource::collection($data['invoices']),
]);
}
public function generate(Request $request): JsonResponse
{
$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'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Unable to generate invoice.'], 422);
}
return response()->json([
'ok' => true,
'updated' => $result['updated'] ?? false,
'updated_ids' => $result['updated_ids'] ?? [],
'insert_id' => $result['insert_id'] ?? null,
]);
}
public function byParent(InvoiceParentRequest $request, int $parentId): JsonResponse
{
$schoolYear = $request->validated()['school_year'] ?? null;
$invoices = Invoice::getInvoicesByParentId($parentId, $schoolYear);
return response()->json([
'ok' => true,
'invoices' => InvoiceResource::collection($invoices),
]);
}
public function parentPayment(InvoiceParentRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$schoolYear = $request->validated()['school_year'] ?? null;
$data = $this->paymentService->getParentInvoiceSummary($parentId, $schoolYear);
return response()->json([
'ok' => true,
'invoices' => InvoiceResource::collection($data['invoices']),
'schoolYears' => $data['schoolYears'],
'selectedYear' => $data['selectedYear'],
'currentSchoolYear' => $data['currentSchoolYear'],
'dueDate' => $data['dueDate'],
]);
}
public function updateStatus(InvoiceStatusRequest $request, int $invoiceId): JsonResponse
{
$status = $request->validated()['status'];
$updated = Invoice::updateInvoiceStatus($invoiceId, $status);
if (!$updated) {
return response()->json(['ok' => false, 'message' => 'Invoice not found.'], 404);
}
return response()->json(['ok' => true, 'status' => $status]);
}
public function unpaid(): JsonResponse
{
$rows = Invoice::getUnpaidInvoices();
return response()->json([
'ok' => true,
'invoices' => InvoiceResource::collection($rows),
]);
}
public function pdf(int $invoiceId): StreamedResponse
{
$pdfBytes = $this->pdfService->buildPdf($invoiceId);
return response()->streamDownload(function () use ($pdfBytes) {
echo $pdfBytes;
}, 'invoice_' . $invoiceId . '.pdf', ['Content-Type' => 'application/pdf']);
}
}
@@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentByParentRequest;
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
{
public function __construct(
private PaymentPlanService $planService,
private PaymentLookupService $lookupService,
private PaymentBalanceService $balanceService
) {
parent::__construct();
}
public function store(Request $request): JsonResponse
{
$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([
'ok' => true,
'payment' => new PaymentResource($payment),
], 201);
}
public function byParent(PaymentByParentRequest $request, int $parentId): JsonResponse
{
$payload = $request->validated();
$payments = $this->lookupService->getByParent($parentId, $payload['school_year'] ?? null);
return response()->json([
'ok' => true,
'payments' => PaymentResource::collection($payments),
]);
}
public function updateBalance(Request $request, int $paymentId): JsonResponse
{
$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) {
return response()->json(['ok' => false, 'message' => 'Payment not found.'], 404);
}
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentEventChargesListRequest;
use App\Services\Payments\PaymentEventChargesService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class PaymentEventChargesController extends BaseApiController
{
public function __construct(private PaymentEventChargesService $service)
{
parent::__construct();
}
public function index(PaymentEventChargesListRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->listCharges($payload['school_year'] ?? null, $payload['semester'] ?? null);
return response()->json(['ok' => true] + $data);
}
public function store(Request $request): JsonResponse
{
$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);
if ($count === 0) {
return response()->json(['ok' => false, 'message' => 'No student selected.'], 422);
}
return response()->json(['ok' => true, 'inserted' => $count]);
}
public function enrolledStudents(PaymentEventChargesListRequest $request, int $parentId): JsonResponse
{
$payload = $request->validated();
$students = $this->service->getEnrolledStudents($parentId, $payload['school_year'] ?? null);
return response()->json(['ok' => true, 'students' => $students]);
}
}
@@ -0,0 +1,105 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentManualEditRequest;
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
{
public function __construct(private PaymentManualService $service)
{
parent::__construct();
}
public function search(Request $request): JsonResponse
{
$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
);
return response()->json(['ok' => true] + $data);
}
public function suggest(Request $request): JsonResponse
{
$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]);
}
public function record(PaymentManualUpdateRequest $request, int $invoiceId): JsonResponse
{
$payload = $request->validated();
$result = $this->service->recordPayment(
$invoiceId,
(float) $payload['amount'],
(string) $payload['payment_method'],
$payload['payment_date'] ?? null,
$payload['check_number'] ?? null,
$payload['payment_type'] ?? null,
$request->file('payment_file'),
$payload['school_year'] ?? null,
$payload['semester'] ?? null,
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true] + $result);
}
public function edit(PaymentManualEditRequest $request, int $paymentId): JsonResponse
{
$payload = $request->validated();
$this->service->editPayment(
$paymentId,
(float) $payload['paid_amount'],
(string) $payload['payment_method'],
$payload['check_number'] ?? null,
$request->file('payment_file'),
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true]);
}
public function file(Request $request, string $filename)
{
$mode = (string) ($request->query('mode') ?? 'download');
return $this->service->serveCheckFile($filename, $mode);
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaymentNotificationListRequest;
use App\Http\Requests\Payments\PaymentNotificationSendRequest;
use App\Http\Resources\Payments\PaymentNotificationLogResource;
use App\Services\Payments\PaymentNotificationService;
use Illuminate\Http\JsonResponse;
class PaymentNotificationController extends BaseApiController
{
public function __construct(private PaymentNotificationService $service)
{
parent::__construct();
}
public function index(PaymentNotificationListRequest $request): JsonResponse
{
$payload = $request->validated();
$logs = $this->service->listLogs(
$payload['from'] ?? null,
$payload['to'] ?? null,
$payload['type'] ?? null
);
return response()->json([
'ok' => true,
'logs' => PaymentNotificationLogResource::collection($logs),
]);
}
public function send(PaymentNotificationSendRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->service->send($payload);
return response()->json(['ok' => true] + $result);
}
}
@@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
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
{
public function __construct(private PaymentTransactionService $service)
{
parent::__construct();
}
public function store(Request $request): JsonResponse
{
$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([
'ok' => true,
'transaction' => new PaymentTransactionResource($transaction->toArray()),
], 201);
}
public function byPayment(int $paymentId): JsonResponse
{
$transactions = $this->service->getByPayment($paymentId);
return response()->json([
'ok' => true,
'transactions' => PaymentTransactionResource::collection($transactions),
]);
}
public function updateStatus(Request $request, string $transactionId): JsonResponse
{
$status = $request->input('status');
if ($status === null) {
$raw = $request->getContent() ?: '';
$json = json_decode($raw, true);
if (is_array($json) && array_key_exists('status', $json)) {
$status = $json['status'];
} else {
$parsed = [];
parse_str($raw, $parsed);
$status = $parsed['status'] ?? null;
}
}
if ($status === null || trim((string) $status) === '') {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['status' => ['The status field is required.']],
], 422);
}
$updated = $this->service->updateStatus($transactionId, (string) $status);
if (!$updated) {
return response()->json(['ok' => false, 'message' => 'Transaction not found.'], 404);
}
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaypalExecuteRequest;
use App\Services\Payments\PaypalPaymentService;
use Illuminate\Http\JsonResponse;
class PaypalPaymentController extends BaseApiController
{
public function __construct(private PaypalPaymentService $service)
{
parent::__construct();
}
public function redirect(): JsonResponse
{
return response()->json([
'ok' => true,
'redirect_url' => $this->service->getRedirectUrl(),
]);
}
public function create(int $paymentId): JsonResponse
{
$result = $this->service->createPayment($paymentId);
return response()->json(['ok' => true] + $result);
}
public function execute(PaypalExecuteRequest $request): JsonResponse
{
$payload = $request->validated();
$this->service->executePayment(
(string) $payload['payer_id'],
$payload['paypal_payment_id'] ?? null,
isset($payload['payment_id']) ? (int) $payload['payment_id'] : null
);
return response()->json(['ok' => true]);
}
public function cancel(): JsonResponse
{
$this->service->cancelPayment();
return response()->json([
'ok' => true,
'message' => 'Payment was canceled.',
]);
}
}
@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Payments\PaypalTransactionsListRequest;
use App\Http\Resources\Payments\PaypalTransactionResource;
use App\Services\Payments\PaypalTransactionsService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\StreamedResponse;
class PaypalTransactionsController extends BaseApiController
{
public function __construct(private PaypalTransactionsService $service)
{
parent::__construct();
}
public function index(PaypalTransactionsListRequest $request): JsonResponse
{
$payload = $request->validated();
$perPage = (int) ($payload['per_page'] ?? 10);
$rows = $this->service->list($payload['q'] ?? null, $perPage);
return response()->json([
'ok' => true,
'transactions' => PaypalTransactionResource::collection($rows->items()),
'pagination' => [
'current_page' => $rows->currentPage(),
'per_page' => $rows->perPage(),
'total' => $rows->total(),
'last_page' => $rows->lastPage(),
],
]);
}
public function downloadCsv(PaypalTransactionsListRequest $request): StreamedResponse
{
$payload = $request->validated();
$rows = $this->service->listAll($payload['q'] ?? null);
$filename = 'paypal_transactions_' . date('Ymd_His') . '.csv';
return response()->streamDownload(function () use ($rows) {
$out = fopen('php://output', 'w');
fputcsv($out, [
'ID', 'Transaction ID', 'Order ID', 'Parent School ID',
'Email', 'Amount', 'Net Amount', 'Currency',
'Status', 'Event Type', 'Created At',
]);
foreach ($rows as $row) {
fputcsv($out, [
$row['id'] ?? null,
$row['transaction_id'] ?? null,
$row['order_id'] ?? null,
$row['parent_school_id'] ?? null,
$row['payer_email'] ?? null,
$row['amount'] ?? null,
$row['net_amount'] ?? null,
$row['currency'] ?? null,
$row['status'] ?? null,
$row['event_type'] ?? null,
$row['created_at'] ?? null,
]);
}
fclose($out);
}, $filename, ['Content-Type' => 'text/csv']);
}
}
@@ -0,0 +1,174 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\PurchaseOrders\PurchaseOrderIndexRequest;
use App\Http\Resources\PurchaseOrders\PurchaseOrderItemResource;
use App\Http\Resources\PurchaseOrders\PurchaseOrderResource;
use App\Models\PurchaseOrder;
use App\Services\PurchaseOrders\PurchaseOrderCreateService;
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
{
public function __construct(
private PurchaseOrderQueryService $queryService,
private PurchaseOrderCreateService $createService,
private PurchaseOrderReceiveService $receiveService,
private PurchaseOrderStatusService $statusService
) {
parent::__construct();
}
public function index(PurchaseOrderIndexRequest $request): JsonResponse
{
$payload = $request->validated();
$orders = $this->queryService->list($payload['q'] ?? null);
return response()->json([
'ok' => true,
'orders' => PurchaseOrderResource::collection($orders),
]);
}
public function options(): JsonResponse
{
$data = $this->queryService->options();
return response()->json([
'ok' => true,
'suppliers' => $data['suppliers'] ?? [],
'supplies' => $data['supplies'] ?? [],
]);
}
public function show(int $id): JsonResponse
{
$data = $this->queryService->find($id);
if (!$data) {
return response()->json(['ok' => false, 'message' => 'Purchase order not found.'], 404);
}
return response()->json([
'ok' => true,
'purchase_order' => new PurchaseOrderResource($data['purchase_order']),
'items' => PurchaseOrderItemResource::collection($data['items'] ?? []),
]);
}
public function store(Request $request): JsonResponse
{
$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);
} catch (RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
} catch (\Throwable $e) {
return response()->json(['ok' => false, 'message' => 'Unable to create purchase order.'], 500);
}
return response()->json([
'ok' => true,
'purchase_order' => new PurchaseOrderResource($po),
], 201);
}
public function receive(Request $request, int $id): JsonResponse
{
$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) {
return response()->json(['ok' => false, 'message' => 'Purchase order not found.'], 404);
}
$user = auth()->user();
$issuedBy = $user ? (string) ($user->email ?? $user->username ?? $user->id) : 'system';
try {
$result = $this->receiveService->receive($id, $payload['items'], $issuedBy);
} catch (RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 409);
} catch (\Throwable $e) {
return response()->json(['ok' => false, 'message' => 'Unable to receive items.'], 500);
}
return response()->json([
'ok' => true,
'status' => $result['status'] ?? null,
'completed' => $result['completed'] ?? false,
]);
}
public function cancel(int $id): JsonResponse
{
$po = PurchaseOrder::query()->find($id);
if (!$po) {
return response()->json(['ok' => false, 'message' => 'Purchase order not found.'], 404);
}
try {
$this->statusService->cancel($id);
} catch (RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 409);
} catch (\Throwable $e) {
return response()->json(['ok' => false, 'message' => 'Unable to cancel purchase order.'], 500);
}
return response()->json(['ok' => true, 'status' => PurchaseOrder::STATUS_CANCELED]);
}
}
@@ -0,0 +1,186 @@
<?php
namespace App\Http\Controllers\Api\Finance;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Refunds\RefundDecisionRequest;
use App\Http\Requests\Refunds\RefundIndexRequest;
use App\Http\Requests\Refunds\RefundPaymentRequest;
use App\Http\Requests\Refunds\RefundRecalculateRequest;
use App\Http\Requests\Refunds\RefundRejectRequest;
use App\Http\Requests\Refunds\RefundStoreRequest;
use App\Http\Resources\Refunds\RefundResource;
use App\Models\Configuration;
use App\Services\Refunds\RefundDecisionService;
use App\Services\Refunds\RefundOverpaymentService;
use App\Services\Refunds\RefundPayoutService;
use App\Services\Refunds\RefundQueryService;
use App\Services\Refunds\RefundRequestService;
use App\Services\Refunds\RefundSummaryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
class RefundController extends BaseApiController
{
public function __construct(
private RefundQueryService $queryService,
private RefundRequestService $requestService,
private RefundDecisionService $decisionService,
private RefundPayoutService $payoutService,
private RefundOverpaymentService $overpaymentService,
private RefundSummaryService $summaryService
) {
parent::__construct();
}
public function index(RefundIndexRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->queryService->list($payload);
return response()->json([
'ok' => true,
'refunds' => RefundResource::collection($result['refunds']),
'pagination' => $result['pagination'],
]);
}
public function show(int $refundId): JsonResponse
{
$refund = $this->queryService->find($refundId);
if (!$refund) {
return response()->json(['ok' => false, 'message' => 'Refund not found.'], 404);
}
return response()->json([
'ok' => true,
'refund' => new RefundResource($refund),
]);
}
public function store(RefundStoreRequest $request): JsonResponse
{
$payload = $request->validated();
$actorId = (int) (auth()->id() ?? 0);
try {
$result = $this->requestService->requestRefund($payload, $actorId);
} catch (\Throwable $e) {
Log::error('Refund request failed.', ['error' => $e->getMessage()]);
return response()->json(['ok' => false, 'message' => 'Failed to submit refund request.'], 500);
}
if (empty($result['ok'])) {
return response()->json([
'ok' => false,
'message' => $result['message'] ?? 'Failed to submit refund request.',
'available' => $result['available'] ?? null,
], 422);
}
return response()->json([
'ok' => true,
'refund_id' => $result['refund_id'] ?? null,
'refund_amount' => $result['refund_amount'] ?? null,
'status' => $result['status'] ?? null,
], 201);
}
public function update(RefundDecisionRequest $request, int $refundId): JsonResponse
{
$payload = $request->validated();
$actorId = (int) (auth()->id() ?? 0);
$result = $this->decisionService->updateDecision(
$refundId,
(string) $payload['status'],
(string) $payload['reason'],
$actorId
);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to update refund.'], 422);
}
return response()->json(['ok' => true]);
}
public function destroy(int $refundId): JsonResponse
{
$actorId = (int) (auth()->id() ?? 0);
$result = $this->decisionService->cancel($refundId, $actorId);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to cancel refund.'], 404);
}
return response()->json(['ok' => true]);
}
public function approve(int $refundId): JsonResponse
{
$actorId = (int) (auth()->id() ?? 0);
$result = $this->decisionService->approve($refundId, $actorId);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Approve failed.'], 422);
}
return response()->json(['ok' => true]);
}
public function reject(RefundRejectRequest $request, int $refundId): JsonResponse
{
$payload = $request->validated();
$actorId = (int) (auth()->id() ?? 0);
$result = $this->decisionService->reject($refundId, (string) $payload['reason'], $actorId);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Reject failed.'], 422);
}
return response()->json(['ok' => true]);
}
public function recordPayment(RefundPaymentRequest $request, int $refundId): JsonResponse
{
$payload = $request->validated();
$payload['check_file'] = $request->file('check_file');
$actorId = (int) (auth()->id() ?? 0);
$result = $this->payoutService->recordPayment($refundId, $payload, $actorId);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to update payment.'], 422);
}
return response()->json(['ok' => true]);
}
public function recalculateOverpayments(RefundRecalculateRequest $request): JsonResponse
{
$payload = $request->validated();
$actorId = (int) (auth()->id() ?? 0);
$result = $this->overpaymentService->recalculate(true, $payload['invoice_number'] ?? null, $actorId);
return response()->json([
'ok' => true,
'created_ids' => $result['created_ids'] ?? null,
'updated_ids' => $result['updated_ids'] ?? null,
'message' => $result['message'] ?? null,
]);
}
public function parentBalances(int $parentId): JsonResponse
{
$termSchoolYear = (string) (Configuration::getConfig('school_year') ?? '');
$termSemester = (string) (Configuration::getConfig('semester') ?? '');
$summary = $this->summaryService->getParentFinancialSummary($parentId, $termSchoolYear, $termSemester);
return response()->json([
'ok' => true,
'summary' => $summary,
]);
}
}
@@ -0,0 +1,450 @@
<?php
namespace App\Http\Controllers\Api\Finance;
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\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\ReimbursementProcessRequest;
use App\Http\Requests\Reimbursements\ReimbursementUnderProcessingRequest;
use App\Http\Resources\Reimbursements\ReimbursementBatchResource;
use App\Http\Resources\Reimbursements\ReimbursementExpenseResource;
use App\Http\Resources\Reimbursements\ReimbursementRecipientResource;
use App\Http\Resources\Reimbursements\ReimbursementResource;
use App\Http\Resources\Reimbursements\ReimbursementUnderProcessingItemResource;
use App\Models\Reimbursement;
use App\Models\ReimbursementBatch;
use App\Services\Files\FileServeService;
use App\Services\Reimbursements\ReimbursementBatchAssignmentService;
use App\Services\Reimbursements\ReimbursementBatchService;
use App\Services\Reimbursements\ReimbursementContextService;
use App\Services\Reimbursements\ReimbursementCrudService;
use App\Services\Reimbursements\ReimbursementDonationService;
use App\Services\Reimbursements\ReimbursementEmailService;
use App\Services\Reimbursements\ReimbursementExportService;
use App\Services\Reimbursements\ReimbursementFileService;
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
{
public function __construct(
private ReimbursementContextService $context,
private ReimbursementQueryService $queryService,
private ReimbursementDonationService $donationService,
private ReimbursementBatchService $batchService,
private ReimbursementBatchAssignmentService $assignmentService,
private ReimbursementFileService $fileService,
private ReimbursementExportService $exportService,
private ReimbursementEmailService $emailService,
private ReimbursementCrudService $crudService,
private FileServeService $fileServeService
) {
parent::__construct();
}
public function underProcessing(ReimbursementUnderProcessingRequest $request): JsonResponse
{
$data = $this->queryService->underProcessing();
return response()->json([
'ok' => true,
'pendingItems' => ReimbursementUnderProcessingItemResource::collection($data['pendingItems'] ?? []),
'existingBatches' => ReimbursementBatchResource::collection($data['existingBatches'] ?? []),
'adminUsers' => ReimbursementRecipientResource::collection($data['adminUsers'] ?? []),
'itemsPayload' => ReimbursementUnderProcessingItemResource::collection($data['itemsPayload'] ?? []),
]);
}
public function markDonation(Request $request): JsonResponse
{
$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 {
$this->donationService->markDonation((int) $payload['expense_id'], $userId);
} catch (RuntimeException $e) {
$message = $e->getMessage();
$status = str_contains($message, 'not found') ? 404 : (str_contains($message, 'already') ? 409 : 422);
return response()->json(['ok' => false, 'message' => $message], $status);
} catch (\Throwable $e) {
return response()->json(['ok' => false, 'message' => 'Unable to mark donation right now.'], 500);
}
return response()->json(['ok' => true]);
}
public function createBatch(Request $request): JsonResponse
{
$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();
$batch = $this->batchService->createBatch($payload['title'] ?? null, $userId, $schoolYear, $semester);
return response()->json([
'ok' => true,
'batch' => $batch,
], 201);
}
public function updateBatchAssignment(ReimbursementBatchAssignmentRequest $request): JsonResponse
{
$payload = $request->validated();
$batchId = (int) ($payload['batch_id'] ?? 0);
$batchNumber = $payload['batch_number'] ?? null;
if ($batchId <= 0 && $batchNumber !== null && trim((string) $batchNumber) !== '') {
$batchId = (int) $batchNumber;
}
$adminIdRaw = $payload['admin_id'] ?? null;
$adminId = ($adminIdRaw === '' || $adminIdRaw === null) ? null : (int) $adminIdRaw;
$reimbursementId = !empty($payload['reimbursement_id']) ? (int) $payload['reimbursement_id'] : null;
try {
$result = $this->assignmentService->updateAssignment(
(int) $payload['expense_id'],
$batchId,
$adminId,
$reimbursementId,
$this->context->getSchoolYear(),
$this->context->getSemester()
);
} catch (RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
}
return response()->json([
'ok' => true,
'assignment' => $result,
]);
}
public function lockBatch(ReimbursementBatchLockRequest $request): JsonResponse
{
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
try {
$this->batchService->lockBatch(
(int) $payload['batch_id'],
$userId,
$this->context->getSchoolYear(),
$this->context->getSemester()
);
} catch (RuntimeException $e) {
$message = $e->getMessage();
$status = str_contains($message, 'check file') ? 409 : 404;
return response()->json(['ok' => false, 'message' => $message], $status);
} catch (\Throwable $e) {
return response()->json(['ok' => false, 'message' => 'Unable to lock batch right now.'], 500);
}
return response()->json(['ok' => true, 'status' => 'closed']);
}
public function uploadBatchAdminFile(ReimbursementBatchAdminFileRequest $request): JsonResponse
{
$payload = $request->validated();
$batchId = (int) $payload['batch_id'];
$adminId = (int) $payload['admin_id'];
$batch = ReimbursementBatch::query()->find($batchId);
if (!$batch) {
return response()->json(['ok' => false, 'message' => 'Batch not found.'], 404);
}
if (strtolower((string) ($batch->status ?? '')) !== ReimbursementBatch::STATUS_OPEN) {
return response()->json(['ok' => false, 'message' => 'Cannot upload files for a locked batch.'], 409);
}
try {
$file = $this->fileService->storeAdminFile($batchId, $adminId, $request->file('check_file'), (int) (auth()->id() ?? 0));
} catch (\Throwable $e) {
return response()->json(['ok' => false, 'message' => 'Unable to store the uploaded file.'], 500);
}
return response()->json([
'ok' => true,
'file' => $file,
]);
}
public function serveAdminCheckFile(FileNameRequest $request, string $name, string $mode = 'inline'): Response|JsonResponse
{
$allowedExtensions = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf'];
$meta = $this->fileServeService->meta(storage_path('uploads/reimbursements'), $name, $allowedExtensions, null);
$disposition = strtolower(trim($mode)) === 'download' ? 'attachment' : 'inline';
return response(file_get_contents($meta['path']), 200, [
'Content-Type' => $meta['mime'],
'Content-Disposition' => $disposition . '; filename="' . $meta['download_name'] . '"',
'Content-Length' => (string) $meta['size'],
'ETag' => $meta['etag'],
'Last-Modified' => $meta['last_modified'],
]);
}
public function index(ReimbursementIndexRequest $request): JsonResponse
{
$payload = $request->validated();
$filters = [
'school_year' => $payload['school_year'] ?? null,
'semester' => $payload['semester'] ?? null,
'status' => $payload['status'] ?? null,
'user_id' => $payload['user_id'] ?? null,
];
$data = $this->queryService->index($filters);
return response()->json([
'ok' => true,
'expenses' => ReimbursementExpenseResource::collection($data['expenses'] ?? []),
'users' => ReimbursementRecipientResource::collection($data['users'] ?? []),
'schoolYears' => $data['schoolYears'] ?? [],
'batchSummaries' => $data['batchSummaries'] ?? [],
'batchDetails' => $data['batchDetails'] ?? [],
'batchAttachments' => $data['batchAttachments'] ?? [],
'donationBatch' => $data['donationBatch'] ?? null,
'donationDetails' => $data['donationDetails'] ?? null,
]);
}
public function sendBatchEmail(ReimbursementBatchEmailRequest $request): JsonResponse
{
$payload = $request->validated();
$receiptIds = $payload['receipts'] ?? [];
$checkIds = $payload['checks'] ?? [];
try {
$ok = $this->emailService->sendBatchEmail(
(int) $payload['batch_id'],
$payload['recipient_email'],
(string) ($payload['message'] ?? ''),
$receiptIds,
$checkIds
);
} catch (RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 404);
}
if (!$ok) {
return response()->json(['ok' => false, 'message' => 'Failed to send email. Please try again.'], 500);
}
return response()->json(['ok' => true, 'message' => 'Email sent successfully.']);
}
public function export(ReimbursementExportRequest $request): Response
{
$payload = $request->validated();
$type = $payload['type'] ?? 'processed';
if ($type === 'under_processing') {
$csv = $this->exportService->buildUnderProcessingCsv();
} else {
$csv = $this->exportService->buildProcessedCsv($payload);
}
return response()->streamDownload(function () use ($csv) {
$out = fopen('php://output', 'w');
fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF));
foreach ($csv['rows'] as $row) {
fputcsv($out, $row);
}
fclose($out);
}, $csv['filename'], [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
public function exportBatch(ReimbursementExportBatchRequest $request): Response
{
$payload = $request->validated();
$csv = $this->exportService->buildBatchCsv((int) $payload['batch_id']);
return response()->streamDownload(function () use ($csv) {
$out = fopen('php://output', 'w');
fprintf($out, chr(0xEF) . chr(0xBB) . chr(0xBF));
foreach ($csv['rows'] as $row) {
fputcsv($out, $row);
}
fclose($out);
}, $csv['filename'], [
'Content-Type' => 'text/csv; charset=UTF-8',
]);
}
public function store(Request $request): JsonResponse
{
$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;
if ($request->hasFile('receipt')) {
$receiptName = $this->fileService->storeReceipt($request->file('receipt'));
}
try {
$reimb = $this->crudService->store(
$payload,
$userId,
$this->context->getSchoolYear(),
$this->context->getSemester(),
$receiptName
);
} catch (RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
}
$data = $reimb->toArray();
$data['receipt_url'] = $this->fileService->receiptUrl($reimb->receipt_path ?? null);
return response()->json([
'ok' => true,
'reimbursement' => new ReimbursementResource($data),
], 201);
}
public function process(ReimbursementProcessRequest $request): JsonResponse
{
$payload = $request->validated();
$userId = (int) (auth()->id() ?? 0);
$receiptName = null;
if ($request->hasFile('receipt')) {
$receiptName = $this->fileService->storeReceipt($request->file('receipt'));
}
try {
$reimb = $this->crudService->process(
$payload,
$userId,
$this->context->getSchoolYear(),
$this->context->getSemester(),
$receiptName
);
} catch (RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
}
$data = $reimb->toArray();
$data['receipt_url'] = $this->fileService->receiptUrl($reimb->receipt_path ?? null);
return response()->json([
'ok' => true,
'reimbursement' => new ReimbursementResource($data),
], 201);
}
public function reimbursedExpenses(): JsonResponse
{
$expenses = $this->queryService->reimbursedExpenses();
return response()->json([
'ok' => true,
'expenses' => $expenses,
]);
}
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);
}
$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;
if ($request->hasFile('receipt')) {
$receiptName = $this->fileService->storeReceipt($request->file('receipt'));
}
if (!empty($payload['remove_receipt'])) {
$receiptName = null;
}
$updated = $this->crudService->update($reimb, $payload, $userId, $receiptName);
$data = $updated->toArray();
$data['receipt_url'] = $this->fileService->receiptUrl($updated->receipt_path ?? null);
return response()->json([
'ok' => true,
'reimbursement' => new ReimbursementResource($data),
]);
}
}
@@ -0,0 +1,72 @@
<?php
namespace App\Http\Controllers\Api\Frontend;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Resources\Frontend\FrontendPageResource;
use App\Http\Resources\Frontend\FrontendUserResource;
use App\Services\Frontend\FrontendPageService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
class FrontendController extends BaseApiController
{
public function __construct(private FrontendPageService $pageService)
{
parent::__construct();
}
public function index(): JsonResponse
{
return $this->success([
'page' => new FrontendPageResource($this->pageService->page('index')),
]);
}
public function facility(): JsonResponse
{
return $this->success([
'page' => new FrontendPageResource($this->pageService->page('facility')),
]);
}
public function team(): JsonResponse
{
return $this->success([
'page' => new FrontendPageResource($this->pageService->page('team')),
]);
}
public function callToAction(): JsonResponse
{
return $this->success([
'page' => new FrontendPageResource($this->pageService->page('call_to_action')),
]);
}
public function testimonial(): JsonResponse
{
return $this->success([
'page' => new FrontendPageResource($this->pageService->page('testimonial')),
]);
}
public function notFound(): JsonResponse
{
return $this->success([
'page' => new FrontendPageResource($this->pageService->page('not_found')),
]);
}
public function fetchUser(): JsonResponse
{
$user = auth()->user();
if (!$user) {
return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED);
}
return $this->success([
'user' => new FrontendUserResource($user),
]);
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Controllers\Api\Frontend;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Resources\Frontend\ProfileIconResource;
use App\Services\Frontend\ProfileIconService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
class InfoIconController extends BaseApiController
{
public function __construct(private ProfileIconService $service)
{
parent::__construct();
}
public function profileIcon(): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED);
}
$payload = $this->service->buildForUser($userId);
return $this->success([
'profile' => new ProfileIconResource($payload),
]);
}
}
@@ -0,0 +1,86 @@
<?php
namespace App\Http\Controllers\Api\Frontend;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Frontend\LandingTeacherRequest;
use App\Http\Resources\Frontend\LandingPageResource;
use App\Services\Frontend\LandingPageService;
use Illuminate\Http\JsonResponse;
class LandingPageController extends BaseApiController
{
public function __construct(private LandingPageService $service)
{
parent::__construct();
}
public function index(LandingTeacherRequest $request): JsonResponse
{
$payload = $this->service->dashboardForUser(
$request->user(),
$request->validated()['class_section_id'] ?? null
);
return $this->success([
'landing' => new LandingPageResource($payload),
]);
}
public function teacher(LandingTeacherRequest $request): JsonResponse
{
$payload = $this->service->teacher(
$request->user(),
$request->validated()['class_section_id'] ?? null
);
return $this->success([
'landing' => new LandingPageResource($payload),
]);
}
public function parent(): JsonResponse
{
$payload = $this->service->parent(auth()->user());
return $this->success([
'landing' => new LandingPageResource($payload),
]);
}
public function administrator(): JsonResponse
{
$payload = $this->service->administrator();
return $this->success([
'landing' => new LandingPageResource($payload),
]);
}
public function admin(): JsonResponse
{
$payload = $this->service->admin();
return $this->success([
'landing' => new LandingPageResource($payload),
]);
}
public function student(): JsonResponse
{
$payload = $this->service->student();
return $this->success([
'landing' => new LandingPageResource($payload),
]);
}
public function guest(): JsonResponse
{
$payload = $this->service->guest();
return $this->success([
'landing' => new LandingPageResource($payload),
]);
}
}
@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Api\Frontend;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Frontend\ContactSubmitRequest;
use App\Http\Resources\Frontend\ContactSubmissionResource;
use App\Http\Resources\Frontend\PageContentResource;
use App\Services\Frontend\ContactSubmissionService;
use App\Services\Frontend\StaticPageService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
class PageController extends BaseApiController
{
public function __construct(
private StaticPageService $pageService,
private ContactSubmissionService $contactService
) {
parent::__construct();
}
public function privacyPolicy(): JsonResponse
{
return $this->renderStatic('privacy_policy.html', 'privacy_policy');
}
public function termsOfService(): JsonResponse
{
return $this->renderStatic('terms_of_service.html', 'terms_of_service');
}
public function helpCenter(): JsonResponse
{
return $this->renderStatic('help_center.html', 'help_center');
}
public function submitContact(ContactSubmitRequest $request): JsonResponse
{
$result = $this->contactService->submit($request->validated());
return $this->success([
'submission' => new ContactSubmissionResource([
'email' => $request->validated('email'),
'email_sent' => $result['email_sent'],
'contact_id' => $result['record']?->id,
]),
], 'Message received.', Response::HTTP_CREATED);
}
private function renderStatic(string $filename, string $type): JsonResponse
{
$result = $this->pageService->load($filename, $type);
if (!$result['ok']) {
return $this->error($result['error'], Response::HTTP_NOT_FOUND);
}
return $this->success([
'page' => new PageContentResource($result),
]);
}
}
@@ -0,0 +1,281 @@
<?php
namespace App\Http\Controllers\Api\Grading;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Grading\BelowSixtyEmailRequest;
use App\Http\Requests\Grading\BelowSixtyMeetingRequest;
use App\Http\Requests\Grading\BelowSixtyMeetingSaveRequest;
use App\Http\Requests\Grading\BelowSixtySendRequest;
use App\Http\Requests\Grading\BelowSixtyStatusRequest;
use App\Http\Requests\Grading\BelowSixtyListRequest;
use App\Http\Requests\Grading\GradingLockAllRequest;
use App\Http\Requests\Grading\GradingLockRequest;
use App\Http\Requests\Grading\GradingOverviewRequest;
use App\Http\Requests\Grading\GradingRefreshRequest;
use App\Http\Requests\Grading\GradingScoreShowRequest;
use App\Http\Requests\Grading\GradingScoreUpdateRequest;
use App\Http\Requests\Grading\PlacementBatchRequest;
use App\Http\Requests\Grading\PlacementBatchUpdateRequest;
use App\Http\Requests\Grading\PlacementLevelsRequest;
use App\Http\Requests\Grading\PlacementLevelRequest;
use App\Http\Requests\Grading\PlacementRequest;
use App\Http\Resources\Grading\BelowSixtyStudentResource;
use App\Http\Resources\Grading\GradingScoreResource;
use App\Services\Grading\GradingBelowSixtyService;
use App\Services\Grading\GradingLockService;
use App\Services\Grading\GradingOverviewService;
use App\Services\Grading\GradingPlacementService;
use App\Services\Grading\GradingRefreshService;
use App\Services\Grading\GradingScoreService;
use Illuminate\Http\JsonResponse;
class GradingController extends BaseApiController
{
public function __construct(
private GradingOverviewService $overviewService,
private GradingScoreService $scoreService,
private GradingLockService $lockService,
private GradingRefreshService $refreshService,
private GradingPlacementService $placementService,
private GradingBelowSixtyService $belowSixtyService
) {
parent::__construct();
}
public function overview(GradingOverviewRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->overviewService->overview(
isset($payload['class_id']) ? (int) $payload['class_id'] : null,
$payload['semester'] ?? null,
$payload['school_year'] ?? null
);
return response()->json(['ok' => true] + $data);
}
public function show(GradingScoreShowRequest $request, string $type, int $classSectionId, int $studentId): JsonResponse
{
$payload = $request->validated();
$data = $this->scoreService->show(
$type,
$classSectionId,
$studentId,
(string) $payload['semester'],
(string) $payload['school_year']
);
return response()->json([
'ok' => true,
'student' => $data['student'],
'scores' => GradingScoreResource::collection($data['scores']),
'type' => $data['type'],
'class_section_id' => $data['class_section_id'],
'semester' => $data['semester'],
'scores_locked' => $data['scores_locked'],
]);
}
public function update(GradingScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$this->scoreService->update($payload, (int) (auth()->id() ?? 0));
return response()->json(['ok' => true]);
}
public function toggleLock(GradingLockRequest $request): JsonResponse
{
$payload = $request->validated();
$locked = $this->lockService->toggle(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true, 'locked' => $locked]);
}
public function lockAll(GradingLockAllRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->lockService->lockAll(
(string) $payload['semester'],
(string) $payload['school_year'],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true, 'locked_count' => $count]);
}
public function refresh(GradingRefreshRequest $request): JsonResponse
{
$payload = $request->validated();
$this->refreshService->refreshSemesterScores(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year']
);
return response()->json(['ok' => true]);
}
public function placement(PlacementRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->placementService->placementContext(
isset($payload['class_section_id']) ? (int) $payload['class_section_id'] : null,
(string) $payload['school_year'],
$payload['placement_test'] ?? null,
$payload['open'] ?? null
);
return response()->json(['ok' => true] + $data);
}
public function updatePlacementLevel(PlacementLevelRequest $request): JsonResponse
{
$payload = $request->validated();
$this->placementService->updatePlacementLevel(
(int) $payload['student_id'],
(string) $payload['school_year'],
$payload['placement_level'] ?? null,
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true]);
}
public function updatePlacementLevels(PlacementLevelsRequest $request): JsonResponse
{
$payload = $request->validated();
$this->placementService->updatePlacementLevels(
(int) $payload['class_section_id'],
(string) $payload['school_year'],
$payload['placement_level'] ?? [],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true]);
}
public function createPlacementBatch(PlacementBatchRequest $request): JsonResponse
{
$payload = $request->validated();
$batchId = $this->placementService->createPlacementBatch(
(string) $payload['school_year'],
(string) $payload['placement_test'],
$payload['placement_level'] ?? [],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true, 'batch_id' => $batchId]);
}
public function showPlacementBatch(int $batchId): JsonResponse
{
$data = $this->placementService->getPlacementBatch($batchId);
return response()->json(['ok' => true] + $data);
}
public function updatePlacementBatch(PlacementBatchUpdateRequest $request, int $batchId): JsonResponse
{
$payload = $request->validated();
$this->placementService->updatePlacementBatch(
$batchId,
(string) $payload['school_year'],
$payload['placement_level'] ?? [],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true]);
}
public function belowSixty(BelowSixtyListRequest $request): JsonResponse
{
$payload = $request->validated();
$rows = $this->belowSixtyService->listRows(
(string) $payload['school_year'],
(string) $payload['semester']
);
return response()->json([
'ok' => true,
'rows' => BelowSixtyStudentResource::collection($rows),
'semester' => $payload['semester'],
'school_year' => $payload['school_year'],
]);
}
public function belowSixtyEmail(BelowSixtyEmailRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->belowSixtyService->emailContext(
(int) $payload['student_id'],
(string) $payload['school_year'],
(string) $payload['semester']
);
return response()->json(['ok' => true] + $data);
}
public function sendBelowSixtyEmail(BelowSixtySendRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->belowSixtyService->emailContext(
(int) $payload['student_id'],
(string) $payload['school_year'],
(string) $payload['semester']
);
$subject = trim((string) ($payload['subject'] ?? ''));
if ($subject !== '') {
$data['subject'] = $subject;
}
if (isset($payload['html']) && trim((string) $payload['html']) !== '') {
$data['html'] = (string) $payload['html'];
}
$this->belowSixtyService->sendEmail($data);
return response()->json(['ok' => true]);
}
public function updateBelowSixtyStatus(BelowSixtyStatusRequest $request): JsonResponse
{
$payload = $request->validated();
$this->belowSixtyService->updateStatus(
(int) $payload['student_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
(string) $payload['status'],
(string) ($payload['note'] ?? ''),
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true]);
}
public function belowSixtyMeeting(BelowSixtyMeetingRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->belowSixtyService->meetingContext(
(int) $payload['student_id'],
(string) $payload['school_year'],
(string) $payload['semester']
);
return response()->json(['ok' => true] + $data);
}
public function saveBelowSixtyMeeting(BelowSixtyMeetingSaveRequest $request): JsonResponse
{
$payload = $request->validated();
$this->belowSixtyService->saveMeeting($payload, (int) (auth()->id() ?? 0));
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\Api\Grading;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Grading\HomeworkTrackingRequest;
use App\Http\Resources\Grading\HomeworkTrackingTeacherResource;
use App\Services\Grading\HomeworkTrackingService;
use Illuminate\Http\JsonResponse;
class HomeworkTrackingController extends BaseApiController
{
public function __construct(private HomeworkTrackingService $service)
{
parent::__construct();
}
public function index(HomeworkTrackingRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->report(
$payload['semester'] ?? null,
$payload['school_year'] ?? null,
isset($payload['page']) ? (int) $payload['page'] : 1
);
return response()->json([
'ok' => true,
'semester' => $data['semester'],
'school_year' => $data['school_year'],
'sundays' => $data['sundays'],
'event_days' => $data['event_days'],
'date_to_index' => $data['date_to_index'],
'teachers' => HomeworkTrackingTeacherResource::collection($data['teachers']),
'has_homework' => $data['has_homework'],
'hw_entered_at' => $data['hw_entered_at'],
'has_homework_by_date' => $data['has_homework_by_date'],
'hw_entered_at_by_date' => $data['hw_entered_at_by_date'],
'page' => $data['page'],
'total_pages' => $data['total_pages'],
'per_page' => $data['per_page'],
'total_rows' => $data['total_rows'],
]);
}
}
@@ -0,0 +1,182 @@
<?php
namespace App\Http\Controllers\Api\Incidents;
use App\Http\Controllers\Api\BaseApiController;
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\Resources\Incidents\IncidentAnalysisStudentResource;
use App\Http\Resources\Incidents\IncidentGradeResource;
use App\Http\Resources\Incidents\IncidentResource;
use App\Http\Resources\Incidents\IncidentStudentOptionResource;
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
{
public function __construct(
private CurrentIncidentService $currentService,
private IncidentHistoryService $historyService,
private IncidentAnalysisService $analysisService
) {
parent::__construct();
}
public function current(): JsonResponse
{
$data = $this->currentService->listCurrent();
return response()->json([
'ok' => true,
'incidents' => IncidentResource::collection($data['incidents']),
'grades' => IncidentGradeResource::collection($data['grades']),
]);
}
public function history(IncidentListRequest $request): JsonResponse
{
$payload = $request->validated();
$incidents = $this->historyService->history($payload['school_year'] ?? null, $payload['semester'] ?? null);
return response()->json([
'ok' => true,
'incidents' => IncidentResource::collection($incidents),
]);
}
public function processed(IncidentListRequest $request): JsonResponse
{
$payload = $request->validated();
$incidents = $this->historyService->processed($payload['school_year'] ?? null, $payload['semester'] ?? null);
return response()->json([
'ok' => true,
'incidents' => IncidentResource::collection($incidents),
]);
}
public function analysis(IncidentListRequest $request): JsonResponse
{
$payload = $request->validated();
$students = $this->analysisService->analyze($payload['school_year'] ?? null, $payload['semester'] ?? null);
return response()->json([
'ok' => true,
'students' => IncidentAnalysisStudentResource::collection($students),
'school_year' => $payload['school_year'] ?? null,
'semester' => $payload['semester'] ?? null,
]);
}
public function studentsByGrade(int $gradeId): JsonResponse
{
$students = $this->currentService->studentsByGrade($gradeId);
return response()->json([
'ok' => true,
'students' => IncidentStudentOptionResource::collection($students),
]);
}
public function store(Request $request): JsonResponse
{
$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'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to add incident.'], 422);
}
return response()->json([
'ok' => true,
'created' => $result['created'],
'incident_id' => $result['incident_id'],
]);
}
public function updateState(IncidentStateRequest $request, int $incidentId): JsonResponse
{
$payload = $request->validated();
$updated = $this->currentService->updateState($incidentId, (string) $payload['incident_state']);
return response()->json([
'ok' => $updated,
]);
}
public function close(Request $request, int $incidentId): JsonResponse
{
$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'],
$payload['action_taken'] ?? null,
(int) (auth()->id() ?? 0)
);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to close incident.'], 422);
}
return response()->json([
'ok' => true,
'incident_id' => $result['incident_id'],
]);
}
public function cancel(IncidentCancelRequest $request, int $incidentId): JsonResponse
{
$payload = $request->validated();
$result = $this->currentService->cancelIncident(
$incidentId,
$payload['state_description'] ?? null,
$payload['action_taken'] ?? null,
(int) (auth()->id() ?? 0)
);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to cancel incident.'], 422);
}
return response()->json([
'ok' => true,
'incident_id' => $result['incident_id'],
]);
}
}
@@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers\Api\Inventory;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Inventory\InventoryCategoryIndexRequest;
use App\Http\Requests\Inventory\InventoryCategoryStoreRequest;
use App\Http\Requests\Inventory\InventoryCategoryUpdateRequest;
use App\Http\Resources\Inventory\InventoryCategoryResource;
use App\Services\Inventory\InventoryCategoryService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
class InventoryCategoryController extends BaseApiController
{
public function __construct(private InventoryCategoryService $categories)
{
parent::__construct();
}
public function index(InventoryCategoryIndexRequest $request): JsonResponse
{
$payload = $request->validated();
$rows = $this->categories->list($payload['type'] ?? null);
return $this->success([
'categories' => InventoryCategoryResource::collection($rows),
]);
}
public function store(InventoryCategoryStoreRequest $request): JsonResponse
{
$result = $this->categories->create($request->validated());
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to save category.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->respondCreated(new InventoryCategoryResource($result['category']));
}
public function update(InventoryCategoryUpdateRequest $request, int $id): JsonResponse
{
try {
$result = $this->categories->update($id, $request->validated());
} catch (\InvalidArgumentException $e) {
return $this->error($e->getMessage(), Response::HTTP_UNPROCESSABLE_ENTITY);
}
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to update category.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success(new InventoryCategoryResource($result['category']));
}
public function destroy(int $id): JsonResponse
{
$result = $this->categories->delete($id);
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to delete category.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->respondDeleted();
}
}
@@ -0,0 +1,154 @@
<?php
namespace App\Http\Controllers\Api\Inventory;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Inventory\InventoryAdjustRequest;
use App\Http\Requests\Inventory\InventoryAuditRequest;
use App\Http\Requests\Inventory\InventoryItemIndexRequest;
use App\Http\Requests\Inventory\InventoryItemStoreRequest;
use App\Http\Requests\Inventory\InventoryItemUpdateRequest;
use App\Http\Requests\Inventory\InventoryTeacherDistributionRequest;
use App\Http\Requests\Inventory\InventoryTeacherDistributionStoreRequest;
use App\Http\Resources\Inventory\InventoryItemResource;
use App\Services\Inventory\InventoryItemService;
use App\Services\Inventory\InventorySummaryService;
use App\Services\Inventory\InventoryTeacherDistributionService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class InventoryController extends BaseApiController
{
public function __construct(
private InventoryItemService $items,
private InventorySummaryService $summary,
private InventoryTeacherDistributionService $distribution
) {
parent::__construct();
}
public function index(InventoryItemIndexRequest $request): JsonResponse
{
$result = $this->items->list($request->validated());
return $this->success([
'type' => $result['type'],
'items' => InventoryItemResource::collection($result['items']),
'categories' => $result['categories'],
'conditionOptions' => $result['conditionOptions'],
'schoolYears' => $result['schoolYears'],
'selectedYear' => $result['selectedYear'],
'currentYear' => $result['currentYear'],
'semester' => $result['semester'],
'userNames' => $result['userNames'],
]);
}
public function show(int $id): JsonResponse
{
$item = $this->items->find($id);
if (!$item) {
return $this->error('Item not found.', Response::HTTP_NOT_FOUND);
}
return $this->success(new InventoryItemResource($item));
}
public function store(InventoryItemStoreRequest $request): JsonResponse
{
$result = $this->items->create($request->validated(), (int) auth()->id());
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to create item.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->respondCreated(new InventoryItemResource($result['item']));
}
public function update(InventoryItemUpdateRequest $request, int $id): JsonResponse
{
$result = $this->items->update($id, $request->validated(), (int) auth()->id());
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to update item.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success(new InventoryItemResource($result['item']));
}
public function destroy(int $id): JsonResponse
{
$result = $this->items->delete($id);
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to delete item.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->respondDeleted();
}
public function audit(InventoryAuditRequest $request, int $id): JsonResponse
{
$result = $this->items->auditClassroom($id, $request->validated(), (int) auth()->id());
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to audit item.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success(new InventoryItemResource($result['item']));
}
public function adjust(InventoryAdjustRequest $request, int $id): JsonResponse
{
$result = $this->items->adjustStock($id, $request->validated(), (int) auth()->id());
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to adjust stock.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success();
}
public function summary(string $type): JsonResponse
{
$payload = $this->summary->summary($type, request()->query('school_year'));
return $this->success($payload);
}
public function summaryAll(): JsonResponse
{
$payload = $this->summary->summaryAll(
request()->query('school_year'),
request()->query('type')
);
return $this->success($payload);
}
public function teacherDistribution(InventoryTeacherDistributionRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->distribution->formData(
(int) auth()->id(),
$payload['class_section_id'] ?? null,
$payload['item_id'] ?? null
);
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Unable to load distribution form.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success($result);
}
public function teacherDistributionStore(InventoryTeacherDistributionStoreRequest $request): JsonResponse
{
try {
$result = $this->distribution->distribute((int) auth()->id(), $request->validated());
} catch (\Throwable $e) {
Log::error('Inventory distribution failed: ' . $e->getMessage());
return $this->error('Unable to distribute inventory.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Unable to distribute inventory.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success($result);
}
}
@@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers\Api\Inventory;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Inventory\InventoryMovementBulkDeleteRequest;
use App\Http\Requests\Inventory\InventoryMovementIndexRequest;
use App\Http\Requests\Inventory\InventoryMovementStoreRequest;
use App\Http\Requests\Inventory\InventoryMovementUpdateRequest;
use App\Http\Resources\Inventory\InventoryMovementResource;
use App\Services\Inventory\InventoryMovementService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
class InventoryMovementController extends BaseApiController
{
public function __construct(private InventoryMovementService $movements)
{
parent::__construct();
}
public function index(InventoryMovementIndexRequest $request): JsonResponse
{
$rows = $this->movements->list($request->validated());
return $this->success([
'movements' => InventoryMovementResource::collection($rows),
]);
}
public function store(InventoryMovementStoreRequest $request): JsonResponse
{
$result = $this->movements->create($request->validated(), (int) auth()->id());
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to create movement.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->respondCreated();
}
public function update(InventoryMovementUpdateRequest $request, int $id): JsonResponse
{
$result = $this->movements->update($id, $request->validated());
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to update movement.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success();
}
public function destroy(int $id): JsonResponse
{
$result = $this->movements->delete($id);
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to delete movement.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->respondDeleted();
}
public function bulkDelete(InventoryMovementBulkDeleteRequest $request): JsonResponse
{
$result = $this->movements->bulkDelete($request->validated()['ids']);
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Bulk delete failed.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success($result);
}
}
@@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Api\Inventory;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Inventory\SupplierIndexRequest;
use App\Http\Requests\Inventory\SupplierStoreRequest;
use App\Http\Requests\Inventory\SupplierUpdateRequest;
use App\Http\Resources\Inventory\SupplierResource;
use App\Services\Inventory\SupplierService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
class SupplierController extends BaseApiController
{
public function __construct(private SupplierService $suppliers)
{
parent::__construct();
}
public function index(SupplierIndexRequest $request): JsonResponse
{
$payload = $request->validated();
$perPage = (int) ($payload['per_page'] ?? 20);
$result = $this->suppliers->list($payload, $perPage);
return $this->success([
'suppliers' => SupplierResource::collection($result['items']),
'pagination' => $result['pagination'],
]);
}
public function store(SupplierStoreRequest $request): JsonResponse
{
$result = $this->suppliers->create($request->validated());
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to save supplier.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->respondCreated(new SupplierResource($result['supplier']));
}
public function update(SupplierUpdateRequest $request, int $id): JsonResponse
{
$result = $this->suppliers->update($id, $request->validated());
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to update supplier.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success(new SupplierResource($result['supplier']));
}
public function destroy(int $id): JsonResponse
{
$result = $this->suppliers->delete($id);
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to delete supplier.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->respondDeleted();
}
}
@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Api\Inventory;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Inventory\SupplyCategoryStoreRequest;
use App\Http\Requests\Inventory\SupplyCategoryUpdateRequest;
use App\Http\Resources\Inventory\SupplyCategoryResource;
use App\Services\Inventory\SupplyCategoryService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
class SupplyCategoryController extends BaseApiController
{
public function __construct(private SupplyCategoryService $categories)
{
parent::__construct();
}
public function index(): JsonResponse
{
$rows = $this->categories->list();
return $this->success([
'categories' => SupplyCategoryResource::collection($rows),
]);
}
public function store(SupplyCategoryStoreRequest $request): JsonResponse
{
$result = $this->categories->create($request->validated());
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to save category.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->respondCreated(new SupplyCategoryResource($result['category']));
}
public function update(SupplyCategoryUpdateRequest $request, int $id): JsonResponse
{
$result = $this->categories->update($id, $request->validated());
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to update category.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success(new SupplyCategoryResource($result['category']));
}
public function destroy(int $id): JsonResponse
{
$result = $this->categories->delete($id);
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to delete category.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->respondDeleted();
}
}
@@ -0,0 +1,222 @@
<?php
namespace App\Http\Controllers\Api\Messaging;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Messaging\MessageIndexRequest;
use App\Http\Requests\Messaging\MessageSendRequest;
use App\Http\Requests\Messaging\MessageUpdateRequest;
use App\Http\Resources\Messaging\MessageCollection;
use App\Http\Resources\Messaging\MessageResource;
use App\Http\Resources\Messaging\RecipientResource;
use App\Models\Message;
use App\Services\Messaging\MessageCommandService;
use App\Services\Messaging\MessageQueryService;
use App\Services\Messaging\MessageRecipientService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class MessagesController extends BaseApiController
{
public function __construct(
private MessageQueryService $queryService,
private MessageCommandService $commandService,
private MessageRecipientService $recipientService
) {
parent::__construct();
}
public function index(MessageIndexRequest $request): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED);
}
$filters = $request->validated();
$page = (int) ($filters['page'] ?? 1);
$perPage = (int) ($filters['per_page'] ?? 20);
$this->authorize('viewAny', Message::class);
$role = $this->queryService->getUserRoleName($userId);
$messages = $this->queryService->inbox($userId, $filters, $page, $perPage);
$collection = new MessageCollection($messages);
$meta = $collection->with($request)['meta'] ?? null;
return $this->success([
'role' => $role,
'messages' => $collection->toArray($request),
'meta' => $meta,
]);
}
public function inbox(MessageIndexRequest $request): JsonResponse
{
return $this->listMessages($request, 'inbox');
}
public function sent(MessageIndexRequest $request): JsonResponse
{
return $this->listMessages($request, 'sent');
}
public function drafts(MessageIndexRequest $request): JsonResponse
{
return $this->listMessages($request, 'drafts');
}
public function trash(MessageIndexRequest $request): JsonResponse
{
return $this->listMessages($request, 'trash');
}
public function show(int $id): JsonResponse
{
$message = $this->queryService->findForUser($id);
if (!$message) {
return $this->error('Message not found.', Response::HTTP_NOT_FOUND);
}
$this->authorize('view', $message);
$userId = (int) (auth()->id() ?? 0);
if ($message->recipient_id === $userId) {
$this->commandService->markRead($message);
$message->refresh();
}
return $this->success([
'message' => new MessageResource($message),
]);
}
public function store(MessageSendRequest $request): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED);
}
$this->authorize('create', Message::class);
try {
$message = $this->commandService->create($userId, $request->validated(), $request->file('attachment'));
} catch (\Throwable $e) {
Log::error('Message send failed: ' . $e->getMessage());
return $this->error($e->getMessage(), Response::HTTP_BAD_REQUEST);
}
return $this->success([
'message' => new MessageResource($message->fresh(['sender', 'recipient'])),
], 'Message sent.', Response::HTTP_CREATED);
}
public function receive(): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED);
}
$messages = $this->queryService->receivedAndMarkRead($userId);
return $this->success([
'messages' => (new MessageCollection(collect($messages)))->toArray(request()),
]);
}
public function recipients(string $type): JsonResponse
{
$type = strtolower(trim($type));
$data = match ($type) {
'teacher' => $this->recipientService->teachers(),
'parent' => $this->recipientService->parents(),
default => null,
};
if ($data === null) {
return $this->error('Invalid recipient type.', Response::HTTP_BAD_REQUEST);
}
$payload = array_map(fn ($row) => (new RecipientResource($row))->toArray(request()), $data);
return $this->success([
'recipients' => $payload,
]);
}
public function update(MessageUpdateRequest $request, int $id): JsonResponse
{
$message = Message::query()->find($id);
if (!$message) {
return $this->error('Message not found.', Response::HTTP_NOT_FOUND);
}
$this->authorize('update', $message);
try {
$updated = $this->commandService->update($message, $request->validated());
} catch (\Throwable $e) {
Log::error('Message update failed: ' . $e->getMessage());
return $this->error('Unable to update message.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success([
'message' => new MessageResource($updated->load(['sender', 'recipient'])),
], 'Message updated.');
}
public function destroy(int $id): JsonResponse
{
$message = Message::query()->find($id);
if (!$message) {
return $this->error('Message not found.', Response::HTTP_NOT_FOUND);
}
$this->authorize('delete', $message);
try {
$trashed = $this->commandService->trash($message);
} catch (\Throwable $e) {
Log::error('Message delete failed: ' . $e->getMessage());
return $this->error('Unable to delete message.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!$trashed) {
return $this->error('Unable to delete message.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success(null, 'Message deleted.');
}
private function listMessages(MessageIndexRequest $request, string $bucket): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED);
}
$filters = $request->validated();
$page = (int) ($filters['page'] ?? 1);
$perPage = (int) ($filters['per_page'] ?? 20);
$this->authorize('viewAny', Message::class);
$paginator = match ($bucket) {
'sent' => $this->queryService->sent($userId, $filters, $page, $perPage),
'drafts' => $this->queryService->drafts($userId, $filters, $page, $perPage),
'trash' => $this->queryService->trash($userId, $filters, $page, $perPage),
default => $this->queryService->inbox($userId, $filters, $page, $perPage),
};
$collection = new MessageCollection($paginator);
$meta = $collection->with($request)['meta'] ?? null;
return $this->success([
'messages' => $collection->toArray($request),
'meta' => $meta,
]);
}
}
@@ -0,0 +1,156 @@
<?php
namespace App\Http\Controllers\Api\Messaging;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Whatsapp\WhatsappInviteSendRequest;
use App\Http\Requests\Whatsapp\WhatsappLinkIndexRequest;
use App\Http\Requests\Whatsapp\WhatsappLinkStoreRequest;
use App\Http\Requests\Whatsapp\WhatsappLinkUpdateRequest;
use App\Http\Requests\Whatsapp\WhatsappMembershipUpdateRequest;
use App\Http\Requests\Whatsapp\WhatsappParentContactsByClassRequest;
use App\Http\Requests\Whatsapp\WhatsappParentContactsRequest;
use App\Http\Resources\Whatsapp\WhatsappClassSectionContactResource;
use App\Http\Resources\Whatsapp\WhatsappGroupLinkCollection;
use App\Http\Resources\Whatsapp\WhatsappGroupLinkResource;
use App\Http\Resources\Whatsapp\WhatsappParentContactResource;
use App\Services\Whatsapp\WhatsappContactService;
use App\Services\Whatsapp\WhatsappContextService;
use App\Services\Whatsapp\WhatsappInviteService;
use App\Services\Whatsapp\WhatsappLinkService;
use App\Services\Whatsapp\WhatsappMembershipService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class WhatsappController extends BaseApiController
{
public function __construct(
private WhatsappLinkService $linkService,
private WhatsappContactService $contactService,
private WhatsappInviteService $inviteService,
private WhatsappMembershipService $membershipService,
private WhatsappContextService $context
) {
parent::__construct();
}
public function index(WhatsappLinkIndexRequest $request): JsonResponse
{
$links = $this->linkService->paginate($request->validated());
$collection = new WhatsappGroupLinkCollection($links);
return $this->success([
'links' => $collection->toArray($request),
'meta' => $collection->with($request)['meta'],
]);
}
public function show(int $linkId): JsonResponse
{
$link = $this->linkService->findOrFail($linkId);
return $this->success([
'link' => new WhatsappGroupLinkResource($link),
]);
}
public function store(WhatsappLinkStoreRequest $request): JsonResponse
{
$link = $this->linkService->upsert($request->validated());
$code = $link->wasRecentlyCreated ? Response::HTTP_CREATED : Response::HTTP_OK;
return $this->success([
'link' => new WhatsappGroupLinkResource($link),
], 'WhatsApp group link saved.', $code);
}
public function update(WhatsappLinkUpdateRequest $request, int $linkId): JsonResponse
{
$link = $this->linkService->update($linkId, $request->validated());
return $this->success([
'link' => new WhatsappGroupLinkResource($link),
], 'WhatsApp group link updated.');
}
public function destroy(int $linkId): JsonResponse
{
$this->linkService->delete($linkId);
return $this->success(null, 'WhatsApp group link deleted.');
}
public function parentContacts(WhatsappParentContactsRequest $request): JsonResponse
{
$payload = $request->validated();
$schoolYear = $this->context->schoolYear($payload['school_year'] ?? null);
$semester = array_key_exists('semester', $payload)
? (string) ($payload['semester'] ?? '')
: $this->context->semester();
$contacts = $this->contactService->listParentContacts($schoolYear, $semester);
return $this->success([
'contacts' => WhatsappParentContactResource::collection($contacts),
]);
}
public function parentContactsByClass(WhatsappParentContactsByClassRequest $request): JsonResponse
{
$payload = $request->validated();
$schoolYear = $this->context->schoolYear($payload['school_year'] ?? null);
$semester = array_key_exists('semester', $payload)
? (string) ($payload['semester'] ?? '')
: $this->context->semester();
$sections = $this->contactService->listParentContactsByClass($schoolYear, $semester);
return $this->success([
'class_sections' => WhatsappClassSectionContactResource::collection($sections),
]);
}
public function sendInvites(WhatsappInviteSendRequest $request): JsonResponse
{
try {
$result = $this->inviteService->send($request->validated());
} catch (\Throwable $e) {
Log::error('WhatsApp invite send failed: ' . $e->getMessage());
return $this->error('Invite processing failed.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Invite processing failed.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success([
'triggered' => (int) ($result['triggered'] ?? 0),
], 'Invite processing started.');
}
public function updateMembership(WhatsappMembershipUpdateRequest $request): JsonResponse
{
try {
$userId = (int) (auth()->id() ?? 0) ?: null;
$result = $this->membershipService->updateMembership($request->validated(), $userId);
} catch (\Throwable $e) {
Log::error('WhatsApp membership update failed: ' . $e->getMessage());
return $this->error('Failed to save membership.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!($result['ok'] ?? false)) {
$code = str_contains((string) ($result['message'] ?? ''), 'missing')
? Response::HTTP_INTERNAL_SERVER_ERROR
: Response::HTTP_UNPROCESSABLE_ENTITY;
return $this->error($result['message'] ?? 'Failed to save membership.', $code);
}
return $this->success([
'saved' => $result['saved'] ?? [],
'term' => $result['term'] ?? [],
], 'Membership saved.');
}
}
@@ -0,0 +1,165 @@
<?php
namespace App\Http\Controllers\Api\Notifications;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Notifications\NotificationActiveRequest;
use App\Http\Requests\Notifications\NotificationDeletedRequest;
use App\Http\Requests\Notifications\NotificationIndexRequest;
use App\Http\Requests\Notifications\NotificationSendRequest;
use App\Http\Requests\Notifications\NotificationUpdateRequest;
use App\Http\Resources\Notifications\NotificationResource;
use App\Http\Resources\Notifications\UserNotificationResource;
use App\Services\Notifications\NotificationActiveService;
use App\Services\Notifications\NotificationDeletedService;
use App\Services\Notifications\NotificationManagementService;
use App\Services\Notifications\NotificationReadService;
use App\Services\Notifications\NotificationSendService;
use App\Services\Notifications\NotificationShowService;
use App\Services\Notifications\NotificationUserListService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
class NotificationController extends BaseApiController
{
public function __construct(
private NotificationUserListService $listService,
private NotificationSendService $sendService,
private NotificationReadService $readService,
private NotificationActiveService $activeService,
private NotificationDeletedService $deletedService,
private NotificationManagementService $managementService,
private NotificationShowService $showService
) {
parent::__construct();
}
public function index(NotificationIndexRequest $request): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$payload = $request->validated();
$result = $this->listService->listForUser($userId, $payload);
return response()->json([
'ok' => true,
'notifications' => UserNotificationResource::collection($result['notifications']),
'pagination' => $result['pagination'],
]);
}
public function show(int $notificationId): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$row = $this->showService->getForUser($userId, $notificationId);
if (!$row) {
return response()->json(['ok' => false, 'message' => 'Notification not found.'], 404);
}
return response()->json([
'ok' => true,
'notification' => new UserNotificationResource($row),
]);
}
public function store(NotificationSendRequest $request): JsonResponse
{
$payload = $request->validated();
$actorId = (int) (auth()->id() ?? 0);
try {
$result = $this->sendService->send($payload, $actorId);
} catch (\Throwable $e) {
Log::error('Notification send failed.', ['error' => $e->getMessage()]);
return response()->json(['ok' => false, 'message' => 'Failed to send notification.'], 500);
}
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => 'Failed to send notification.'], 422);
}
return response()->json([
'ok' => true,
'notification' => new NotificationResource($result['notification']),
'recipient_count' => $result['recipient_count'] ?? 0,
], 201);
}
public function update(NotificationUpdateRequest $request, int $notificationId): JsonResponse
{
$payload = $request->validated();
$result = $this->managementService->update($notificationId, $payload);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to update notification.'], 404);
}
return response()->json([
'ok' => true,
'notification' => new NotificationResource($result['notification']),
]);
}
public function destroy(int $notificationId): JsonResponse
{
$deleted = $this->managementService->delete($notificationId);
if (!$deleted) {
return response()->json(['ok' => false, 'message' => 'Notification not found.'], 404);
}
return response()->json(['ok' => true]);
}
public function restore(int $notificationId): JsonResponse
{
$restored = $this->managementService->restore($notificationId);
if (!$restored) {
return response()->json(['ok' => false, 'message' => 'Notification not found.'], 404);
}
return response()->json(['ok' => true]);
}
public function markRead(int $notificationId): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$ok = $this->readService->markRead($userId, $notificationId);
if (!$ok) {
return response()->json(['ok' => false, 'message' => 'Notification not found.'], 404);
}
return response()->json(['ok' => true]);
}
public function active(NotificationActiveRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->activeService->list($payload['target_group'] ?? null);
return response()->json([
'ok' => true,
'notifications' => $data['notifications'],
]);
}
public function deleted(NotificationDeletedRequest $request): JsonResponse
{
$data = $this->deletedService->list();
return response()->json([
'ok' => true,
'notifications' => $data['notifications'],
]);
}
}
@@ -0,0 +1,108 @@
<?php
namespace App\Http\Controllers\Api\Parents;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Parents\ParentAttendanceReportSubmitRequest;
use App\Http\Requests\Parents\ParentAttendanceReportListRequest;
use App\Http\Requests\Parents\ParentAttendanceReportCheckRequest;
use App\Http\Requests\Parents\ParentAttendanceReportUpdateRequest;
use App\Http\Resources\Parents\ParentAttendanceReportResource;
use App\Services\Parents\ParentAttendanceReportService;
use Illuminate\Http\JsonResponse;
class ParentAttendanceReportController extends BaseApiController
{
public function __construct(private ParentAttendanceReportService $reportService)
{
parent::__construct();
}
public function form(): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$data = $this->reportService->formData($parentId);
return response()->json([
'ok' => true,
'students' => $data['students'],
'today' => $data['today'],
'sundays' => $data['sundays'],
'defaultDate' => $data['defaultDate'],
'myReports' => ParentAttendanceReportResource::collection($data['myReports']),
'cutoffThreshold' => $data['cutoffThreshold'],
]);
}
public function submit(ParentAttendanceReportSubmitRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
try {
$result = $this->reportService->submit($parentId, $request->validated());
} catch (\RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
}
return response()->json([
'ok' => true,
'inserted' => $result['inserted'],
'blocked' => $result['blocked'],
'students' => $result['students'],
'dates' => $result['dates'],
], 201);
}
public function list(ParentAttendanceReportListRequest $request): JsonResponse
{
$payload = $request->validated();
$start = $payload['start'] ?? now()->toDateString();
$end = $payload['end'] ?? null;
$schoolYear = $payload['school_year'] ?? null;
$semester = $payload['semester'] ?? null;
$rows = $this->reportService->listReports($start, $end, $schoolYear, $semester);
return response()->json([
'ok' => true,
'reports' => ParentAttendanceReportResource::collection($rows),
]);
}
public function checkExisting(ParentAttendanceReportCheckRequest $request): JsonResponse
{
try {
$rows = $this->reportService->checkExisting($request->validated());
} catch (\RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
}
return response()->json([
'ok' => true,
'students' => $rows,
]);
}
public function update(ParentAttendanceReportUpdateRequest $request, int $reportId): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
try {
$this->reportService->updateReport($parentId, $reportId, $request->validated());
} catch (\RuntimeException $e) {
return response()->json(['ok' => false, 'message' => $e->getMessage()], 422);
}
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,284 @@
<?php
namespace App\Http\Controllers\Api\Parents;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Parents\ParentAttendanceRequest;
use App\Http\Requests\Parents\ParentEnrollmentActionRequest;
use App\Http\Requests\Parents\ParentEnrollmentRequest;
use App\Http\Requests\Parents\ParentEmergencyContactRequest;
use App\Http\Requests\Parents\ParentEventParticipationRequest;
use App\Http\Requests\Parents\ParentInvoiceRequest;
use App\Http\Requests\Parents\ParentProfileUpdateRequest;
use App\Http\Requests\Parents\ParentRegistrationRequest;
use App\Http\Requests\Parents\ParentStudentUpdateRequest;
use App\Http\Resources\Parents\ParentAttendanceResource;
use App\Http\Resources\Parents\ParentEmergencyContactResource;
use App\Http\Resources\Parents\ParentStudentResource;
use App\Http\Resources\Invoices\InvoiceResource;
use App\Services\Parents\ParentAttendanceService;
use App\Services\Parents\ParentEmergencyContactService;
use App\Services\Parents\ParentEnrollmentService;
use App\Services\Parents\ParentEventParticipationService;
use App\Services\Parents\ParentInvoiceService;
use App\Services\Parents\ParentProfileService;
use App\Services\Parents\ParentRegistrationService;
use Illuminate\Http\JsonResponse;
class ParentController extends BaseApiController
{
public function __construct(
private ParentAttendanceService $attendanceService,
private ParentInvoiceService $invoiceService,
private ParentEnrollmentService $enrollmentService,
private ParentRegistrationService $registrationService,
private ParentEmergencyContactService $emergencyContactService,
private ParentProfileService $profileService,
private ParentEventParticipationService $eventService
) {
parent::__construct();
}
public function attendance(ParentAttendanceRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$data = $this->attendanceService->listAttendance($parentId, $request->validated()['school_year'] ?? null);
return response()->json([
'ok' => true,
'attendance' => ParentAttendanceResource::collection($data['attendance']),
'schoolYears' => $data['schoolYears'],
'selectedYear' => $data['selectedYear'],
]);
}
public function invoices(ParentInvoiceRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$schoolYear = $request->validated()['school_year'] ?? null;
$rows = $this->invoiceService->listInvoices($parentId, $schoolYear);
return response()->json([
'ok' => true,
'invoices' => InvoiceResource::collection($rows),
]);
}
public function enrollments(ParentEnrollmentRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$data = $this->enrollmentService->overview($parentId, $request->validated()['school_year'] ?? null);
return response()->json([
'ok' => true,
'students' => ParentStudentResource::collection($data['students']),
'schoolYears' => $data['schoolYears'],
'selectedYear' => $data['selectedYear'],
'withdrawalDeadline' => $data['withdrawalDeadline'],
'lastDayOfRegistration' => $data['lastDayOfRegistration'],
'schoolStartDate' => $data['schoolStartDate'],
]);
}
public function updateEnrollments(ParentEnrollmentActionRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$payload = $request->validated();
$result = $this->enrollmentService->updateEnrollment(
$parentId,
$payload['enroll'] ?? [],
$payload['withdraw'] ?? []
);
return response()->json([
'ok' => true,
'enrolled' => $result['enrolled'],
'withdrawn' => $result['withdrawn'],
]);
}
public function registration(): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$data = $this->registrationService->overview($parentId);
return response()->json([
'ok' => true,
'parent' => $data['parent'],
'existingKids' => ParentStudentResource::collection($data['existingKids']),
'emergencies' => ParentEmergencyContactResource::collection($data['emergencies']),
'maxChilds' => $data['maxChilds'],
'maxEmergency' => $data['maxEmergency'],
'lastDayOfRegistration' => $data['lastDayOfRegistration'],
'registrationAgeDeadline' => $data['registrationAgeDeadline'],
]);
}
public function storeRegistration(ParentRegistrationRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$payload = $request->validated();
$result = $this->registrationService->register(
$parentId,
$payload['students'] ?? [],
$payload['emergency_contacts'] ?? []
);
return response()->json([
'ok' => true,
'created_student_ids' => $result['created_student_ids'],
], 201);
}
public function updateStudent(ParentStudentUpdateRequest $request, int $studentId): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$this->registrationService->updateStudent($parentId, $studentId, $request->validated());
return response()->json(['ok' => true]);
}
public function deleteStudent(int $studentId): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$this->registrationService->deleteStudent($parentId, $studentId);
return response()->json(['ok' => true]);
}
public function emergencyContacts(): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$rows = $this->emergencyContactService->list($parentId);
return response()->json([
'ok' => true,
'contacts' => ParentEmergencyContactResource::collection($rows),
]);
}
public function storeEmergencyContact(ParentEmergencyContactRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$contact = $this->emergencyContactService->store($parentId, $request->validated());
return response()->json([
'ok' => true,
'contact' => new ParentEmergencyContactResource($contact),
], 201);
}
public function updateEmergencyContact(ParentEmergencyContactRequest $request, int $contactId): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$contact = $this->emergencyContactService->update($parentId, $contactId, $request->validated());
return response()->json([
'ok' => true,
'contact' => new ParentEmergencyContactResource($contact),
]);
}
public function profile(): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$profile = $this->profileService->getProfile($parentId);
return response()->json([
'ok' => true,
'profile' => $profile,
]);
}
public function updateProfile(ParentProfileUpdateRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$profile = $this->profileService->updateProfile($parentId, $request->validated());
return response()->json([
'ok' => true,
'profile' => $profile,
]);
}
public function events(): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$data = $this->eventService->overview($parentId);
return response()->json([
'ok' => true,
'activeEvents' => $data['activeEvents'],
'charges' => $data['charges'],
'yourStudents' => $data['yourStudents'],
]);
}
public function updateParticipation(ParentEventParticipationRequest $request): JsonResponse
{
$parentId = (int) (auth()->id() ?? 0);
if ($parentId <= 0) {
return response()->json(['ok' => false, 'message' => 'Unauthorized.'], 401);
}
$this->eventService->updateParticipation($parentId, $request->validated()['participation'] ?? []);
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,105 @@
<?php
namespace App\Http\Controllers\Api\Reports;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Files\FileNameRequest;
use App\Http\Resources\Files\FileMetaResource;
use App\Services\Files\ExamDraftDownloadNameService;
use App\Services\Files\FileServeService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class FilesController extends BaseApiController
{
public function __construct(
private FileServeService $fileService,
private ExamDraftDownloadNameService $draftNameService
) {
parent::__construct();
}
public function receipt(FileNameRequest $request, string $name): Response|JsonResponse
{
return $this->serveFile(
$request,
storage_path('uploads/receipts'),
$name,
['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf'],
null,
false
);
}
public function reimb(FileNameRequest $request, string $name): Response|JsonResponse
{
return $this->serveFile(
$request,
storage_path('uploads/reimbursements'),
$name,
['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf'],
null,
true
);
}
public function earlyDismissalSignature(FileNameRequest $request, string $name): Response|JsonResponse
{
return $this->serveFile(
$request,
storage_path('uploads/early_dismissal_signatures'),
$name,
['jpg', 'jpeg', 'png', 'webp', 'gif', 'pdf'],
null,
true
);
}
public function examDraftTeacher(FileNameRequest $request, string $name): Response|JsonResponse
{
$downloadName = $this->draftNameService->build($name, 'drafts');
return $this->serveFile(
$request,
storage_path('uploads/exams/drafts'),
$name,
['doc', 'docx', 'pdf'],
$downloadName,
true
);
}
public function examDraftFinal(FileNameRequest $request, string $name): Response|JsonResponse
{
$downloadName = $this->draftNameService->build($name, 'finals');
return $this->serveFile(
$request,
storage_path('uploads/exams/finals'),
$name,
['doc', 'docx', 'pdf'],
$downloadName,
true
);
}
private function serveFile(
FileNameRequest $request,
string $baseDir,
string $name,
array $allowedExtensions,
?string $downloadName,
bool $nosniff
): Response|JsonResponse {
if ($request->boolean('meta')) {
$meta = $this->fileService->meta($baseDir, $name, $allowedExtensions, $downloadName);
return response()->json([
'ok' => true,
'file' => new FileMetaResource($meta),
]);
}
return $this->fileService->serveInline($baseDir, $name, $allowedExtensions, $request, $downloadName, $nosniff);
}
}
@@ -0,0 +1,145 @@
<?php
namespace App\Http\Controllers\Api\Reports;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Reports\ReportCards\ReportCardAcknowledgementRequest;
use App\Http\Requests\Reports\ReportCards\ReportCardCompletenessRequest;
use App\Http\Requests\Reports\ReportCards\ReportCardMetaRequest;
use App\Http\Requests\Reports\ReportCards\ReportCardPdfRequest;
use App\Http\Resources\Reports\ReportCards\ReportCardAcknowledgementResource;
use App\Http\Resources\Reports\ReportCards\ReportCardClassSectionResource;
use App\Http\Resources\Reports\ReportCards\ReportCardCompletenessStudentResource;
use App\Http\Resources\Reports\ReportCards\ReportCardStudentMetaResource;
use App\Models\Configuration;
use App\Services\Reports\ReportCards\ReportCardService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class ReportCardsController extends BaseApiController
{
public function __construct(private ReportCardService $service)
{
parent::__construct();
}
public function meta(ReportCardMetaRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->service->meta(
$payload['school_year'] ?? null,
$payload['semester'] ?? null
);
return $this->success([
'schoolYears' => $result['schoolYears'] ?? [],
'selectedYear' => $result['selectedYear'] ?? null,
'semesters' => $result['semesters'] ?? [],
'selectedSemester' => $result['selectedSemester'] ?? null,
'students' => ReportCardStudentMetaResource::collection($result['students'] ?? []),
'classSections' => ReportCardClassSectionResource::collection($result['classSections'] ?? []),
]);
}
public function completeness(ReportCardCompletenessRequest $request): JsonResponse
{
$payload = $request->validated();
$schoolYear = $this->resolveSchoolYear($payload['school_year'] ?? null);
$semester = $this->resolveSemester($payload['semester'] ?? null);
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
$result = $this->service->completeness($schoolYear, $semester, $classSectionId);
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Unable to generate completeness report.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success([
'summary' => $result['summary'] ?? null,
'students' => ReportCardCompletenessStudentResource::collection($result['students'] ?? []),
'class_section_name' => $result['class_section_name'] ?? null,
'class_section_id' => $result['class_section_id'] ?? null,
'school_year' => $result['school_year'] ?? $schoolYear,
'semester' => $result['semester'] ?? $semester,
'roster_semester' => $result['roster_semester'] ?? null,
'used_fallback' => (bool) ($result['used_fallback'] ?? false),
], $result['message'] ?? 'Success');
}
public function acknowledgement(ReportCardAcknowledgementRequest $request): JsonResponse
{
$payload = $request->validated();
$studentId = (int) ($payload['student_id'] ?? 0);
$schoolYear = $this->resolveSchoolYear($payload['school_year'] ?? null);
$semester = $this->resolveSemester($payload['semester'] ?? null);
$result = $this->service->acknowledgement($studentId, $schoolYear, $semester);
return $this->success(new ReportCardAcknowledgementResource($result));
}
public function studentReport(ReportCardPdfRequest $request, int $studentId)
{
try {
$result = $this->service->generateSingleReport($studentId, $request->validated());
} catch (\Throwable $e) {
Log::error('Report card PDF generation failed: ' . $e->getMessage());
return $this->error('Failed to generate report card.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Unable to generate report card.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$disposition = ($request->boolean('download') ? 'attachment' : 'inline');
return response($result['pdf'], 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => $disposition . '; filename="' . ($result['filename'] ?? 'ReportCard.pdf') . '"',
'Cache-Control' => 'private, max-age=0, must-revalidate',
'Pragma' => 'public',
]);
}
public function classReport(ReportCardPdfRequest $request, int $classSectionId)
{
try {
$result = $this->service->generateClassReport($classSectionId, $request->validated());
} catch (\Throwable $e) {
Log::error('Class report card PDF generation failed: ' . $e->getMessage());
return $this->error('Failed to generate class report cards.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Unable to generate class report cards.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
$disposition = ($request->boolean('download') ? 'attachment' : 'inline');
return response($result['pdf'], 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => $disposition . '; filename="' . ($result['filename'] ?? 'ClassReportCards.pdf') . '"',
'Cache-Control' => 'private, max-age=0, must-revalidate',
'Pragma' => 'public',
]);
}
private function resolveSchoolYear(?string $schoolYear): string
{
$value = trim((string) ($schoolYear ?? ''));
if ($value !== '') {
return $value;
}
return trim((string) (Configuration::getConfig('school_year') ?? ''));
}
private function resolveSemester(?string $semester): string
{
$value = trim((string) ($semester ?? ''));
if ($value !== '') {
return $value;
}
return trim((string) (Configuration::getConfig('semester') ?? ''));
}
}
@@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Api\Reports;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Reports\SlipLogListRequest;
use App\Http\Requests\Reports\SlipPreviewRequest;
use App\Http\Requests\Reports\SlipPrintRequest;
use App\Http\Requests\Reports\SlipReprintRequest;
use App\Http\Resources\Reports\LateSlipLogResource;
use App\Services\Reports\SlipPrinterService;
use Illuminate\Http\JsonResponse;
class SlipPrinterController extends BaseApiController
{
public function __construct(private SlipPrinterService $service)
{
parent::__construct();
}
public function print(SlipPrintRequest $request)
{
$userId = (int) (auth()->id() ?? 0);
$result = $this->service->print($request->validated(), $userId);
if (!$result['ok']) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Print failed.'], 422);
}
return response($result['pdf'], 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="' . $result['filename'] . '"',
]);
}
public function preview(SlipPreviewRequest $request): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
$result = $this->service->preview($request->validated(), $userId);
return response()->json([
'ok' => true,
'text' => $result['text'],
'width' => $result['width'],
]);
}
public function logs(SlipLogListRequest $request): JsonResponse
{
$payload = $request->validated();
$rows = $this->service->logs($payload['school_year'] ?? null, $payload['semester'] ?? null);
return response()->json([
'ok' => true,
'logs' => LateSlipLogResource::collection($rows),
]);
}
public function reprint(SlipReprintRequest $request)
{
$userId = (int) (auth()->id() ?? 0);
$result = $this->service->reprint((int) $request->validated()['id'], $userId);
if (!$result['ok']) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Slip not found.'], 404);
}
return response($result['pdf'], 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="' . $result['filename'] . '"',
]);
}
}
@@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers\Api\Reports;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Reports\Stickers\StickerFormDataRequest;
use App\Http\Requests\Reports\Stickers\StickerPrintRequest;
use App\Http\Requests\Reports\Stickers\StickerStudentsByClassRequest;
use App\Http\Resources\Reports\Stickers\StickerClassSectionResource;
use App\Http\Resources\Reports\Stickers\StickerPresetResource;
use App\Http\Resources\Reports\Stickers\StickerStudentResource;
use App\Services\Reports\Stickers\StickerPresetService;
use App\Services\Reports\Stickers\StickerPrintService;
use App\Services\Reports\Stickers\StickerQueryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class StickersController extends BaseApiController
{
public function __construct(
private StickerQueryService $queryService,
private StickerPresetService $presetService,
private StickerPrintService $printService
) {
parent::__construct();
}
public function formData(StickerFormDataRequest $request): JsonResponse
{
$payload = $request->validated();
$schoolYear = $this->queryService->resolveSchoolYear($payload['school_year'] ?? null);
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
$classes = $this->queryService->listClassesWithStudents($schoolYear);
$students = $classSectionId > 0
? $this->queryService->listStudentsByClass($classSectionId, $schoolYear)
: $this->queryService->listStudentsForYear($schoolYear);
$presets = $this->presetService->presets();
return $this->success([
'classes' => StickerClassSectionResource::collection($classes),
'students' => StickerStudentResource::collection($students),
'presets' => StickerPresetResource::collection($presets),
]);
}
public function studentsByClass(StickerStudentsByClassRequest $request): JsonResponse
{
$payload = $request->validated();
$schoolYear = $this->queryService->resolveSchoolYear($payload['school_year'] ?? null);
$classSectionId = (int) ($payload['class_section_id'] ?? 0);
$students = $this->queryService->listStudentsByClass($classSectionId, $schoolYear);
return $this->success([
'students' => StickerStudentResource::collection($students),
]);
}
public function print(StickerPrintRequest $request)
{
try {
$result = $this->printService->generate($request->validated());
} catch (\Throwable $e) {
Log::error('Sticker print failed: ' . $e->getMessage());
return $this->error('Failed to generate stickers.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Unable to generate stickers.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return response($result['pdf'], 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="' . ($result['filename'] ?? 'Student_Stickers.pdf') . '"',
]);
}
}
@@ -0,0 +1,245 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\Roles\AssignUserRolesRequest;
use App\Http\Requests\Roles\PermissionStoreRequest;
use App\Http\Requests\Roles\PermissionUpdateRequest;
use App\Http\Requests\Roles\RoleListRequest;
use App\Http\Requests\Roles\RolePermissionUpdateRequest;
use App\Http\Requests\Roles\RoleStoreRequest;
use App\Http\Requests\Roles\RoleUpdateRequest;
use App\Http\Requests\Roles\UserRoleListRequest;
use App\Http\Resources\Roles\PermissionResource;
use App\Http\Resources\Roles\RolePermissionResource;
use App\Http\Resources\Roles\RoleResource;
use App\Http\Resources\Roles\UserWithRolesResource;
use App\Models\Permission;
use App\Models\Role;
use App\Services\Roles\PermissionCrudService;
use App\Services\Roles\RoleAssignmentService;
use App\Services\Roles\RoleCrudService;
use App\Services\Roles\RolePermissionService;
use App\Services\Roles\RoleQueryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class RolePermissionController extends BaseApiController
{
public function __construct(
private RoleQueryService $queryService,
private RoleAssignmentService $assignmentService,
private RolePermissionService $permissionService,
private RoleCrudService $roleCrudService,
private PermissionCrudService $permissionCrudService
) {
parent::__construct();
}
public function roles(RoleListRequest $request): JsonResponse
{
$roles = $this->queryService->listRoles($request->validated());
return $this->success([
'roles' => RoleResource::collection($roles->items()),
'meta' => [
'total' => $roles->total(),
'per_page' => $roles->perPage(),
'current_page' => $roles->currentPage(),
'last_page' => $roles->lastPage(),
],
]);
}
public function showRole(int $roleId): JsonResponse
{
$role = Role::query()->find($roleId);
if (!$role) {
return $this->error('Role not found.', Response::HTTP_NOT_FOUND);
}
return $this->success([
'role' => new RoleResource($role),
]);
}
public function storeRole(RoleStoreRequest $request): JsonResponse
{
try {
$role = $this->roleCrudService->create($request->validated());
} catch (\Throwable $e) {
Log::error('Role store failed: ' . $e->getMessage());
return $this->error('Failed to create role.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success([
'role' => new RoleResource($role),
], 'Role created successfully.', Response::HTTP_CREATED);
}
public function updateRole(RoleUpdateRequest $request, int $roleId): JsonResponse
{
$role = Role::query()->find($roleId);
if (!$role) {
return $this->error('Role not found.', Response::HTTP_NOT_FOUND);
}
try {
$role = $this->roleCrudService->update($role, $request->validated());
} catch (\Throwable $e) {
Log::error('Role update failed: ' . $e->getMessage());
return $this->error('Failed to update role.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success([
'role' => new RoleResource($role),
], 'Role updated successfully.');
}
public function deleteRole(int $roleId): JsonResponse
{
$role = Role::query()->find($roleId);
if (!$role) {
return $this->error('Role not found.', Response::HTTP_NOT_FOUND);
}
try {
$this->roleCrudService->delete($role);
} catch (\Throwable $e) {
Log::error('Role delete failed: ' . $e->getMessage());
return $this->error('Failed to delete role.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success(null, 'Role deleted successfully.');
}
public function users(UserRoleListRequest $request): JsonResponse
{
$users = $this->queryService->listUsersWithRoles($request->validated());
return $this->success([
'users' => UserWithRolesResource::collection($users->items()),
'meta' => [
'total' => $users->total(),
'per_page' => $users->perPage(),
'current_page' => $users->currentPage(),
'last_page' => $users->lastPage(),
],
]);
}
public function assignRoles(AssignUserRolesRequest $request, int $userId): JsonResponse
{
try {
$result = $this->assignmentService->assignRoles(
$userId,
(array) ($request->validated()['role_ids'] ?? []),
(int) (auth()->id() ?? 0)
);
} catch (\Throwable $e) {
Log::error('Assign roles failed: ' . $e->getMessage());
return $this->error('Failed to update roles.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!($result['ok'] ?? false)) {
return $this->error($result['message'] ?? 'Failed to update roles.', Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $this->success([
'role_names' => $result['role_names'] ?? [],
], 'Roles updated successfully.');
}
public function permissions(): JsonResponse
{
$permissions = $this->permissionService->listPermissions();
return $this->success([
'permissions' => PermissionResource::collection($permissions),
]);
}
public function showPermission(int $permissionId): JsonResponse
{
$permission = Permission::query()->find($permissionId);
if (!$permission) {
return $this->error('Permission not found.', Response::HTTP_NOT_FOUND);
}
return $this->success([
'permission' => new PermissionResource($permission),
]);
}
public function storePermission(PermissionStoreRequest $request): JsonResponse
{
try {
$permission = $this->permissionCrudService->create($request->validated());
} catch (\Throwable $e) {
Log::error('Permission store failed: ' . $e->getMessage());
return $this->error('Failed to create permission.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success([
'permission' => new PermissionResource($permission),
], 'Permission created successfully.', Response::HTTP_CREATED);
}
public function updatePermission(PermissionUpdateRequest $request, int $permissionId): JsonResponse
{
$permission = Permission::query()->find($permissionId);
if (!$permission) {
return $this->error('Permission not found.', Response::HTTP_NOT_FOUND);
}
try {
$permission = $this->permissionCrudService->update($permission, $request->validated());
} catch (\Throwable $e) {
Log::error('Permission update failed: ' . $e->getMessage());
return $this->error('Failed to update permission.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success([
'permission' => new PermissionResource($permission),
], 'Permission updated successfully.');
}
public function deletePermission(int $permissionId): JsonResponse
{
$permission = Permission::query()->find($permissionId);
if (!$permission) {
return $this->error('Permission not found.', Response::HTTP_NOT_FOUND);
}
try {
$this->permissionCrudService->delete($permission);
} catch (\Throwable $e) {
Log::error('Permission delete failed: ' . $e->getMessage());
return $this->error('Failed to delete permission.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success(null, 'Permission deleted successfully.');
}
public function rolePermissions(int $roleId): JsonResponse
{
$rows = $this->permissionService->listRolePermissions($roleId);
return $this->success([
'permissions' => RolePermissionResource::collection($rows),
]);
}
public function updateRolePermissions(RolePermissionUpdateRequest $request, int $roleId): JsonResponse
{
try {
$this->permissionService->saveRolePermissions($roleId, $request->validated()['permissions'] ?? []);
} catch (\Throwable $e) {
Log::error('Role permissions update failed: ' . $e->getMessage());
return $this->error('Failed to update role permissions.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success(null, 'Permissions saved successfully.');
}
}
@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Requests\Roles\RoleSwitchRequest;
use App\Services\Roles\RoleSwitchService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
class RoleSwitcherController extends BaseApiController
{
public function __construct(private RoleSwitchService $switchService)
{
parent::__construct();
}
public function index(): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return $this->error('Please log in first.', Response::HTTP_UNAUTHORIZED);
}
$roles = $this->switchService->getUserRoleNames($userId);
if (empty($roles)) {
return $this->error('No roles assigned to this user.', Response::HTTP_NOT_FOUND);
}
return $this->success([
'roles' => $roles,
]);
}
public function switch(RoleSwitchRequest $request): JsonResponse
{
$role = (string) $request->validated()['role'];
$route = $this->switchService->switchRole($role);
return $this->success([
'role' => $role,
'dashboard_route' => $route,
], 'Role switched successfully.');
}
}
@@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
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
{
public function __construct(
private ExamScoreService $service,
private SemesterScoreService $semesterScoreService
) {
parent::__construct();
}
public function index(Request $request): JsonResponse
{
$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([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students']),
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'scores_locked' => $data['scoresLocked'],
'missing_ok_map' => $data['missingOkMap'],
]);
}
public function update(ScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->service->update(
'final_exam',
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['scores'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
$this->refreshSemesterScores((int) $payload['class_section_id'], (string) $payload['semester'], (string) $payload['school_year']);
return response()->json(['ok' => true, 'updated' => $count]);
}
public function bySchoolYear(Request $request): JsonResponse
{
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
$validator = Validator::make($data, [
'school_year' => ['required', 'string', 'max:50'],
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$payload = $validator->validated();
$data = $this->service->listBySchoolYear('final_exam', $payload['school_year']);
return response()->json([
'ok' => true,
'school_year' => $data['schoolYear'],
'count' => count($data['rows']),
'rows' => $data['rows'],
]);
}
private function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void
{
$studentInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, (int) (auth()->id() ?? 0));
if (empty($studentInfo)) {
return;
}
try {
$this->semesterScoreService->updateScoresForStudents($studentInfo, $semester, $schoolYear);
} catch (\Throwable $e) {
}
}
}
@@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScoreAddColumnRequest;
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
{
public function __construct(
private HomeworkScoreService $service,
private SemesterScoreService $semesterScoreService
) {
parent::__construct();
}
public function index(Request $request): JsonResponse
{
$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,
$payload['school_year'] ?? null
);
return response()->json([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students']),
'headers' => $data['headers'],
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'scores_locked' => $data['scoresLocked'],
'missing_ok_map' => $data['missingOkMap'],
]);
}
public function update(ScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->service->update(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['scores'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
$this->refreshSemesterScores((int) $payload['class_section_id'], (string) $payload['semester'], (string) $payload['school_year']);
return response()->json(['ok' => true, 'updated' => $count]);
}
public function addColumn(ScoreAddColumnRequest $request): JsonResponse
{
$payload = $request->validated();
$nextIndex = $this->service->addColumn(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true, 'homework_index' => $nextIndex]);
}
public function bySchoolYear(Request $request): JsonResponse
{
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
$validator = Validator::make($data, [
'school_year' => ['required', 'string', 'max:50'],
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$payload = $validator->validated();
$data = $this->service->listBySchoolYear($payload['school_year']);
return response()->json([
'ok' => true,
'school_year' => $data['schoolYear'],
'count' => count($data['rows']),
'rows' => $data['rows'],
]);
}
private function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void
{
$studentInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, (int) (auth()->id() ?? 0));
if (empty($studentInfo)) {
return;
}
try {
$this->semesterScoreService->updateScoresForStudents($studentInfo, $semester, $schoolYear);
} catch (\Throwable $e) {
}
}
}
@@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
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
{
public function __construct(
private ExamScoreService $service,
private SemesterScoreService $semesterScoreService
) {
parent::__construct();
}
public function index(Request $request): JsonResponse
{
$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([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students']),
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'scores_locked' => $data['scoresLocked'],
'missing_ok_map' => $data['missingOkMap'],
]);
}
public function update(ScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->service->update(
'midterm_exam',
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['scores'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
$this->refreshSemesterScores((int) $payload['class_section_id'], (string) $payload['semester'], (string) $payload['school_year']);
return response()->json(['ok' => true, 'updated' => $count]);
}
public function bySchoolYear(Request $request): JsonResponse
{
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
$validator = Validator::make($data, [
'school_year' => ['required', 'string', 'max:50'],
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$payload = $validator->validated();
$data = $this->service->listBySchoolYear('midterm_exam', $payload['school_year']);
return response()->json([
'ok' => true,
'school_year' => $data['schoolYear'],
'count' => count($data['rows']),
'rows' => $data['rows'],
]);
}
private function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void
{
$studentInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, (int) (auth()->id() ?? 0));
if (empty($studentInfo)) {
return;
}
try {
$this->semesterScoreService->updateScoresForStudents($studentInfo, $semester, $schoolYear);
} catch (\Throwable $e) {
}
}
}
@@ -0,0 +1,110 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
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
{
public function __construct(
private ParticipationScoreService $service,
private SemesterScoreService $semesterScoreService
) {
parent::__construct();
}
public function index(Request $request): JsonResponse
{
$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,
$payload['school_year'] ?? null
);
return response()->json([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students']),
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'scores_locked' => $data['scoresLocked'],
'missing_ok_map' => $data['missingOkMap'],
]);
}
public function update(ScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->service->update(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['scores'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
$this->refreshSemesterScores((int) $payload['class_section_id'], (string) $payload['semester'], (string) $payload['school_year']);
return response()->json(['ok' => true, 'updated' => $count]);
}
public function bySchoolYear(Request $request): JsonResponse
{
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
$validator = Validator::make($data, [
'school_year' => ['required', 'string', 'max:50'],
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$payload = $validator->validated();
$data = $this->service->listBySchoolYear($payload['school_year']);
return response()->json([
'ok' => true,
'school_year' => $data['schoolYear'],
'count' => count($data['rows']),
'rows' => $data['rows'],
]);
}
private function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void
{
$studentInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, (int) (auth()->id() ?? 0));
if (empty($studentInfo)) {
return;
}
try {
$this->semesterScoreService->updateScoresForStudents($studentInfo, $semester, $schoolYear);
} catch (\Throwable $e) {
}
}
}
@@ -0,0 +1,126 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScoreAddColumnRequest;
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
{
public function __construct(
private ProjectScoreService $service,
private SemesterScoreService $semesterScoreService
) {
parent::__construct();
}
public function index(Request $request): JsonResponse
{
$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,
$payload['school_year'] ?? null
);
return response()->json([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students']),
'headers' => $data['headers'],
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'scores_locked' => $data['scoresLocked'],
'missing_ok_map' => $data['missingOkMap'],
]);
}
public function update(ScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->service->update(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['scores'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
$this->refreshSemesterScores((int) $payload['class_section_id'], (string) $payload['semester'], (string) $payload['school_year']);
return response()->json(['ok' => true, 'updated' => $count]);
}
public function addColumn(ScoreAddColumnRequest $request): JsonResponse
{
$payload = $request->validated();
$nextIndex = $this->service->addColumn(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true, 'project_index' => $nextIndex]);
}
public function bySchoolYear(Request $request): JsonResponse
{
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
$validator = Validator::make($data, [
'school_year' => ['required', 'string', 'max:50'],
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$payload = $validator->validated();
$data = $this->service->listBySchoolYear($payload['school_year']);
return response()->json([
'ok' => true,
'school_year' => $data['schoolYear'],
'count' => count($data['rows']),
'rows' => $data['rows'],
]);
}
private function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void
{
$studentInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, (int) (auth()->id() ?? 0));
if (empty($studentInfo)) {
return;
}
try {
$this->semesterScoreService->updateScoresForStudents($studentInfo, $semester, $schoolYear);
} catch (\Throwable $e) {
// swallow errors to keep API response stable
}
}
}
@@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScoreAddColumnRequest;
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
{
public function __construct(
private QuizScoreService $service,
private SemesterScoreService $semesterScoreService
) {
parent::__construct();
}
public function index(Request $request): JsonResponse
{
$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,
$payload['school_year'] ?? null
);
return response()->json([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students']),
'headers' => $data['headers'],
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'scores_locked' => $data['scoresLocked'],
'missing_ok_map' => $data['missingOkMap'],
]);
}
public function update(ScoreUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$count = $this->service->update(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['scores'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
$this->refreshSemesterScores((int) $payload['class_section_id'], (string) $payload['semester'], (string) $payload['school_year']);
return response()->json(['ok' => true, 'updated' => $count]);
}
public function addColumn(ScoreAddColumnRequest $request): JsonResponse
{
$payload = $request->validated();
$nextIndex = $this->service->addColumn(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
(int) (auth()->id() ?? 0)
);
return response()->json(['ok' => true, 'quiz_index' => $nextIndex]);
}
public function bySchoolYear(Request $request): JsonResponse
{
$data = array_merge($request->query->all(), $request->all(), $request->json()->all());
$validator = Validator::make($data, [
'school_year' => ['required', 'string', 'max:50'],
]);
if ($validator->fails()) {
return response()->json([
'message' => 'Validation failed.',
'errors' => $validator->errors(),
], 422);
}
$payload = $validator->validated();
$data = $this->service->listBySchoolYear($payload['school_year']);
return response()->json([
'ok' => true,
'school_year' => $data['schoolYear'],
'count' => count($data['rows']),
'rows' => $data['rows'],
]);
}
private function refreshSemesterScores(int $classSectionId, string $semester, string $schoolYear): void
{
$studentInfo = Student::getStudentInfoByClassSectionId($classSectionId, $semester, $schoolYear, (int) (auth()->id() ?? 0));
if (empty($studentInfo)) {
return;
}
try {
$this->semesterScoreService->updateScoresForStudents($studentInfo, $semester, $schoolYear);
} catch (\Throwable $e) {
}
}
}
@@ -0,0 +1,110 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScoreCommentListRequest;
use App\Http\Requests\Scores\ScoreCommentSaveRequest;
use App\Http\Requests\Scores\ScoreCommentUpdateRequest;
use App\Http\Resources\Scores\ScoreCommentResource;
use App\Services\Scores\ScoreCommentService;
use Illuminate\Http\JsonResponse;
class ScoreCommentController extends BaseApiController
{
public function __construct(private ScoreCommentService $service)
{
parent::__construct();
}
public function index(ScoreCommentListRequest $request): JsonResponse
{
$payload = $request->validated();
$query = $request->query->all();
if ($query === []) {
$rawQuery = (string) $request->server->get('QUERY_STRING', '');
parse_str($rawQuery, $query);
}
$json = $request->json()->all();
if ($json === []) {
$decoded = json_decode($request->getContent() ?: '', true);
$json = is_array($decoded) ? $decoded : [];
}
$classSectionId = $payload['class_section_id']
?? ($query['class_section_id'] ?? null)
?? $request->input('class_section_id')
?? ($json['class_section_id'] ?? null);
$semester = $payload['semester']
?? ($query['semester'] ?? null)
?? $request->input('semester')
?? ($json['semester'] ?? null);
$schoolYear = $payload['school_year']
?? ($query['school_year'] ?? null)
?? $request->input('school_year')
?? ($json['school_year'] ?? null);
$data = $this->service->list(
$classSectionId !== null ? (string) $classSectionId : null,
$semester !== null ? (string) $semester : null,
$schoolYear !== null ? (string) $schoolYear : null
);
$studentRows = [];
foreach ($data['students'] as $student) {
$studentId = (int) ($student['student_id'] ?? 0);
$studentRows[] = [
'student_id' => $studentId,
'firstname' => $student['firstname'] ?? null,
'lastname' => $student['lastname'] ?? null,
'school_id' => $student['school_id'] ?? null,
'comments' => $data['comments'][$studentId] ?? [],
];
}
return response()->json([
'ok' => true,
'students' => ScoreCommentResource::collection($studentRows),
'semester' => $data['semester'],
'school_year' => $data['schoolYear'],
'class_section_id' => $data['classSectionId'],
'scores_locked' => $data['scoresLocked'],
]);
}
public function store(ScoreCommentSaveRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->service->save(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['comments'],
$payload['missing_ok'] ?? [],
(int) (auth()->id() ?? 0)
);
if (!empty($result['errors'])) {
return response()->json(['ok' => false, 'errors' => $result['errors']], 422);
}
return response()->json(['ok' => true]);
}
public function update(ScoreCommentUpdateRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->service->update(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['comments'] ?? [],
$payload['reviews'] ?? [],
(int) (auth()->id() ?? 0)
);
if (!empty($result['errors'])) {
return response()->json(['ok' => false, 'errors' => $result['errors']], 422);
}
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScoreLockRequest;
use App\Http\Requests\Scores\ScoreOverviewRequest;
use App\Http\Requests\Scores\ScoreStudentRequest;
use App\Http\Resources\Scores\ScoreStudentResource;
use App\Services\Scores\ScoreDashboardService;
use Illuminate\Http\JsonResponse;
class ScoreController extends BaseApiController
{
public function __construct(private ScoreDashboardService $service)
{
parent::__construct();
}
public function overview(ScoreOverviewRequest $request): JsonResponse
{
$payload = $request->validated();
$teacherId = (int) ($request->user()?->id ?? (auth()->id() ?? 0));
$classSectionId = $payload['class_section_id'] ?? $request->input('class_section_id');
$semester = $payload['semester'] ?? $request->input('semester');
$schoolYear = $payload['school_year'] ?? $request->input('school_year');
$data = $this->service->overview(
$teacherId,
isset($classSectionId) ? (int) $classSectionId : null,
$semester !== null ? (string) $semester : null,
$schoolYear !== null ? (string) $schoolYear : null
);
return response()->json([
'ok' => true,
'students' => ScoreStudentResource::collection($data['students'] ?? []),
'class_section_id' => $data['class_section_id'] ?? null,
'class_section_name' => $data['class_section_name'] ?? null,
'semester' => $data['semester'] ?? null,
'school_year' => $data['school_year'] ?? null,
'scores_locked' => $data['scoresLocked'] ?? false,
]);
}
public function lock(ScoreLockRequest $request): JsonResponse
{
$payload = $request->validated();
$this->service->submitScoresLock(
(int) $payload['class_section_id'],
(string) $payload['semester'],
(string) $payload['school_year'],
$payload['missing_ok'] ?? [],
(int) ($request->user()?->id ?? (auth()->id() ?? 0))
);
return response()->json(['ok' => true]);
}
public function studentScores(ScoreStudentRequest $request): JsonResponse
{
$payload = $request->validated();
$parentId = (int) ($request->user()?->id ?? (auth()->id() ?? 0));
$data = $this->service->viewStudentScores($parentId, (string) $payload['school_year']);
return response()->json(['ok' => true] + $data);
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers\Api\Scores;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Scores\ScorePredictorRequest;
use App\Http\Resources\Scores\ScorePredictorStudentResource;
use App\Services\Scores\ScorePredictorService;
use Illuminate\Http\JsonResponse;
class ScorePredictorController extends BaseApiController
{
public function __construct(private ScorePredictorService $service)
{
parent::__construct();
}
public function index(ScorePredictorRequest $request): JsonResponse
{
$payload = $request->validated();
$data = $this->service->report(
$payload['school_year'] ?? null,
isset($payload['class_section_id']) ? (int) $payload['class_section_id'] : null
);
return response()->json([
'ok' => true,
'students' => ScorePredictorStudentResource::collection($data['students'] ?? []),
'school_year' => $data['school_year'] ?? null,
'class_sections' => $data['class_sections'] ?? [],
'selected_class_section_id' => $data['selected_class_section_id'] ?? null,
'semester' => $data['semester'] ?? null,
]);
}
}
@@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers\Api\Settings;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Settings\ConfigurationStoreRequest;
use App\Http\Requests\Settings\ConfigurationUpdateRequest;
use App\Http\Resources\Settings\ConfigurationResource;
use App\Services\Settings\ConfigurationService;
use Illuminate\Http\JsonResponse;
class ConfigurationAdminController extends BaseApiController
{
public function __construct(private ConfigurationService $service)
{
parent::__construct();
}
public function index(): JsonResponse
{
$configs = $this->service->list();
return response()->json([
'ok' => true,
'configs' => ConfigurationResource::collection($configs),
]);
}
public function store(ConfigurationStoreRequest $request): JsonResponse
{
$config = $this->service->store($request->validated());
return response()->json([
'ok' => true,
'config' => new ConfigurationResource($config->toArray()),
], 201);
}
public function update(ConfigurationUpdateRequest $request, int $id): JsonResponse
{
$config = $this->service->update($id, $request->validated());
if (!$config) {
return response()->json(['ok' => false, 'message' => 'Configuration not found.'], 404);
}
return response()->json([
'ok' => true,
'config' => new ConfigurationResource($config->toArray()),
]);
}
public function destroy(int $id): JsonResponse
{
$deleted = $this->service->delete($id);
if (!$deleted) {
return response()->json(['ok' => false, 'message' => 'Configuration not found.'], 404);
}
return response()->json(['ok' => true]);
}
}
@@ -0,0 +1,166 @@
<?php
namespace App\Http\Controllers\Api\Settings;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Events\EventChargeIndexRequest;
use App\Http\Requests\Events\EventChargeUpdateRequest;
use App\Http\Requests\Events\EventIndexRequest;
use App\Http\Requests\Events\EventStoreRequest;
use App\Http\Requests\Events\EventStudentsWithChargesRequest;
use App\Http\Requests\Events\EventUpdateRequest;
use App\Http\Resources\Events\EventChargeResource;
use App\Http\Resources\Events\EventResource;
use App\Http\Resources\Events\EventStudentChargeResource;
use App\Services\Events\EventCategoryService;
use App\Services\Events\EventChargeQueryService;
use App\Services\Events\EventChargeService;
use App\Services\Events\EventListService;
use App\Services\Events\EventManagementService;
use App\Services\Events\EventStudentChargeService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
class EventController extends BaseApiController
{
public function __construct(
private EventListService $listService,
private EventManagementService $managementService,
private EventChargeQueryService $chargeQueryService,
private EventChargeService $chargeService,
private EventStudentChargeService $studentChargeService
) {
parent::__construct();
}
public function index(EventIndexRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->listService->list($payload);
return response()->json([
'ok' => true,
'events' => EventResource::collection($result['events']),
'pagination' => $result['pagination'],
'active_count' => $result['active_count'],
'categories' => EventCategoryService::categories(),
]);
}
public function show(int $eventId): JsonResponse
{
$event = $this->listService->find($eventId);
if (!$event) {
return response()->json(['ok' => false, 'message' => 'Event not found.'], 404);
}
return response()->json([
'ok' => true,
'event' => new EventResource($event),
]);
}
public function store(EventStoreRequest $request): JsonResponse
{
$payload = $request->validated();
$actorId = (int) (auth()->id() ?? 0);
try {
$result = $this->managementService->create($payload, $request->file('flyer'), $actorId);
} catch (\Throwable $e) {
Log::error('Event creation failed.', ['error' => $e->getMessage()]);
return response()->json(['ok' => false, 'message' => 'Failed to create event.'], 500);
}
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => 'Failed to create event.'], 422);
}
return response()->json([
'ok' => true,
'event' => new EventResource($result['event']),
'charges_created' => $result['charges_created'] ?? 0,
], 201);
}
public function update(EventUpdateRequest $request, int $eventId): JsonResponse
{
$payload = $request->validated();
$result = $this->managementService->update($eventId, $payload, $request->file('flyer'));
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to update event.'], 404);
}
return response()->json([
'ok' => true,
'event' => new EventResource($result['event']),
]);
}
public function destroy(int $eventId): JsonResponse
{
$actorId = (int) (auth()->id() ?? 0);
$result = $this->managementService->delete($eventId, $actorId);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to delete event.'], 404);
}
return response()->json(['ok' => true]);
}
public function charges(EventChargeIndexRequest $request): JsonResponse
{
$payload = $request->validated();
$result = $this->chargeQueryService->list($payload);
return response()->json([
'ok' => true,
'charges' => EventChargeResource::collection($result['charges']),
'pagination' => $result['pagination'],
]);
}
public function updateCharges(EventChargeUpdateRequest $request, int $eventId): JsonResponse
{
$payload = $request->validated();
$actorId = (int) (auth()->id() ?? 0);
$result = $this->chargeService->updateParticipation(
(int) $payload['parent_id'],
$eventId,
$payload['participation'],
(string) $payload['school_year'],
(string) $payload['semester'],
$actorId
);
if (empty($result['ok'])) {
return response()->json(['ok' => false, 'message' => $result['message'] ?? 'Failed to update charges.'], 422);
}
return response()->json([
'ok' => true,
'created' => $result['created'],
'updated' => $result['updated'],
'deleted' => $result['deleted'],
]);
}
public function studentsWithCharges(EventStudentsWithChargesRequest $request): JsonResponse
{
$payload = $request->validated();
$students = $this->studentChargeService->listStudentsWithCharges(
(int) $payload['parent_id'],
(string) $payload['school_year'],
(string) $payload['semester']
);
return response()->json([
'ok' => true,
'students' => EventStudentChargeResource::collection($students),
]);
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers\Api\Settings;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Settings\PolicyShowRequest;
use App\Http\Resources\Settings\PolicyResource;
use App\Services\Policy\PolicyContentService;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
class PolicyController extends BaseApiController
{
public function __construct(private PolicyContentService $service)
{
parent::__construct();
}
public function school(PolicyShowRequest $request): JsonResponse
{
return $this->respondPolicy('school');
}
public function picture(PolicyShowRequest $request): JsonResponse
{
return $this->respondPolicy('picture');
}
private function respondPolicy(string $type): JsonResponse
{
try {
$policy = $this->service->getPolicy($type);
} catch (\InvalidArgumentException $e) {
return $this->error('Policy not found.', Response::HTTP_NOT_FOUND);
} catch (\Throwable $e) {
return $this->error('Unable to load policy.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success([
'policy' => new PolicyResource($policy),
]);
}
}
@@ -0,0 +1,139 @@
<?php
namespace App\Http\Controllers\Api\Settings;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Preferences\PreferencesIndexRequest;
use App\Http\Requests\Preferences\PreferencesUpsertRequest;
use App\Http\Resources\Preferences\PreferencesCollection;
use App\Http\Resources\Preferences\PreferencesResource;
use App\Models\Preferences;
use App\Services\Preferences\PreferencesCommandService;
use App\Services\Preferences\PreferencesQueryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class PreferencesController extends BaseApiController
{
public function __construct(
private PreferencesQueryService $queryService,
private PreferencesCommandService $commandService
) {
parent::__construct();
}
public function index(PreferencesIndexRequest $request): JsonResponse
{
$this->authorize('viewAny', Preferences::class);
$filters = $request->validated();
$page = (int) ($filters['page'] ?? 1);
$perPage = (int) ($filters['per_page'] ?? 20);
$rows = $this->queryService->paginate($filters, $page, $perPage);
$collection = new PreferencesCollection($rows);
return $this->success([
'preferences' => $collection->toArray($request),
'meta' => $collection->with($request)['meta'],
]);
}
public function show(): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED);
}
$payload = $this->queryService->getForUser($userId);
return $this->success([
'preferences' => new PreferencesResource($payload['preferences']),
'options' => $payload['options'],
]);
}
public function showForUser(int $userId): JsonResponse
{
$row = Preferences::query()->where('user_id', $userId)->first();
if (!$row) {
return $this->error('Preferences not found.', Response::HTTP_NOT_FOUND);
}
$this->authorize('view', $row);
$payload = $this->queryService->getForUser($userId);
return $this->success([
'preferences' => new PreferencesResource($payload['preferences']),
'options' => $payload['options'],
]);
}
public function store(PreferencesUpsertRequest $request): JsonResponse
{
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED);
}
try {
$saved = $this->commandService->upsert($userId, $request->validated());
} catch (\Throwable $e) {
Log::error('Preferences save failed: ' . $e->getMessage());
return $this->error('Unable to save preferences.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
$payload = $this->queryService->getForUser($userId);
return $this->success([
'preferences' => new PreferencesResource($payload['preferences']),
], 'Preferences saved.', $saved->wasRecentlyCreated ? Response::HTTP_CREATED : Response::HTTP_OK);
}
public function updateForUser(PreferencesUpsertRequest $request, int $userId): JsonResponse
{
$row = Preferences::query()->where('user_id', $userId)->first();
if ($row) {
$this->authorize('update', $row);
}
try {
$saved = $this->commandService->upsert($userId, $request->validated());
} catch (\Throwable $e) {
Log::error('Preferences update failed: ' . $e->getMessage());
return $this->error('Unable to save preferences.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
$payload = $this->queryService->getForUser($userId);
return $this->success([
'preferences' => new PreferencesResource($payload['preferences']),
], 'Preferences saved.', $saved->wasRecentlyCreated ? Response::HTTP_CREATED : Response::HTTP_OK);
}
public function destroy(int $userId): JsonResponse
{
$row = Preferences::query()->where('user_id', $userId)->first();
if (!$row) {
return $this->error('Preferences not found.', Response::HTTP_NOT_FOUND);
}
$this->authorize('delete', $row);
try {
$deleted = $this->commandService->delete($row);
} catch (\Throwable $e) {
Log::error('Preferences delete failed: ' . $e->getMessage());
return $this->error('Unable to delete preferences.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
if (!$deleted) {
return $this->error('Unable to delete preferences.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success(null, 'Preferences deleted.');
}
}
@@ -0,0 +1,154 @@
<?php
namespace App\Http\Controllers\Api\Settings;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Settings\SchoolCalendarIndexRequest;
use App\Http\Requests\Settings\SchoolCalendarOptionsRequest;
use App\Http\Requests\Settings\SchoolCalendarStoreRequest;
use App\Http\Requests\Settings\SchoolCalendarUpdateRequest;
use App\Http\Resources\Settings\SchoolCalendarEventDetailResource;
use App\Http\Resources\Settings\SchoolCalendarEventResource;
use App\Services\Settings\SchoolCalendar\SchoolCalendarContextService;
use App\Services\Settings\SchoolCalendar\SchoolCalendarFormatterService;
use App\Services\Settings\SchoolCalendar\SchoolCalendarMeetingService;
use App\Services\Settings\SchoolCalendar\SchoolCalendarMutationService;
use App\Services\Settings\SchoolCalendar\SchoolCalendarQueryService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class SchoolCalendarController extends BaseApiController
{
public function __construct(
private SchoolCalendarContextService $contextService,
private SchoolCalendarQueryService $queryService,
private SchoolCalendarMeetingService $meetingService,
private SchoolCalendarFormatterService $formatterService,
private SchoolCalendarMutationService $mutationService
) {
parent::__construct();
}
public function options(SchoolCalendarOptionsRequest $request): JsonResponse
{
return $this->success([
'event_types' => $this->contextService->eventTypes(),
'defaults' => [
'school_year' => $this->contextService->defaultSchoolYear(),
'semester' => $this->contextService->defaultSemester(),
],
]);
}
public function index(SchoolCalendarIndexRequest $request): JsonResponse
{
$filters = $request->validated();
$audience = $filters['audience'] ?? null;
$includeMeetings = array_key_exists('include_meetings', $filters)
? (bool) $filters['include_meetings']
: true;
if (empty($filters['school_year'])) {
$filters['school_year'] = $this->contextService->defaultSchoolYear();
}
$events = $this->queryService->listEvents($filters);
$events = $this->queryService->filterEventsForAudience($events, $audience);
$formatted = $events->map(function ($event) use ($audience) {
return $this->formatterService->formatEvent($event, $audience);
})->all();
if ($includeMeetings) {
$parentUserId = $audience === 'parent' ? (int) (auth()->id() ?? 0) : null;
$meetingRows = $this->meetingService->listMeetings($filters['school_year'] ?? '', $audience, $parentUserId);
foreach ($meetingRows as $row) {
$formatted[] = $this->formatterService->formatMeetingEvent($row, $audience);
}
}
return $this->success([
'events' => SchoolCalendarEventResource::collection($formatted),
]);
}
public function show(int $eventId): JsonResponse
{
$event = $this->queryService->findEvent($eventId);
if (!$event) {
return $this->error('Event not found.', Response::HTTP_NOT_FOUND);
}
return $this->success([
'event' => new SchoolCalendarEventDetailResource($event),
]);
}
public function store(SchoolCalendarStoreRequest $request): JsonResponse
{
$payload = $request->validated();
$targets = $this->extractEmailTargets($payload);
try {
$result = $this->mutationService->create($payload, $targets);
} catch (\Throwable $e) {
Log::error('SchoolCalendar store failed: ' . $e->getMessage());
return $this->error('Failed to save event.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success([
'event' => new SchoolCalendarEventDetailResource($result['event']),
'email_status' => $result['email_status'] ?? [],
], 'Event added successfully.', Response::HTTP_CREATED);
}
public function update(SchoolCalendarUpdateRequest $request, int $eventId): JsonResponse
{
$event = $this->queryService->findEvent($eventId);
if (!$event) {
return $this->error('Event not found.', Response::HTTP_NOT_FOUND);
}
$payload = $request->validated();
$targets = $this->extractEmailTargets($payload);
try {
$result = $this->mutationService->update($event, $payload, $targets);
} catch (\Throwable $e) {
Log::error('SchoolCalendar update failed: ' . $e->getMessage());
return $this->error('Failed to update event.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success([
'event' => new SchoolCalendarEventDetailResource($result['event']),
'email_status' => $result['email_status'] ?? [],
], 'Event updated successfully.');
}
public function destroy(int $eventId): JsonResponse
{
$event = $this->queryService->findEvent($eventId);
if (!$event) {
return $this->error('Event not found.', Response::HTTP_NOT_FOUND);
}
try {
$this->mutationService->delete($event);
} catch (\Throwable $e) {
Log::error('SchoolCalendar delete failed: ' . $e->getMessage());
return $this->error('Failed to delete event.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success(null, 'Event deleted successfully.');
}
private function extractEmailTargets(array $payload): array
{
return [
'parents' => !empty($payload['send_email_parent']),
'teachers' => !empty($payload['send_email_teacher']),
'admins' => !empty($payload['send_email_admin']),
];
}
}
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Api\Settings;
use App\Http\Controllers\Api\BaseApiController;
use App\Http\Requests\Settings\SettingsUpdateRequest;
use App\Http\Resources\Settings\SettingsResource;
use App\Models\Setting;
use App\Services\Settings\SettingsService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\Response;
class SettingsController extends BaseApiController
{
public function __construct(private SettingsService $settingsService)
{
parent::__construct();
}
public function index(): JsonResponse
{
$this->authorize('viewAny', Setting::class);
return $this->success([
'settings' => new SettingsResource($this->settingsService->get()),
]);
}
public function update(SettingsUpdateRequest $request): JsonResponse
{
$settings = Setting::singleton();
$this->authorize('update', $settings);
$userId = (int) (auth()->id() ?? 0);
if ($userId <= 0) {
return $this->error('Unauthorized.', Response::HTTP_UNAUTHORIZED);
}
try {
$updated = $this->settingsService->update($request->validated(), $userId);
} catch (\Throwable $e) {
Log::error('Settings update failed: ' . $e->getMessage());
return $this->error('Unable to update settings.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $this->success([
'settings' => new SettingsResource($updated),
], 'Settings updated.');
}
}

Some files were not shown because too many files have changed in this diff Show More