Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f39dbee62 | |||
| 863e330dd8 | |||
| 2ef71cc92b | |||
| 3e6c577085 | |||
| 182036cc41 | |||
| f6be51576c | |||
| ba1206e314 | |||
| 25e7b3c67c | |||
| 17d73e2f92 | |||
| a82f7aedbc | |||
| c235fd2170 | |||
| abebe0d9c0 | |||
| 311bb93977 | |||
| 2e9a391280 | |||
| 3bf4c687da | |||
| 20ee70d153 | |||
| 5eeaec0257 | |||
| a953f00675 | |||
| 99d399d36c | |||
| b7bdee3af5 | |||
| 51ab9cbdbb | |||
| d0c8ac533b | |||
| f236e5bfe4 | |||
| 6a03e547cc | |||
| 51d2504e99 | |||
| d19d27bd88 | |||
| cd73300bc6 | |||
| 6e8c3f5466 | |||
| e2c3e3cf85 | |||
| d9567f925f | |||
| d7d718c469 | |||
| 380740e4c8 | |||
| af250d9fd4 | |||
| 52bd34e213 | |||
| ad5e42c6c7 | |||
| f8aa350bfd | |||
| adc4219766 | |||
| 79e44fe037 | |||
| 1cb3573d4b | |||
| 0c3e9b16f7 | |||
| b3efb8c815 | |||
| 6fbca86b9b | |||
| d76c871cb7 |
+37
-4
@@ -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
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
class DeleteUnverifiedUser
|
||||
{
|
||||
public function __construct(public int $userId)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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'] ?? [])
|
||||
);
|
||||
|
||||
+20
-2
@@ -3,10 +3,11 @@
|
||||
namespace App\Http\Controllers\Api\Administrator;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Administrator\SendTeacherSubmissionNotificationsRequest;
|
||||
use App\Services\Administrator\AdministratorTeacherSubmissionService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class AdministratorTeacherSubmissionController extends Controller
|
||||
{
|
||||
@@ -20,13 +21,30 @@ class AdministratorTeacherSubmissionController extends Controller
|
||||
return response()->json($this->service->report());
|
||||
}
|
||||
|
||||
public function notify(SendTeacherSubmissionNotificationsRequest $request): JsonResponse
|
||||
public function notify(Request $request): JsonResponse
|
||||
{
|
||||
$adminId = (int) Auth::id();
|
||||
if ($adminId <= 0) {
|
||||
return response()->json(['message' => 'Please log in first.'], 401);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'notify' => ['required', 'array', 'min:1'],
|
||||
'missing_items' => ['nullable', 'array'],
|
||||
], [
|
||||
'notify.required' => 'Select at least one teacher to notify.',
|
||||
'notify.array' => 'Notify payload must be an array.',
|
||||
'notify.min' => 'Select at least one teacher to notify.',
|
||||
'missing_items.array' => 'Missing items payload must be an array.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$result = $this->service->sendNotifications($request, $adminId);
|
||||
|
||||
return response()->json([
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user