add school year model
This commit is contained in:
@@ -16,9 +16,9 @@ class AdministratorTeacherSubmissionController extends Controller
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(): JsonResponse
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
return response()->json($this->service->report());
|
||||
return response()->json($this->service->report($request->query()));
|
||||
}
|
||||
|
||||
public function notify(Request $request): JsonResponse
|
||||
|
||||
@@ -23,6 +23,8 @@ class EmailExtractorController extends BaseApiController
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'users' => $emails['users'] ?? [],
|
||||
'parents' => $emails['parents'] ?? [],
|
||||
'emails' => new EmailExtractorEmailsResource($emails),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\SchoolYears;
|
||||
|
||||
use App\Http\Controllers\Api\Core\BaseApiController;
|
||||
use App\Http\Requests\SchoolYears\CloseSchoolYearRequest;
|
||||
use App\Http\Requests\SchoolYears\PreviewCloseSchoolYearRequest;
|
||||
use App\Http\Requests\SchoolYears\SaveSchoolYearRequest;
|
||||
use App\Services\SchoolYears\SchoolYearClosureService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SchoolYearController extends BaseApiController
|
||||
{
|
||||
public function __construct(private SchoolYearClosureService $service)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
return response()->json(['ok' => true, 'data' => $this->service->list()]);
|
||||
}
|
||||
|
||||
public function store(SaveSchoolYearRequest $request): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->service->createYear($request->validated(), $this->getCurrentUserId()),
|
||||
], Response::HTTP_CREATED);
|
||||
}
|
||||
|
||||
public function update(SaveSchoolYearRequest $request, int $schoolYear): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->service->updateYear($schoolYear, $request->validated(), $this->getCurrentUserId()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function summary(int $schoolYear): JsonResponse
|
||||
{
|
||||
return response()->json(['ok' => true, 'data' => $this->service->summary($schoolYear)]);
|
||||
}
|
||||
|
||||
public function validateClose(PreviewCloseSchoolYearRequest $request, int $schoolYear): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'validation' => $this->service->validateClose($schoolYear, $request->validated()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function previewClose(PreviewCloseSchoolYearRequest $request, int $schoolYear): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->service->previewClose($schoolYear, $request->validated()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function close(CloseSchoolYearRequest $request, int $schoolYear): JsonResponse
|
||||
{
|
||||
try {
|
||||
$result = $this->service->close($schoolYear, $request->validated(), $this->getCurrentUserId());
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
} catch (\Throwable $e) {
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], Response::HTTP_CONFLICT);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true, 'data' => $result]);
|
||||
}
|
||||
|
||||
public function reopen(Request $request, int $schoolYear): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->service->reopen($schoolYear, $this->getCurrentUserId()),
|
||||
]);
|
||||
}
|
||||
|
||||
public function parentBalances(Request $request, int $schoolYear): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->service->parentBalances($schoolYear, $request->query('to_school_year')),
|
||||
]);
|
||||
}
|
||||
|
||||
public function promotionPreview(Request $request, int $schoolYear): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->service->promotionPreview($schoolYear, $request->query('to_school_year')),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Web;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\SchoolYears\SchoolYearClosureService;
|
||||
use App\Services\SchoolYears\SchoolYearResolver;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
use PHPOpenSourceSaver\JWTAuth\Facades\JWTAuth;
|
||||
|
||||
class SchoolYearAdminPageController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private SchoolYearClosureService $service,
|
||||
private SchoolYearResolver $resolver
|
||||
) {
|
||||
}
|
||||
|
||||
public function show(Request $request): View
|
||||
{
|
||||
$years = $this->service->list();
|
||||
$selectedId = (int) ($request->query('year') ?? 0);
|
||||
$activeYear = collect($years)->firstWhere('is_current', true);
|
||||
|
||||
$defaultNext = $activeYear
|
||||
? ($this->resolver->suggestNextName((string) $activeYear['name']) ?? '')
|
||||
: '';
|
||||
[$defaultStart, $defaultEnd] = $defaultNext !== ''
|
||||
? $this->resolver->inferDatesFromName($defaultNext)
|
||||
: ['', ''];
|
||||
|
||||
return view('admin.school-years', [
|
||||
'pageConfig' => [
|
||||
'jwt' => JWTAuth::fromUser($request->user()),
|
||||
'selected_year_id' => $selectedId,
|
||||
'docs_url' => url('/docs'),
|
||||
'default_draft' => [
|
||||
'name' => $defaultNext,
|
||||
'start_date' => $defaultStart,
|
||||
'end_date' => $defaultEnd,
|
||||
],
|
||||
'api' => [
|
||||
'index' => url('/api/v1/school-years'),
|
||||
'store' => url('/api/v1/school-years'),
|
||||
'update' => url('/api/v1/school-years/__ID__'),
|
||||
'summary' => url('/api/v1/school-years/__ID__/summary'),
|
||||
'validate_close' => url('/api/v1/school-years/__ID__/validate-close'),
|
||||
'preview_close' => url('/api/v1/school-years/__ID__/preview-close'),
|
||||
'close' => url('/api/v1/school-years/__ID__/close'),
|
||||
'reopen' => url('/api/v1/school-years/__ID__/reopen'),
|
||||
'parent_balances' => url('/api/v1/school-years/__ID__/parent-balances'),
|
||||
'promotion_preview' => url('/api/v1/school-years/__ID__/promotion-preview'),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$payload = $request->validate([
|
||||
'name' => ['required', 'string', 'max:50'],
|
||||
'start_date' => ['required', 'date'],
|
||||
'end_date' => ['required', 'date', 'after:start_date'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$year = $this->service->createYear($payload, optional($request->user())->id);
|
||||
} catch (ValidationException $e) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('admin.school-years', ['year' => $year['id']])
|
||||
->with('status', sprintf('Draft school year %s created.', $year['name']));
|
||||
}
|
||||
|
||||
public function update(Request $request, int $schoolYear): RedirectResponse
|
||||
{
|
||||
$payload = $request->validate([
|
||||
'name' => ['required', 'string', 'max:50'],
|
||||
'start_date' => ['required', 'date'],
|
||||
'end_date' => ['required', 'date', 'after:start_date'],
|
||||
]);
|
||||
|
||||
$updated = $this->service->updateYear($schoolYear, $payload, optional($request->user())->id);
|
||||
|
||||
return redirect()
|
||||
->route('admin.school-years', ['year' => $updated['id']])
|
||||
->with('status', sprintf('School year %s updated.', $updated['name']));
|
||||
}
|
||||
|
||||
public function validateClose(Request $request, int $schoolYear): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'validation' => $this->service->validateClose($schoolYear, $this->validatedClosePayload($request)),
|
||||
]);
|
||||
}
|
||||
|
||||
public function previewClose(Request $request, int $schoolYear): JsonResponse
|
||||
{
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'data' => $this->service->previewClose($schoolYear, $this->validatedClosePayload($request)),
|
||||
]);
|
||||
}
|
||||
|
||||
public function close(Request $request, int $schoolYear): RedirectResponse
|
||||
{
|
||||
$payload = $this->validatedClosePayload($request);
|
||||
$payload['confirmation'] = (string) $request->validate([
|
||||
'confirmation' => ['required', 'string', 'max:80'],
|
||||
])['confirmation'];
|
||||
|
||||
$result = $this->service->close($schoolYear, $payload, optional($request->user())->id);
|
||||
|
||||
return redirect()
|
||||
->route('admin.school-years', ['year' => $result['new_school_year']['id']])
|
||||
->with('status', sprintf(
|
||||
'Closed %s and opened %s.',
|
||||
$result['closed_school_year']['name'],
|
||||
$result['new_school_year']['name']
|
||||
));
|
||||
}
|
||||
|
||||
public function reopen(Request $request, int $schoolYear): RedirectResponse
|
||||
{
|
||||
$year = $this->service->reopen($schoolYear, optional($request->user())->id);
|
||||
|
||||
return redirect()
|
||||
->route('admin.school-years', ['year' => $year['id']])
|
||||
->with('status', sprintf('School year %s is now active.', $year['name']));
|
||||
}
|
||||
|
||||
private function validatedClosePayload(Request $request): array
|
||||
{
|
||||
return $request->validate([
|
||||
'new_school_year' => ['required', 'array'],
|
||||
'new_school_year.name' => ['required', 'string', 'max:50'],
|
||||
'new_school_year.start_date' => ['required', 'date'],
|
||||
'new_school_year.end_date' => ['required', 'date', 'after:new_school_year.start_date'],
|
||||
'transfer_unpaid_balances' => ['nullable', 'boolean'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Configuration;
|
||||
use App\Services\SchoolYears\SchoolYearWriteGuard;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureSchoolYearEditable
|
||||
{
|
||||
public function __construct(private SchoolYearWriteGuard $guard)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (!in_array(strtoupper($request->method()), ['POST', 'PUT', 'PATCH', 'DELETE'], true)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->guard->assertEditable($this->resolveSchoolYears($request));
|
||||
} catch (RuntimeException $e) {
|
||||
return response()->json([
|
||||
'ok' => false,
|
||||
'message' => $e->getMessage(),
|
||||
], 409);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
private function resolveSchoolYears(Request $request): array
|
||||
{
|
||||
$years = [];
|
||||
|
||||
foreach (['school_year', 'current_school_year'] as $field) {
|
||||
$value = $request->input($field, $request->query($field));
|
||||
if (is_string($value) && trim($value) !== '') {
|
||||
$years[] = trim($value);
|
||||
}
|
||||
}
|
||||
|
||||
$routeYears = [
|
||||
['param' => 'invoiceId', 'table' => 'invoices', 'column' => 'school_year'],
|
||||
['param' => 'invoice', 'table' => 'invoices', 'column' => 'school_year'],
|
||||
['param' => 'paymentId', 'table' => 'payments', 'column' => 'school_year'],
|
||||
['param' => 'payment', 'table' => 'payments', 'column' => 'school_year'],
|
||||
['param' => 'refundId', 'table' => 'refunds', 'column' => 'school_year'],
|
||||
['param' => 'refund', 'table' => 'refunds', 'column' => 'school_year'],
|
||||
['param' => 'promotionId', 'table' => 'student_promotion_records', 'column' => 'current_school_year'],
|
||||
['param' => 'promotion', 'table' => 'student_promotion_records', 'column' => 'current_school_year'],
|
||||
['param' => 'eventCharge', 'table' => 'event_charges', 'column' => 'school_year'],
|
||||
['param' => 'eventId', 'table' => 'events', 'column' => 'school_year'],
|
||||
['param' => 'classSection', 'table' => 'classSection', 'column' => 'school_year'],
|
||||
['param' => 'classSectionId', 'table' => 'classSection', 'column' => 'school_year'],
|
||||
];
|
||||
|
||||
foreach ($routeYears as $mapping) {
|
||||
$value = $request->route($mapping['param']);
|
||||
if ($value === null || $value === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$resolved = $this->lookupYear((string) $value, $mapping['table'], $mapping['column']);
|
||||
if ($resolved !== null) {
|
||||
$years[] = $resolved;
|
||||
}
|
||||
}
|
||||
|
||||
if ($years === []) {
|
||||
$current = trim((string) (Configuration::getConfig('school_year') ?? ''));
|
||||
if ($current !== '') {
|
||||
$years[] = $current;
|
||||
}
|
||||
}
|
||||
|
||||
return $years;
|
||||
}
|
||||
|
||||
private function lookupYear(string $id, string $table, string $column): ?string
|
||||
{
|
||||
if (!ctype_digit($id) || !DB::getSchemaBuilder()->hasTable($table)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$primaryKey = match ($table) {
|
||||
'classSection' => 'class_section_id',
|
||||
'student_promotion_records' => 'promotion_id',
|
||||
default => 'id',
|
||||
};
|
||||
|
||||
$value = DB::table($table)->where($primaryKey, (int) $id)->value($column);
|
||||
|
||||
return is_string($value) && trim($value) !== '' ? trim($value) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\SchoolYears;
|
||||
|
||||
class CloseSchoolYearRequest extends PreviewCloseSchoolYearRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return array_merge(parent::rules(), [
|
||||
'confirmation' => ['required', 'string', 'max:80'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\SchoolYears;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class PreviewCloseSchoolYearRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'new_school_year' => ['required', 'array'],
|
||||
'new_school_year.name' => ['required', 'string', 'max:50'],
|
||||
'new_school_year.start_date' => ['required', 'date'],
|
||||
'new_school_year.end_date' => ['required', 'date', 'after:new_school_year.start_date'],
|
||||
'transfer_unpaid_balances' => ['nullable', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\SchoolYears;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SaveSchoolYearRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:50'],
|
||||
'start_date' => ['required', 'date'],
|
||||
'end_date' => ['required', 'date', 'after:start_date'],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user