fix calendar page
API CI/CD / Validate (composer + pint) (push) Successful in 3m9s
API CI/CD / Test (PHPUnit) (push) Failing after 2m0s
API CI/CD / Build frontend assets (push) Failing after 1m55s
API CI/CD / Security audit (push) Failing after 55s
API CI/CD / Deploy to shared hosting (PHP) (push) Has been skipped

This commit is contained in:
root
2026-07-05 14:18:40 -04:00
parent 5e35fefd69
commit cccc2872cd
12 changed files with 860 additions and 838 deletions
+88
View File
@@ -0,0 +1,88 @@
# ────────────────────────────────────
# Docker Dev Environment
# ────────────────────────────────────
NODE_ENV=development
# ────────────────────────────────────
# DATABASE (PostgreSQL via docker)
# ────────────────────────────────────
DATABASE_URL=postgresql://postgres:password@postgres:5432/rentaldrivego
# ────────────────────────────────────
# REDIS
# ────────────────────────────────────
REDIS_URL=redis://redis:6379
# ────────────────────────────────────
# JWT
# ────────────────────────────────────
JWT_SECRET=bvDokCe6RD7Jb5cxJ6P5z4rdTwS37hyUgglD8HGCE6Xwo6g3SEuZ9XNzjou53raUf6B2jMcEWxQm0Y5zcw2THw
JWT_EXPIRY=8h
# ────────────────────────────────────
# FILE STORAGE
# ────────────────────────────────────
FILE_STORAGE_ROOT=/var/lib/rentaldrivego/storage
# ────────────────────────────────────
# URLS (docker services)
# ────────────────────────────────────
API_URL=http://localhost:4000
API_HOST=0.0.0.0
API_PORT=4000
API_INTERNAL_URL=http://api:4000
ADMIN_URL=http://localhost:3002
DASHBOARD_URL=http://localhost:3001
MARKETPLACE_URL=http://localhost:3000
NEXT_PUBLIC_ADMIN_URL=http://localhost:3002
NEXT_PUBLIC_DASHBOARD_URL=http://localhost:3001
NEXT_PUBLIC_MARKETPLACE_URL=http://localhost:3000
NEXT_PUBLIC_CUSTOM_DOMAIN_TARGET=http://localhost:3000
# ────────────────────────────────────
# CORS
# ────────────────────────────────────
CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002
# ────────────────────────────────────
# MAIL (log driver in dev)
# ────────────────────────────────────
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=none
MAIL_FROM_ADDRESS=dev@rentaldrivego.local
MAIL_FROM_NAME="RentalDriveGo Dev"
MAIL_REPLY_TO_ADDRESS=dev@rentaldrivego.local
MAIL_REPLY_TO_NAME="RentalDriveGo Dev"
# ────────────────────────────────────
# ADMIN SEED (initial dev account)
# ────────────────────────────────────
ADMIN_SEED_EMAIL=admin@rentaldrivego.dev
ADMIN_SEED_FIRST_NAME=Dev
ADMIN_SEED_LAST_NAME=Admin
ADMIN_SEED_PASSWORD=password
# ────────────────────────────────────
# EXTERNAL SERVICES (blank in dev)
# ────────────────────────────────────
AMANPAY_BASE_URL=
AMANPAY_MERCHANT_ID=
AMANPAY_SECRET_KEY=
AMANPAY_WEBHOOK_SECRET=
PAYPAL_BASE_URL=
PAYPAL_CLIENT_ID=
PAYPAL_CLIENT_SECRET=
PAYPAL_WEBHOOK_ID=
TWILIO_ACCOUNT_SID=
TWILIO_AUTH_TOKEN=
TWILIO_PHONE_NUMBER=
TWILIO_WHATSAPP_NUMBER=
FIREBASE_CLIENT_EMAIL=
FIREBASE_PRIVATE_KEY=
FIREBASE_PROJECT_ID=
RESEND_API_KEY=
EMAIL_FROM=
EMAIL_FROM_NAME=
+2 -2
View File
@@ -1,6 +1,6 @@
APP_NAME=Alrahma_API
APP_ENV=production
APP_KEY=base64:CHANGE_ME_RUN_php_artisan_key_generate
APP_KEY=base64:bvDokCe6RD7Jb5cxJ6P5z4rdTwS37hyUgglD8HGCE6Xwo6g3SEuZ9XNzjou53raUf6B2jMcEWxQm0Y5zcw2THw
APP_DEBUG=false
APP_TIMEZONE=America/New_York
APP_URL=https://api.alrahmaisgl.org
@@ -103,7 +103,7 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
# ----------------------------
# JWT
# ----------------------------
JWT_SECRET=CHANGE_ME_USE_STRONG_RANDOM_SECRET
JWT_SECRET=d7UbttsiymIIh-5nHw4tYoYqwGjZX5VhBtR-FnWaBTpXX0uP6D4EWx86fxYdLKMk0wIEV5xeNUIRjlNBG-eeHw
JWT_ALGO=HS256
JWT_TTL=60
JWT_REFRESH_TTL=20160
BIN
View File
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,59 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Services\Settings\SchoolCalendar\SchoolCalendarContextService;
use App\Services\Settings\SchoolCalendar\SchoolCalendarQueryService;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
class AdminCalendarPageController extends Controller
{
public function __construct(
private SchoolCalendarContextService $contextService,
private SchoolCalendarQueryService $queryService,
) {}
public function show(Request $request): View
{
$schoolYear = trim((string) $request->query('school_year', ''));
if ($schoolYear === '') {
$schoolYear = $this->contextService->defaultSchoolYear();
}
$semester = trim((string) $request->query('semester', ''));
$filters = ['school_year' => $schoolYear];
if ($semester !== '') {
$filters['semester'] = $semester;
}
$events = $this->queryService->listEvents($filters);
return view('admin.calendar', [
'events' => $events,
'schoolYear' => $schoolYear,
'semester' => $semester,
'defaultSemester' => $semester !== '' ? $semester : $this->contextService->defaultSemester(),
'schoolYears' => $this->schoolYearOptions($schoolYear),
]);
}
/**
* @return list<string>
*/
private function schoolYearOptions(string $selectedSchoolYear): array
{
$currentYear = (int) date('Y') + 1;
$years = [];
for ($year = $currentYear; $year >= 2023; $year--) {
$years[] = ($year - 1).'-'.$year;
}
if ($selectedSchoolYear !== '' && ! in_array($selectedSchoolYear, $years, true)) {
array_unshift($years, $selectedSchoolYear);
}
return array_values(array_unique($years));
}
}
-19
View File
@@ -20,13 +20,6 @@
0 => 'Laravel\\Sanctum\\SanctumServiceProvider',
),
),
'laravel/tinker' =>
array (
'providers' =>
array (
0 => 'Laravel\\Tinker\\TinkerServiceProvider',
),
),
'nesbot/carbon' =>
array (
'providers' =>
@@ -48,16 +41,4 @@
0 => 'Termwind\\Laravel\\TermwindServiceProvider',
),
),
'php-open-source-saver/jwt-auth' =>
array (
'aliases' =>
array (
'JWTAuth' => 'PHPOpenSourceSaver\\JWTAuth\\Facades\\JWTAuth',
'JWTFactory' => 'PHPOpenSourceSaver\\JWTAuth\\Facades\\JWTFactory',
),
'providers' =>
array (
0 => 'PHPOpenSourceSaver\\JWTAuth\\Providers\\LaravelServiceProvider',
),
),
);
+9 -16
View File
@@ -27,14 +27,12 @@
23 => 'Laravel\\Pail\\PailServiceProvider',
24 => 'Laravel\\Sail\\SailServiceProvider',
25 => 'Laravel\\Sanctum\\SanctumServiceProvider',
26 => 'Laravel\\Tinker\\TinkerServiceProvider',
27 => 'Carbon\\Laravel\\ServiceProvider',
28 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
29 => 'Termwind\\Laravel\\TermwindServiceProvider',
30 => 'PHPOpenSourceSaver\\JWTAuth\\Providers\\LaravelServiceProvider',
31 => 'App\\Providers\\AppServiceProvider',
32 => 'App\\Providers\\EventServiceProvider',
33 => 'Laravel\\Sanctum\\SanctumServiceProvider',
26 => 'Carbon\\Laravel\\ServiceProvider',
27 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
28 => 'Termwind\\Laravel\\TermwindServiceProvider',
29 => 'App\\Providers\\AppServiceProvider',
30 => 'App\\Providers\\EventServiceProvider',
31 => 'Laravel\\Sanctum\\SanctumServiceProvider',
),
'eager' =>
array (
@@ -53,10 +51,9 @@
12 => 'Carbon\\Laravel\\ServiceProvider',
13 => 'NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider',
14 => 'Termwind\\Laravel\\TermwindServiceProvider',
15 => 'PHPOpenSourceSaver\\JWTAuth\\Providers\\LaravelServiceProvider',
16 => 'App\\Providers\\AppServiceProvider',
17 => 'App\\Providers\\EventServiceProvider',
18 => 'Laravel\\Sanctum\\SanctumServiceProvider',
15 => 'App\\Providers\\AppServiceProvider',
16 => 'App\\Providers\\EventServiceProvider',
17 => 'Laravel\\Sanctum\\SanctumServiceProvider',
),
'deferred' =>
array (
@@ -216,7 +213,6 @@
'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider',
'Laravel\\Sail\\Console\\InstallCommand' => 'Laravel\\Sail\\SailServiceProvider',
'Laravel\\Sail\\Console\\PublishCommand' => 'Laravel\\Sail\\SailServiceProvider',
'command.tinker' => 'Laravel\\Tinker\\TinkerServiceProvider',
),
'when' =>
array (
@@ -262,8 +258,5 @@
'Laravel\\Sail\\SailServiceProvider' =>
array (
),
'Laravel\\Tinker\\TinkerServiceProvider' =>
array (
),
),
);
+2 -6
View File
@@ -6,15 +6,11 @@
"keywords": ["laravel", "framework"],
"license": "MIT",
"require": {
"php": "^8.2",
"php": "^8.4",
"chillerlan/php-qrcode": "^5.0",
"dompdf/dompdf": "^3.1",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.3",
"laravel/tinker": "^2.10.1",
"php-open-source-saver/jwt-auth": "^2.8",
"setasign/fpdf": "^1.8",
"tecnickcom/tcpdf": "^6.11"
"laravel/sanctum": "^4.3"
},
"require-dev": {
"fakerphp/faker": "^1.23",
Generated
+225 -795
View File
File diff suppressed because it is too large Load Diff
+350
View File
@@ -0,0 +1,350 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>School Calendar</title>
<style>
:root {
--bg: #f5f7fb;
--panel: #ffffff;
--line: #d9e0ea;
--ink: #172033;
--muted: #637083;
--success: #198754;
--primary: #0d6efd;
--danger: #dc3545;
--secondary: #6c757d;
--warning-bg: #fff3cd;
--warning-line: #ffec99;
--shadow: 0 12px 32px rgba(23, 32, 51, 0.08);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
color: var(--ink);
font-family: Arial, Helvetica, sans-serif;
background: var(--bg);
}
a,
button,
select {
font: inherit;
}
.container-fluid {
width: 100%;
padding: 24px;
}
.wrapper {
max-width: 1380px;
margin: 0 auto;
}
.content {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
padding: 24px;
}
h2 {
margin: 0 0 18px;
text-align: center;
font-size: 1.9rem;
}
.toolbar {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-bottom: 18px;
}
.filter-form {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
justify-content: center;
gap: 14px;
margin-bottom: 18px;
text-align: center;
}
.form-label {
display: inline-block;
margin-bottom: 6px;
font-weight: 600;
}
.form-select {
min-width: 150px;
padding: 8px 34px 8px 10px;
border: 1px solid #c8d1dc;
border-radius: 6px;
background: #fff;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
padding: 8px 12px;
border: 1px solid transparent;
border-radius: 6px;
color: #fff;
text-decoration: none;
cursor: pointer;
}
.btn-sm {
min-height: 30px;
padding: 5px 9px;
font-size: 0.875rem;
}
.btn-success { background: var(--success); }
.btn-primary { background: var(--primary); }
.btn-danger { background: var(--danger); }
.btn-secondary { background: var(--secondary); }
.btn-outline-secondary {
background: #fff;
border-color: var(--secondary);
color: var(--secondary);
}
.alert {
margin-bottom: 16px;
padding: 12px 14px;
border-radius: 6px;
}
.alert-success {
color: #0f5132;
background: #d1e7dd;
border: 1px solid #badbcc;
}
.alert-danger {
color: #842029;
background: #f8d7da;
border: 1px solid #f5c2c7;
}
.alert-info {
color: #055160;
background: #cff4fc;
border: 1px solid #b6effb;
}
.table-responsive {
width: 100%;
overflow-x: auto;
margin-bottom: 18px;
}
table {
width: 100%;
border-collapse: collapse;
background: #fff;
}
th,
td {
padding: 10px 12px;
border: 1px solid var(--line);
text-align: left;
vertical-align: middle;
}
th {
background: #eef3f8;
font-weight: 700;
white-space: nowrap;
}
tbody tr:nth-child(even) {
background: #f9fbfd;
}
.text-center { text-align: center; }
.small { font-size: 0.875rem; }
.table-warning {
background: var(--warning-bg);
border-color: var(--warning-line);
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
white-space: nowrap;
}
.delete-form {
display: inline;
}
@media (max-width: 760px) {
.container-fluid {
padding: 12px;
}
.content {
padding: 16px;
}
.toolbar {
justify-content: stretch;
}
.toolbar .btn,
.filter-form .btn {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="wrapper">
<div class="content">
<h2>School Calendar</h2>
@php
$addParams = array_filter([
'school_year' => $schoolYear ?? '',
'semester' => $semester ?? '',
], fn ($value) => $value !== '');
$addQuery = $addParams ? ('?'.http_build_query($addParams)) : '';
$resetUrl = url()->current();
@endphp
<div class="toolbar">
<a href="{{ url('/administrator/calendar_add_event') }}{{ $addQuery }}" class="btn btn-success">Add School Calendar Entry</a>
</div>
<form method="get" action="{{ url()->current() }}" class="filter-form">
<div>
<label for="school_year" class="form-label">School Year:</label>
<select name="school_year" id="school_year" class="form-select">
@foreach ($schoolYears as $option)
<option value="{{ $option }}" @selected($option === ($schoolYear ?? ''))>{{ $option }}</option>
@endforeach
</select>
</div>
<div>
<label for="semester" class="form-label">Semester:</label>
<select name="semester" id="semester" class="form-select">
<option value="">-</option>
<option value="Fall" @selected(strcasecmp((string) ($semester ?? ''), 'Fall') === 0)>Fall</option>
<option value="Spring" @selected(strcasecmp((string) ($semester ?? ''), 'Spring') === 0)>Spring</option>
</select>
</div>
<div>
<button type="submit" class="btn btn-secondary">Apply</button>
<a href="{{ $resetUrl }}" class="btn btn-outline-secondary">Reset</a>
</div>
</form>
@if (session('success'))
<div class="alert alert-success">{{ session('success') }}</div>
@endif
@php
$emailStatus = session('email_status');
$emailStatusClass = session('email_status_class', 'info');
@endphp
@if ($emailStatus)
<div class="alert alert-{{ $emailStatusClass }}">{{ $emailStatus }}</div>
@endif
@if (session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<div class="table-responsive">
<table id="myTable">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Type</th>
<th>Description</th>
<th>Date</th>
<th>Notify Parent</th>
<th>Notify Teacher</th>
<th>Notify Admin</th>
<th>No School</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@if ($events->isNotEmpty())
@if ($emailStatus)
<tr class="table-warning">
<td colspan="10" class="text-center small">{{ $emailStatus }}</td>
</tr>
@endif
@foreach ($events as $event)
<tr>
<td>{{ $event->id }}</td>
<td>{{ $event->title }}</td>
<td>{{ $event->event_type ?: '-' }}</td>
<td>{{ $event->description }}</td>
<td>{{ optional($event->date)->format('m-d-Y') }}</td>
<td class="text-center">
<input type="checkbox" disabled @checked((bool) $event->notify_parent)>
</td>
<td class="text-center">
<input type="checkbox" disabled @checked((bool) $event->notify_teacher)>
</td>
<td class="text-center">
<input type="checkbox" disabled @checked((bool) $event->notify_admin)>
</td>
<td class="text-center">
<input type="checkbox" disabled @checked((bool) $event->no_school)>
</td>
<td>
<div class="actions">
<a href="{{ url('/administrator/calendar_edit/'.$event->id) }}" class="btn btn-primary btn-sm">Edit</a>
<form action="{{ url('/administrator/calendar_delete/'.$event->id) }}" method="post" class="delete-form">
@csrf
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
</form>
</div>
</td>
</tr>
@endforeach
@else
<tr>
<td colspan="10" class="text-center">No school calendar entries found</td>
</tr>
@endif
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
document.querySelectorAll('.delete-form').forEach((form) => {
form.addEventListener('submit', (event) => {
if (!window.confirm('Are you sure you want to delete this school calendar entry?')) {
event.preventDefault();
}
});
});
</script>
</body>
</html>
+5
View File
@@ -3,6 +3,7 @@
use App\Http\Controllers\Api\Auth\AuthSessionController;
use App\Http\Controllers\Api\Auth\SessionTimeoutController;
use App\Http\Controllers\Api\System\DashboardRedirectController;
use App\Http\Controllers\Web\AdminCalendarPageController;
use App\Http\Controllers\Web\SchoolYearAdminPageController;
use App\Http\Controllers\Web\TeacherCalendarPageController;
use Illuminate\Support\Facades\Route;
@@ -81,6 +82,10 @@ Route::middleware('web')->group(function () {
->name('teacher.calendar');
Route::middleware('auth')->get('app/teacher/calendar', [TeacherCalendarPageController::class, 'show'])
->name('app.teacher.calendar');
Route::middleware(['auth', 'admin.access'])->get('administrator/calendar_view', [AdminCalendarPageController::class, 'show'])
->name('admin.calendar-view');
Route::middleware(['auth', 'admin.access'])->get('app/administrator/calendar_view', [AdminCalendarPageController::class, 'show'])
->name('app.admin.calendar-view');
Route::middleware(['auth', 'admin.access'])->get('administrator/school-years', [SchoolYearAdminPageController::class, 'show'])
->name('admin.school-years');
Route::middleware(['auth', 'admin.access'])->get('app/administrator/school-years', [SchoolYearAdminPageController::class, 'show'])
+120
View File
@@ -0,0 +1,120 @@
<?php
namespace Tests\Feature\Web;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class AdminCalendarPageTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$compiledPath = storage_path('framework/views');
if (! is_dir($compiledPath)) {
mkdir($compiledPath, 0777, true);
}
config([
'app.key' => 'base64:'.base64_encode(random_bytes(32)),
'app.cipher' => 'AES-256-CBC',
'view.compiled' => $compiledPath,
]);
}
public function test_admin_calendar_view_lists_school_calendar_events_for_selected_year(): void
{
$this->seedAdminContext();
DB::table('calendar_events')->insert([
'title' => 'Back to School Night',
'date' => '2025-09-14',
'description' => 'Families meet teachers.',
'notify_parent' => 1,
'notify_admin' => 1,
'notify_teacher' => 1,
'no_school' => 0,
'semester' => 'Fall',
'school_year' => '2025-2026',
'notification_sent' => 0,
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('calendar_events')->insert([
'title' => 'Other Year Event',
'date' => '2026-09-13',
'description' => 'Should not render for the selected year.',
'notify_parent' => 1,
'notify_admin' => 1,
'notify_teacher' => 1,
'no_school' => 0,
'semester' => 'Fall',
'school_year' => '2026-2027',
'notification_sent' => 0,
'created_at' => now(),
'updated_at' => now(),
]);
$response = $this->actingAs(User::query()->findOrFail(1))
->get('/app/administrator/calendar_view?school_year=2025-2026');
$response->assertOk();
$response->assertSee('School Calendar');
$response->assertSee('Add School Calendar Entry');
$response->assertSee('Notify Parent');
$response->assertSee('No School');
$response->assertSee('Back to School Night');
$response->assertSee('Families meet teachers.');
$response->assertDontSee('Other Year Event');
}
private function seedAdminContext(): void
{
DB::table('configuration')->insert([
['config_key' => 'school_year', 'config_value' => '2025-2026'],
['config_key' => 'semester', 'config_value' => 'Fall'],
]);
DB::table('roles')->insert([
'id' => 1,
'name' => 'administrator',
'slug' => 'administrator',
'is_active' => 1,
]);
DB::table('users')->insert([
'id' => 1,
'school_id' => 1,
'firstname' => 'Admin',
'lastname' => 'User',
'gender' => 'Female',
'cellphone' => '5555555555',
'email' => 'admin-calendar@example.com',
'address_street' => '123 Main',
'city' => 'City',
'state' => 'ST',
'zip' => '12345',
'accept_school_policy' => 1,
'is_verified' => 1,
'status' => 'Active',
'is_suspended' => 0,
'failed_attempts' => 0,
'password' => bcrypt('secret'),
'semester' => 'Fall',
'school_year' => '2025-2026',
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('user_roles')->insert([
'user_id' => 1,
'role_id' => 1,
]);
}
}