From 256fda511dab11cf6cad65dc700c021f7c66efdf Mon Sep 17 00:00:00 2001 From: Johnrey Gabon Date: Wed, 4 Feb 2026 00:42:31 +0800 Subject: [PATCH] feat: add Trix and sortablejs editor dependency and update UI components - Added "trix" and "sortablejs" dependency to package.json for rich text editing capabilities. - Added modules in superadmin --- .phpstorm.meta.php | 228 ++- _ide_helper.php | 288 ++- app/Livewire/AvatarSelector.php | 56 +- app/Livewire/Components/EventCalendar.php | 21 +- app/Livewire/Faq.php | 34 + app/Livewire/Gso/Notifications.php | 94 +- app/Livewire/Gso/Profile.php | 157 +- app/Livewire/Osa/Dashboard.php | 128 +- app/Livewire/Osa/Notifications.php | 94 +- app/Livewire/Osa/Profile.php | 155 +- app/Livewire/Osa/TicketReview/Index.php | 32 +- app/Livewire/StudentOrg/EditTicket.php | 10 +- app/Livewire/StudentOrg/Notifications.php | 93 +- app/Livewire/StudentOrg/Profile.php | 157 +- app/Livewire/StudentOrg/SubmitTicket.php | 96 +- app/Livewire/Superadmin/Archive/Index.php | 3 +- app/Livewire/Superadmin/Calendar/Index.php | 921 ++++----- app/Livewire/Superadmin/Dashboard.php | 182 +- app/Livewire/Superadmin/FaqEditor.php | 401 ++++ app/Livewire/Superadmin/FaqManager.php | 226 +++ app/Livewire/Superadmin/Logs.php | 7 + app/Livewire/Superadmin/Notifications.php | 110 +- app/Livewire/Superadmin/Profile.php | 157 +- app/Livewire/Superadmin/Reports/Index.php | 226 ++- .../SystemSettings/ContentSectionEditor.php | 246 +++ .../SystemSettings/ContentSectionManager.php | 161 ++ .../SystemSettings/EventTypeEditor.php | 140 ++ .../SystemSettings/EventTypeManager.php | 130 +- .../Superadmin/SystemSettings/Index.php | 10 + .../SystemSettings/OrganizationManager.php | 7 +- .../Superadmin/SystemSettings/VenueEditor.php | 146 ++ .../SystemSettings/VenueManager.php | 139 ++ app/Livewire/Superadmin/Users/Index.php | 11 +- app/Models/ContentSection.php | 193 ++ app/Models/Event_Type.php | 9 + app/Models/Faq.php | 111 ++ app/Models/Ticket.php | 4 +- app/Models/User.php | 5 +- .../AnnouncementNotification.php | 53 + app/Providers/AppServiceProvider.php | 31 +- app/Services/TransactionLogService.php | 92 +- app/Traits/WithProfilePhoto.php | 4 +- app/View/Html/Sanitizer.php | 33 + app/helpers.php | 24 + composer.json | 9 +- composer.lock | 711 ++++--- config/livewire.php | 2 +- ...0_000001_create_content_sections_table.php | 36 + ...6_01_30_221339_create_rich_texts_table.php | 25 + .../2026_02_02_000000_create_faqs_table.php | 36 + database/seeders/ContentSectionSeeder.php | 122 ++ database/seeders/FaqSeeder.php | 111 ++ docker/nginx.conf | 3 + docker/start.sh | 11 +- package-lock.json | 574 +++--- package.json | 4 +- resources/css/app.css | 156 ++ resources/js/app.js | 5 + resources/js/libs/trix.js | 5 + .../components/announcement-banner.blade.php | 43 + .../components/content-section.blade.php | 28 + .../documentary-requirements.blade.php | 75 + resources/views/components/footer.blade.php | 20 +- .../views/components/layouts/app.blade.php | 3 + .../components/layouts/gso-layout.blade.php | 3 + .../views/components/layouts/guest.blade.php | 2 +- .../layouts/student-org-layout.blade.php | 7 +- .../components/layouts/superadmin.blade.php | 29 +- .../components/terms_and_conditions.blade.php | 122 +- .../views/components/tickets/review.blade.php | 150 +- .../sections/organization-info.blade.php | 63 +- .../components/tickets/ticket-card.blade.php | 11 +- .../views/components/trix-input.blade.php | 11 + .../components/event-calendar.blade.php | 1775 +++++++++-------- resources/views/livewire/faq.blade.php | 152 ++ .../views/livewire/gso/dashboard.blade.php | 304 ++- .../livewire/gso/notifications.blade.php | 8 + .../livewire/layout/navigation.blade.php | 162 +- .../views/livewire/osa/dashboard.blade.php | 432 ++-- .../livewire/osa/notifications.blade.php | 11 +- .../views/livewire/osa/reports.blade.php | 9 +- .../livewire/osa/ticket-management.blade.php | 42 +- .../osa/ticket-review/index.blade.php | 45 +- .../pages/auth/forgot-password.blade.php | 16 +- .../student-org/edit-ticket.blade.php | 171 +- .../student-org/notifications.blade.php | 8 + .../student-org/submit-ticket.blade.php | 959 +++++---- .../livewire/superadmin/dashboard.blade.php | 348 +++- .../livewire/superadmin/faq-editor.blade.php | 274 +++ .../livewire/superadmin/faq-manager.blade.php | 201 ++ .../views/livewire/superadmin/logs.blade.php | 84 +- .../superadmin/notifications.blade.php | 516 +++-- .../placeholders/dashboard.blade.php | 65 + .../superadmin/placeholders/logs.blade.php | 41 + .../superadmin/placeholders/reports.blade.php | 53 + .../superadmin/placeholders/users.blade.php | 47 + .../superadmin/reports/index.blade.php | 477 +++-- .../content-section-editor.blade.php | 282 +++ .../content-section-manager.blade.php | 227 +++ .../system-settings/course-manager.blade.php | 97 +- .../event-type-editor.blade.php | 138 ++ .../event-type-manager.blade.php | 111 +- .../system-settings/index.blade.php | 168 +- .../organization-manager.blade.php | 121 +- .../partials/organization-card.blade.php | 52 + .../system-settings/venue-editor.blade.php | 114 ++ .../system-settings/venue-manager.blade.php | 116 ++ .../superadmin/tickets/index.blade.php | 5 +- .../livewire/superadmin/users/index.blade.php | 129 +- .../views/reports/osa/summary-pdf.blade.php | 4 +- routes/web.php | 21 +- 111 files changed, 10954 insertions(+), 4633 deletions(-) create mode 100644 app/Livewire/Faq.php create mode 100644 app/Livewire/Superadmin/FaqEditor.php create mode 100644 app/Livewire/Superadmin/FaqManager.php create mode 100644 app/Livewire/Superadmin/SystemSettings/ContentSectionEditor.php create mode 100644 app/Livewire/Superadmin/SystemSettings/ContentSectionManager.php create mode 100644 app/Livewire/Superadmin/SystemSettings/EventTypeEditor.php create mode 100644 app/Livewire/Superadmin/SystemSettings/VenueEditor.php create mode 100644 app/Livewire/Superadmin/SystemSettings/VenueManager.php create mode 100644 app/Models/ContentSection.php create mode 100644 app/Models/Faq.php create mode 100644 app/Notifications/AnnouncementNotification.php create mode 100644 app/View/Html/Sanitizer.php create mode 100644 app/helpers.php create mode 100644 database/migrations/2026_01_30_000001_create_content_sections_table.php create mode 100644 database/migrations/2026_01_30_221339_create_rich_texts_table.php create mode 100644 database/migrations/2026_02_02_000000_create_faqs_table.php create mode 100644 database/seeders/ContentSectionSeeder.php create mode 100644 database/seeders/FaqSeeder.php create mode 100644 resources/js/libs/trix.js create mode 100644 resources/views/components/announcement-banner.blade.php create mode 100644 resources/views/components/content-section.blade.php create mode 100644 resources/views/components/documentary-requirements.blade.php create mode 100644 resources/views/components/trix-input.blade.php create mode 100644 resources/views/livewire/faq.blade.php create mode 100644 resources/views/livewire/superadmin/faq-editor.blade.php create mode 100644 resources/views/livewire/superadmin/faq-manager.blade.php create mode 100644 resources/views/livewire/superadmin/placeholders/dashboard.blade.php create mode 100644 resources/views/livewire/superadmin/placeholders/logs.blade.php create mode 100644 resources/views/livewire/superadmin/placeholders/reports.blade.php create mode 100644 resources/views/livewire/superadmin/placeholders/users.blade.php create mode 100644 resources/views/livewire/superadmin/system-settings/content-section-editor.blade.php create mode 100644 resources/views/livewire/superadmin/system-settings/content-section-manager.blade.php create mode 100644 resources/views/livewire/superadmin/system-settings/event-type-editor.blade.php create mode 100644 resources/views/livewire/superadmin/system-settings/partials/organization-card.blade.php create mode 100644 resources/views/livewire/superadmin/system-settings/venue-editor.blade.php create mode 100644 resources/views/livewire/superadmin/system-settings/venue-manager.blade.php diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index 9f0f047..5a18328 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -30,6 +30,7 @@ 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, 'Livewire\LivewireManager' => \Livewire\Volt\LivewireManager::class, + 'Resend\Contracts\Client' => \Resend\Client::class, 'auth' => \Illuminate\Auth\AuthManager::class, 'auth.driver' => \Illuminate\Auth\SessionGuard::class, 'auth.password' => \Illuminate\Auth\Passwords\PasswordBrokerManager::class, @@ -37,7 +38,6 @@ 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, 'cache' => \Illuminate\Cache\CacheManager::class, 'cache.store' => \Illuminate\Cache\Repository::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, 'command.tinker' => \Laravel\Tinker\Console\TinkerCommand::class, 'composer' => \Illuminate\Support\Composer::class, 'cookie' => \Illuminate\Cookie\CookieJar::class, @@ -103,6 +103,7 @@ 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, 'Livewire\LivewireManager' => \Livewire\Volt\LivewireManager::class, + 'Resend\Contracts\Client' => \Resend\Client::class, 'auth' => \Illuminate\Auth\AuthManager::class, 'auth.driver' => \Illuminate\Auth\SessionGuard::class, 'auth.password' => \Illuminate\Auth\Passwords\PasswordBrokerManager::class, @@ -110,7 +111,6 @@ 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, 'cache' => \Illuminate\Cache\CacheManager::class, 'cache.store' => \Illuminate\Cache\Repository::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, 'command.tinker' => \Laravel\Tinker\Console\TinkerCommand::class, 'composer' => \Illuminate\Support\Composer::class, 'cookie' => \Illuminate\Cookie\CookieJar::class, @@ -176,6 +176,7 @@ 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, 'Livewire\LivewireManager' => \Livewire\Volt\LivewireManager::class, + 'Resend\Contracts\Client' => \Resend\Client::class, 'auth' => \Illuminate\Auth\AuthManager::class, 'auth.driver' => \Illuminate\Auth\SessionGuard::class, 'auth.password' => \Illuminate\Auth\Passwords\PasswordBrokerManager::class, @@ -183,7 +184,6 @@ 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, 'cache' => \Illuminate\Cache\CacheManager::class, 'cache.store' => \Illuminate\Cache\Repository::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, 'command.tinker' => \Laravel\Tinker\Console\TinkerCommand::class, 'composer' => \Illuminate\Support\Composer::class, 'cookie' => \Illuminate\Cookie\CookieJar::class, @@ -249,6 +249,7 @@ 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, 'Livewire\LivewireManager' => \Livewire\Volt\LivewireManager::class, + 'Resend\Contracts\Client' => \Resend\Client::class, 'auth' => \Illuminate\Auth\AuthManager::class, 'auth.driver' => \Illuminate\Auth\SessionGuard::class, 'auth.password' => \Illuminate\Auth\Passwords\PasswordBrokerManager::class, @@ -256,7 +257,6 @@ 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, 'cache' => \Illuminate\Cache\CacheManager::class, 'cache.store' => \Illuminate\Cache\Repository::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, 'command.tinker' => \Laravel\Tinker\Console\TinkerCommand::class, 'composer' => \Illuminate\Support\Composer::class, 'cookie' => \Illuminate\Cookie\CookieJar::class, @@ -322,6 +322,7 @@ 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, 'Livewire\LivewireManager' => \Livewire\Volt\LivewireManager::class, + 'Resend\Contracts\Client' => \Resend\Client::class, 'auth' => \Illuminate\Auth\AuthManager::class, 'auth.driver' => \Illuminate\Auth\SessionGuard::class, 'auth.password' => \Illuminate\Auth\Passwords\PasswordBrokerManager::class, @@ -329,7 +330,6 @@ 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, 'cache' => \Illuminate\Cache\CacheManager::class, 'cache.store' => \Illuminate\Cache\Repository::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, 'command.tinker' => \Laravel\Tinker\Console\TinkerCommand::class, 'composer' => \Illuminate\Support\Composer::class, 'cookie' => \Illuminate\Cookie\CookieJar::class, @@ -395,6 +395,7 @@ 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, 'Livewire\LivewireManager' => \Livewire\Volt\LivewireManager::class, + 'Resend\Contracts\Client' => \Resend\Client::class, 'auth' => \Illuminate\Auth\AuthManager::class, 'auth.driver' => \Illuminate\Auth\SessionGuard::class, 'auth.password' => \Illuminate\Auth\Passwords\PasswordBrokerManager::class, @@ -402,7 +403,6 @@ 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, 'cache' => \Illuminate\Cache\CacheManager::class, 'cache.store' => \Illuminate\Cache\Repository::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, 'command.tinker' => \Laravel\Tinker\Console\TinkerCommand::class, 'composer' => \Illuminate\Support\Composer::class, 'cookie' => \Illuminate\Cookie\CookieJar::class, @@ -468,6 +468,7 @@ 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, 'Livewire\LivewireManager' => \Livewire\Volt\LivewireManager::class, + 'Resend\Contracts\Client' => \Resend\Client::class, 'auth' => \Illuminate\Auth\AuthManager::class, 'auth.driver' => \Illuminate\Auth\SessionGuard::class, 'auth.password' => \Illuminate\Auth\Passwords\PasswordBrokerManager::class, @@ -475,7 +476,6 @@ 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, 'cache' => \Illuminate\Cache\CacheManager::class, 'cache.store' => \Illuminate\Cache\Repository::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, 'command.tinker' => \Laravel\Tinker\Console\TinkerCommand::class, 'composer' => \Illuminate\Support\Composer::class, 'cookie' => \Illuminate\Cookie\CookieJar::class, @@ -541,6 +541,7 @@ 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, 'Livewire\LivewireManager' => \Livewire\Volt\LivewireManager::class, + 'Resend\Contracts\Client' => \Resend\Client::class, 'auth' => \Illuminate\Auth\AuthManager::class, 'auth.driver' => \Illuminate\Auth\SessionGuard::class, 'auth.password' => \Illuminate\Auth\Passwords\PasswordBrokerManager::class, @@ -548,7 +549,6 @@ 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, 'cache' => \Illuminate\Cache\CacheManager::class, 'cache.store' => \Illuminate\Cache\Repository::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, 'command.tinker' => \Laravel\Tinker\Console\TinkerCommand::class, 'composer' => \Illuminate\Support\Composer::class, 'cookie' => \Illuminate\Cookie\CookieJar::class, @@ -614,6 +614,7 @@ 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, 'Livewire\LivewireManager' => \Livewire\Volt\LivewireManager::class, + 'Resend\Contracts\Client' => \Resend\Client::class, 'auth' => \Illuminate\Auth\AuthManager::class, 'auth.driver' => \Illuminate\Auth\SessionGuard::class, 'auth.password' => \Illuminate\Auth\Passwords\PasswordBrokerManager::class, @@ -621,7 +622,6 @@ 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, 'cache' => \Illuminate\Cache\CacheManager::class, 'cache.store' => \Illuminate\Cache\Repository::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, 'command.tinker' => \Laravel\Tinker\Console\TinkerCommand::class, 'composer' => \Illuminate\Support\Composer::class, 'cookie' => \Illuminate\Cookie\CookieJar::class, @@ -687,6 +687,7 @@ 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, 'Livewire\LivewireManager' => \Livewire\Volt\LivewireManager::class, + 'Resend\Contracts\Client' => \Resend\Client::class, 'auth' => \Illuminate\Auth\AuthManager::class, 'auth.driver' => \Illuminate\Auth\SessionGuard::class, 'auth.password' => \Illuminate\Auth\Passwords\PasswordBrokerManager::class, @@ -694,7 +695,6 @@ 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, 'cache' => \Illuminate\Cache\CacheManager::class, 'cache.store' => \Illuminate\Cache\Repository::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, 'command.tinker' => \Laravel\Tinker\Console\TinkerCommand::class, 'composer' => \Illuminate\Support\Composer::class, 'cookie' => \Illuminate\Cookie\CookieJar::class, @@ -760,6 +760,7 @@ 'Illuminate\Routing\Contracts\CallableDispatcher' => \Illuminate\Routing\CallableDispatcher::class, 'Illuminate\Routing\Contracts\ControllerDispatcher' => \Illuminate\Routing\ControllerDispatcher::class, 'Livewire\LivewireManager' => \Livewire\Volt\LivewireManager::class, + 'Resend\Contracts\Client' => \Resend\Client::class, 'auth' => \Illuminate\Auth\AuthManager::class, 'auth.driver' => \Illuminate\Auth\SessionGuard::class, 'auth.password' => \Illuminate\Auth\Passwords\PasswordBrokerManager::class, @@ -767,7 +768,6 @@ 'blade.compiler' => \Illuminate\View\Compilers\BladeCompiler::class, 'cache' => \Illuminate\Cache\CacheManager::class, 'cache.store' => \Illuminate\Cache\Repository::class, - 'command.debugbar.clear' => \Barryvdh\Debugbar\Console\ClearCommand::class, 'command.tinker' => \Laravel\Tinker\Console\TinkerCommand::class, 'composer' => \Illuminate\Support\Composer::class, 'cookie' => \Illuminate\Cookie\CookieJar::class, @@ -1193,7 +1193,7 @@ 'livewire.layout' => 'string', 'livewire.lazy_placeholder' => 'NULL', 'livewire.temporary_file_upload.disk' => 'NULL', - 'livewire.temporary_file_upload.rules' => 'NULL', + 'livewire.temporary_file_upload.rules' => 'array', 'livewire.temporary_file_upload.directory' => 'NULL', 'livewire.temporary_file_upload.middleware' => 'NULL', 'livewire.temporary_file_upload.preview_mimes' => 'array', @@ -1258,9 +1258,9 @@ 'mail.mailers.smtp.scheme' => 'NULL', 'mail.mailers.smtp.url' => 'NULL', 'mail.mailers.smtp.host' => 'string', - 'mail.mailers.smtp.port' => 'string', - 'mail.mailers.smtp.username' => 'string', - 'mail.mailers.smtp.password' => 'string', + 'mail.mailers.smtp.port' => 'integer', + 'mail.mailers.smtp.username' => 'NULL', + 'mail.mailers.smtp.password' => 'NULL', 'mail.mailers.smtp.timeout' => 'integer', 'mail.mailers.smtp.local_domain' => 'string', 'mail.mailers.ses.transport' => 'string', @@ -1471,11 +1471,17 @@ 'image.options.decodeAnimation' => 'boolean', 'image.options.blendingColor' => 'string', 'image.options.strip' => 'boolean', - 'resend.api_key' => 'NULL', + 'resend.api_key' => 'string', 'resend.domain' => 'NULL', 'resend.path' => 'string', 'resend.webhook.secret' => 'NULL', 'resend.webhook.tolerance' => 'integer', + 'globalid.app_name' => 'string', + 'rich-text-laravel.model' => 'string', + 'rich-text-laravel.encrypted_model' => 'string', + 'rich-text-laravel.supported_files_content_types' => 'array', + 'rich-text-laravel.editor' => 'string', + 'rich-text-laravel.editors.trix' => 'string', 'tinker.commands' => 'array', 'tinker.alias' => 'array', 'tinker.dont_alias' => 'array', @@ -1839,7 +1845,7 @@ 'livewire.layout' => 'string', 'livewire.lazy_placeholder' => 'NULL', 'livewire.temporary_file_upload.disk' => 'NULL', - 'livewire.temporary_file_upload.rules' => 'NULL', + 'livewire.temporary_file_upload.rules' => 'array', 'livewire.temporary_file_upload.directory' => 'NULL', 'livewire.temporary_file_upload.middleware' => 'NULL', 'livewire.temporary_file_upload.preview_mimes' => 'array', @@ -1904,9 +1910,9 @@ 'mail.mailers.smtp.scheme' => 'NULL', 'mail.mailers.smtp.url' => 'NULL', 'mail.mailers.smtp.host' => 'string', - 'mail.mailers.smtp.port' => 'string', - 'mail.mailers.smtp.username' => 'string', - 'mail.mailers.smtp.password' => 'string', + 'mail.mailers.smtp.port' => 'integer', + 'mail.mailers.smtp.username' => 'NULL', + 'mail.mailers.smtp.password' => 'NULL', 'mail.mailers.smtp.timeout' => 'integer', 'mail.mailers.smtp.local_domain' => 'string', 'mail.mailers.ses.transport' => 'string', @@ -2117,11 +2123,17 @@ 'image.options.decodeAnimation' => 'boolean', 'image.options.blendingColor' => 'string', 'image.options.strip' => 'boolean', - 'resend.api_key' => 'NULL', + 'resend.api_key' => 'string', 'resend.domain' => 'NULL', 'resend.path' => 'string', 'resend.webhook.secret' => 'NULL', 'resend.webhook.tolerance' => 'integer', + 'globalid.app_name' => 'string', + 'rich-text-laravel.model' => 'string', + 'rich-text-laravel.encrypted_model' => 'string', + 'rich-text-laravel.supported_files_content_types' => 'array', + 'rich-text-laravel.editor' => 'string', + 'rich-text-laravel.editors.trix' => 'string', 'tinker.commands' => 'array', 'tinker.alias' => 'array', 'tinker.dont_alias' => 'array', @@ -2485,7 +2497,7 @@ 'livewire.layout' => 'string', 'livewire.lazy_placeholder' => 'NULL', 'livewire.temporary_file_upload.disk' => 'NULL', - 'livewire.temporary_file_upload.rules' => 'NULL', + 'livewire.temporary_file_upload.rules' => 'array', 'livewire.temporary_file_upload.directory' => 'NULL', 'livewire.temporary_file_upload.middleware' => 'NULL', 'livewire.temporary_file_upload.preview_mimes' => 'array', @@ -2550,9 +2562,9 @@ 'mail.mailers.smtp.scheme' => 'NULL', 'mail.mailers.smtp.url' => 'NULL', 'mail.mailers.smtp.host' => 'string', - 'mail.mailers.smtp.port' => 'string', - 'mail.mailers.smtp.username' => 'string', - 'mail.mailers.smtp.password' => 'string', + 'mail.mailers.smtp.port' => 'integer', + 'mail.mailers.smtp.username' => 'NULL', + 'mail.mailers.smtp.password' => 'NULL', 'mail.mailers.smtp.timeout' => 'integer', 'mail.mailers.smtp.local_domain' => 'string', 'mail.mailers.ses.transport' => 'string', @@ -2763,11 +2775,17 @@ 'image.options.decodeAnimation' => 'boolean', 'image.options.blendingColor' => 'string', 'image.options.strip' => 'boolean', - 'resend.api_key' => 'NULL', + 'resend.api_key' => 'string', 'resend.domain' => 'NULL', 'resend.path' => 'string', 'resend.webhook.secret' => 'NULL', 'resend.webhook.tolerance' => 'integer', + 'globalid.app_name' => 'string', + 'rich-text-laravel.model' => 'string', + 'rich-text-laravel.encrypted_model' => 'string', + 'rich-text-laravel.supported_files_content_types' => 'array', + 'rich-text-laravel.editor' => 'string', + 'rich-text-laravel.editors.trix' => 'string', 'tinker.commands' => 'array', 'tinker.alias' => 'array', 'tinker.dont_alias' => 'array', @@ -2939,85 +2957,93 @@ 'dompdf.options.font_height_ratio','dompdf.options.enable_html5_parser','blade-heroicons.prefix','blade-heroicons.fallback','blade-heroicons.class', 'blade-heroicons.attributes','image.driver','image.options.autoOrientation','image.options.decodeAnimation','image.options.blendingColor', 'image.options.strip','resend.api_key','resend.domain','resend.path','resend.webhook.secret', -'resend.webhook.tolerance','tinker.commands','tinker.alias','tinker.dont_alias',); +'resend.webhook.tolerance','globalid.app_name','rich-text-laravel.model','rich-text-laravel.encrypted_model','rich-text-laravel.supported_files_content_types', +'rich-text-laravel.editor','rich-text-laravel.editors.trix','tinker.commands','tinker.alias','tinker.dont_alias',); registerArgumentsSet('middleware', 'web','api','auth','auth.basic','auth.session', 'cache.headers','can','guest','password.confirm','precognitive', 'signed','throttle','verified','role',); registerArgumentsSet('routes', 'debugbar.openhandler','debugbar.clockwork','debugbar.assets.css','debugbar.assets.js','debugbar.cache.delete', -'debugbar.queries.explain','livewire.update','livewire.upload-file', -'livewire.preview-file','resend.webhook','mary.toogle-sidebar','mary.spotlight','mary.upload','about-us','profile','dashboard', -'keep-alive','admin.dashboard','admin.ticket','osa.ticket-review.index','osa.ticket-review.show', -'admin.calendar','admin.notifications','admin.reports','admin.archive','admin.profile', -'superadmin.dashboard','superadmin.users','superadmin.roles','superadmin.calendar','superadmin.tickets', -'superadmin.archive','superadmin.reports','superadmin.notifications','superadmin.system-settings','superadmin.admin-tools', -'superadmin.logs','superadmin.profile','gso.dashboard','gso.ticket-review','gso.ticket-details', -'gso.calendar','gso.notifications','gso.communication','gso.reports','gso.reports.export', -'gso.profile','student-org.dashboard','student-org.submit-ticket','student-org.my-tickets','student-org.calendar', -'student-org.reschedule','student-org.notifications','student-org.history','student-org.profile','login', -'admin.login','student-org.login','gso.login','superadmin.login','register', -'password.request','password.reset','verification.notice','verification.verify','password.confirm', -'logout','logout.get','storage.local',); +'debugbar.queries.explain','livewire.upload-file','livewire.preview-file', +'resend.webhook','mary.toogle-sidebar','mary.spotlight','mary.upload','about-us','profile','dashboard','keep-alive', +'admin.dashboard','admin.ticket','osa.ticket-review.index','osa.ticket-review.show','admin.calendar', +'admin.notifications','admin.reports','admin.archive','admin.profile','superadmin.dashboard', +'superadmin.users','superadmin.roles','superadmin.calendar','superadmin.tickets','superadmin.archive', +'superadmin.reports','superadmin.notifications','superadmin.system-settings','superadmin.content-section.create','superadmin.content-section.edit', +'superadmin.event-type.create','superadmin.event-type.edit','superadmin.admin-tools','superadmin.logs','superadmin.profile', +'gso.dashboard','gso.ticket-review','gso.ticket-details','gso.calendar','gso.notifications', +'gso.communication','gso.reports','gso.reports.export','gso.profile','student-org.dashboard', +'student-org.submit-ticket','student-org.my-tickets','student-org.calendar','student-org.reschedule','student-org.notifications', +'student-org.history','student-org.profile','login','admin.login','student-org.login', +'gso.login','superadmin.login','register','password.request','password.reset', +'verification.notice','verification.verify','password.confirm','logout','logout.get','storage.local', +'livewire.update',); registerArgumentsSet('views', -'components.app-layout','components.comment-boxes.normal-comment','components.comment-boxes.ticket-comments','components.dashboard.quick-action-card','components.dashboard.stat-card', -'components.footer','components.forms.email-input','components.forms.input-error','components.forms.input-label','components.forms.password-input', -'components.forms.password-with-strength','components.forms.text-input','components.layouts.app','components.layouts.gso-layout','components.layouts.guest', -'components.layouts.public','components.layouts.student-org-layout','components.layouts.superadmin','components.notifications.list','components.terms_and_conditions', -'components.tickets.latest-remark','components.tickets.progress-badge','components.tickets.progress-section','components.tickets.review','components.tickets.sections.additional-info', -'components.tickets.sections.attachments-list','components.tickets.sections.budget-info','components.tickets.sections.event-details','components.tickets.sections.organization-info','components.tickets.sections.schedule-venue', -'components.tickets.ticket-actions','components.tickets.ticket-card','components.tickets.ticket-pagination','components.tickets.ticket-preview','components.tickets.ticketinfo', -'components.tickets.upc-events-card','components.ui.action-message','components.ui.activity-item','components.ui.application-logo','components.ui.auth-session-status', -'components.ui.avatar','components.ui.danger-button','components.ui.dropdown','components.ui.dropdown-link','components.ui.empty-state', -'components.ui.input-error','components.ui.input-label','components.ui.modal','components.ui.nav-link','components.ui.primary-button', -'components.ui.responsive-nav-link','components.ui.secondary-button','components.ui.section-header','emails.password-changed','emails.reset-password', -'emails.tickets.gso-approved','emails.tickets.gso-for-revision','emails.tickets.gso-rejected','emails.tickets.ticket-amended','emails.tickets.ticket-approved', -'emails.tickets.ticket-comment','emails.tickets.ticket-for-revision','emails.tickets.ticket-forwarded-gso','emails.tickets.ticket-rejected','emails.tickets.ticket-revision-requested', -'emails.tickets.ticket-status-updated','emails.tickets.ticket-submitted','emails.verify-email','errors.401','errors.402', -'errors.403','errors.404','errors.419','errors.429','errors.500', -'errors.503','errors.layout','errors.minimal','livewire.about-us','livewire.avatar-selector', -'livewire.components.event-calendar','livewire.components.event-calendar-scripts','livewire.components.ticket-comments','livewire.gso.calendar','livewire.gso.dashboard', -'livewire.gso.details','livewire.gso.notifications','livewire.gso.profile','livewire.gso.reports','livewire.gso.ticket-review', -'livewire.layout.navigation','livewire.notification-dropdown','livewire.osa.archive','livewire.osa.archived-event-details','livewire.osa.communication', -'livewire.osa.dashboard','livewire.osa.notifications','livewire.osa.placeholders.communication','livewire.osa.placeholders.dashboard','livewire.osa.placeholders.event-calendar', -'livewire.osa.placeholders.notifications','livewire.osa.placeholders.stats-grid','livewire.osa.placeholders.table','livewire.osa.placeholders.ticket-cards','livewire.osa.placeholders.ticket-management', -'livewire.osa.placeholders.ticket-review','livewire.osa.profile','livewire.osa.reports','livewire.osa.ticket-management','livewire.osa.ticket-review.index', -'livewire.osa.ticket-review.show','livewire.pages.auth.confirm-password','livewire.pages.auth.forgot-password','livewire.pages.auth.login','livewire.pages.auth.register', -'livewire.pages.auth.reset-password','livewire.pages.auth.verify-email','livewire.placeholders.notification-list','livewire.profile.delete-user-form','livewire.profile.update-password-form', -'livewire.profile.update-profile-information-form','livewire.session-timeout','livewire.student-org.dashboard','livewire.student-org.edit-ticket','livewire.student-org.history', -'livewire.student-org.my-ticket','livewire.student-org.notifications','livewire.student-org.profile','livewire.student-org.reschedule','livewire.student-org.submit-ticket', -'livewire.superadmin.admin-tools.index','livewire.superadmin.archive.index','livewire.superadmin.calendar.index','livewire.superadmin.dashboard','livewire.superadmin.logs', -'livewire.superadmin.notifications','livewire.superadmin.placeholders.notifications','livewire.superadmin.profile','livewire.superadmin.reports.index','livewire.superadmin.roles.index', -'livewire.superadmin.system-settings.course-manager','livewire.superadmin.system-settings.event-type-manager','livewire.superadmin.system-settings.index','livewire.superadmin.system-settings.organization-manager','livewire.superadmin.tickets.index', -'livewire.superadmin.users.index','osa.welcome','reports.events-pdf','reports.gso.summary-pdf','reports.osa.summary-pdf', -'superadmin.archive','superadmin.dashboard','superadmin.logs','superadmin.reports','superadmin.roles', -'superadmin.settings','superadmin.users','volt-livewire::about-us','volt-livewire::avatar-selector','volt-livewire::components.event-calendar', -'volt-livewire::components.event-calendar-scripts','volt-livewire::components.ticket-comments','volt-livewire::gso.calendar','volt-livewire::gso.dashboard','volt-livewire::gso.details', -'volt-livewire::gso.notifications','volt-livewire::gso.profile','volt-livewire::gso.reports','volt-livewire::gso.ticket-review','volt-livewire::layout.navigation', -'volt-livewire::notification-dropdown','volt-livewire::osa.archive','volt-livewire::osa.archived-event-details','volt-livewire::osa.communication','volt-livewire::osa.dashboard', -'volt-livewire::osa.notifications','volt-livewire::osa.placeholders.communication','volt-livewire::osa.placeholders.dashboard','volt-livewire::osa.placeholders.event-calendar','volt-livewire::osa.placeholders.notifications', -'volt-livewire::osa.placeholders.stats-grid','volt-livewire::osa.placeholders.table','volt-livewire::osa.placeholders.ticket-cards','volt-livewire::osa.placeholders.ticket-management','volt-livewire::osa.placeholders.ticket-review', -'volt-livewire::osa.profile','volt-livewire::osa.reports','volt-livewire::osa.ticket-management','volt-livewire::osa.ticket-review.index','volt-livewire::osa.ticket-review.show', -'volt-livewire::pages.auth.confirm-password','volt-livewire::pages.auth.forgot-password','volt-livewire::pages.auth.login','volt-livewire::pages.auth.register','volt-livewire::pages.auth.reset-password', -'volt-livewire::pages.auth.verify-email','volt-livewire::placeholders.notification-list','volt-livewire::profile.delete-user-form','volt-livewire::profile.update-password-form','volt-livewire::profile.update-profile-information-form', -'volt-livewire::session-timeout','volt-livewire::student-org.dashboard','volt-livewire::student-org.edit-ticket','volt-livewire::student-org.history','volt-livewire::student-org.my-ticket', -'volt-livewire::student-org.notifications','volt-livewire::student-org.profile','volt-livewire::student-org.reschedule','volt-livewire::student-org.submit-ticket','volt-livewire::superadmin.admin-tools.index', -'volt-livewire::superadmin.archive.index','volt-livewire::superadmin.calendar.index','volt-livewire::superadmin.dashboard','volt-livewire::superadmin.logs','volt-livewire::superadmin.notifications', -'volt-livewire::superadmin.placeholders.notifications','volt-livewire::superadmin.profile','volt-livewire::superadmin.reports.index','volt-livewire::superadmin.roles.index','volt-livewire::superadmin.system-settings.course-manager', -'volt-livewire::superadmin.system-settings.event-type-manager','volt-livewire::superadmin.system-settings.index','volt-livewire::superadmin.system-settings.organization-manager','volt-livewire::superadmin.tickets.index','volt-livewire::superadmin.users.index', -'laravel-exceptions-renderer::components.badge','laravel-exceptions-renderer::components.empty-state','laravel-exceptions-renderer::components.file-with-line','laravel-exceptions-renderer::components.formatted-source','laravel-exceptions-renderer::components.frame', -'laravel-exceptions-renderer::components.frame-code','laravel-exceptions-renderer::components.header','laravel-exceptions-renderer::components.http-method','laravel-exceptions-renderer::components.icons.alert','laravel-exceptions-renderer::components.icons.check', -'laravel-exceptions-renderer::components.icons.chevron-left','laravel-exceptions-renderer::components.icons.chevron-right','laravel-exceptions-renderer::components.icons.chevrons-down-up','laravel-exceptions-renderer::components.icons.chevrons-left','laravel-exceptions-renderer::components.icons.chevrons-right', -'laravel-exceptions-renderer::components.icons.chevrons-up-down','laravel-exceptions-renderer::components.icons.copy','laravel-exceptions-renderer::components.icons.database','laravel-exceptions-renderer::components.icons.folder','laravel-exceptions-renderer::components.icons.folder-open', -'laravel-exceptions-renderer::components.icons.globe','laravel-exceptions-renderer::components.icons.info','laravel-exceptions-renderer::components.icons.laravel-ascii','laravel-exceptions-renderer::components.laravel-ascii-spotlight','laravel-exceptions-renderer::components.layout', -'laravel-exceptions-renderer::components.query','laravel-exceptions-renderer::components.request-body','laravel-exceptions-renderer::components.request-header','laravel-exceptions-renderer::components.request-url','laravel-exceptions-renderer::components.routing', -'laravel-exceptions-renderer::components.routing-parameter','laravel-exceptions-renderer::components.section-container','laravel-exceptions-renderer::components.separator','laravel-exceptions-renderer::components.syntax-highlight','laravel-exceptions-renderer::components.topbar', -'laravel-exceptions-renderer::components.trace','laravel-exceptions-renderer::components.vendor-frame','laravel-exceptions-renderer::components.vendor-frames','laravel-exceptions-renderer::markdown','laravel-exceptions-renderer::show', -'laravel-exceptions::401','laravel-exceptions::402','laravel-exceptions::403','laravel-exceptions::404','laravel-exceptions::419', -'laravel-exceptions::429','laravel-exceptions::500','laravel-exceptions::503','laravel-exceptions::layout','laravel-exceptions::minimal', -'livewire::bootstrap','livewire::simple-bootstrap','livewire::simple-tailwind','livewire::tailwind','notifications::email', -'pagination::bootstrap-4','pagination::bootstrap-5','pagination::default','pagination::semantic-ui','pagination::simple-bootstrap-4', -'pagination::simple-bootstrap-5','pagination::simple-default','pagination::simple-tailwind','pagination::tailwind',); +'components.announcement-banner','components.app-layout','components.comment-boxes.normal-comment','components.comment-boxes.ticket-comments','components.content-section', +'components.dashboard.quick-action-card','components.dashboard.stat-card','components.documentary-requirements','components.footer','components.forms.email-input', +'components.forms.input-error','components.forms.input-label','components.forms.password-input','components.forms.password-with-strength','components.forms.text-input', +'components.layouts.app','components.layouts.gso-layout','components.layouts.guest','components.layouts.public','components.layouts.student-org-layout', +'components.layouts.superadmin','components.notifications.list','components.terms_and_conditions','components.tickets.latest-remark','components.tickets.progress-badge', +'components.tickets.progress-section','components.tickets.review','components.tickets.sections.additional-info','components.tickets.sections.attachments-list','components.tickets.sections.budget-info', +'components.tickets.sections.event-details','components.tickets.sections.organization-info','components.tickets.sections.schedule-venue','components.tickets.ticket-actions','components.tickets.ticket-card', +'components.tickets.ticket-pagination','components.tickets.ticket-preview','components.tickets.ticketinfo','components.tickets.upc-events-card','components.trix-input', +'components.ui.action-message','components.ui.activity-item','components.ui.application-logo','components.ui.auth-session-status','components.ui.avatar', +'components.ui.danger-button','components.ui.dropdown','components.ui.dropdown-link','components.ui.empty-state','components.ui.input-error', +'components.ui.input-label','components.ui.modal','components.ui.nav-link','components.ui.primary-button','components.ui.responsive-nav-link', +'components.ui.secondary-button','components.ui.section-header','emails.password-changed','emails.reset-password','emails.tickets.gso-approved', +'emails.tickets.gso-for-revision','emails.tickets.gso-rejected','emails.tickets.ticket-amended','emails.tickets.ticket-approved','emails.tickets.ticket-comment', +'emails.tickets.ticket-for-revision','emails.tickets.ticket-forwarded-gso','emails.tickets.ticket-rejected','emails.tickets.ticket-revision-requested','emails.tickets.ticket-status-updated', +'emails.tickets.ticket-submitted','emails.verify-email','errors.401','errors.402','errors.403', +'errors.404','errors.419','errors.429','errors.500','errors.503', +'errors.layout','errors.minimal','livewire.about-us','livewire.avatar-selector','livewire.components.event-calendar', +'livewire.components.event-calendar-scripts','livewire.components.ticket-comments','livewire.gso.calendar','livewire.gso.dashboard','livewire.gso.details', +'livewire.gso.notifications','livewire.gso.profile','livewire.gso.reports','livewire.gso.ticket-review','livewire.layout.navigation', +'livewire.notification-dropdown','livewire.osa.archive','livewire.osa.archived-event-details','livewire.osa.communication','livewire.osa.dashboard', +'livewire.osa.notifications','livewire.osa.placeholders.communication','livewire.osa.placeholders.dashboard','livewire.osa.placeholders.event-calendar','livewire.osa.placeholders.notifications', +'livewire.osa.placeholders.stats-grid','livewire.osa.placeholders.table','livewire.osa.placeholders.ticket-cards','livewire.osa.placeholders.ticket-management','livewire.osa.placeholders.ticket-review', +'livewire.osa.profile','livewire.osa.reports','livewire.osa.ticket-management','livewire.osa.ticket-review.index','livewire.osa.ticket-review.show', +'livewire.pages.auth.confirm-password','livewire.pages.auth.forgot-password','livewire.pages.auth.login','livewire.pages.auth.register','livewire.pages.auth.reset-password', +'livewire.pages.auth.verify-email','livewire.placeholders.notification-list','livewire.profile.delete-user-form','livewire.profile.update-password-form','livewire.profile.update-profile-information-form', +'livewire.session-timeout','livewire.student-org.dashboard','livewire.student-org.edit-ticket','livewire.student-org.history','livewire.student-org.my-ticket', +'livewire.student-org.notifications','livewire.student-org.profile','livewire.student-org.reschedule','livewire.student-org.submit-ticket','livewire.superadmin.admin-tools.index', +'livewire.superadmin.archive.index','livewire.superadmin.calendar.index','livewire.superadmin.dashboard','livewire.superadmin.logs','livewire.superadmin.notifications', +'livewire.superadmin.placeholders.dashboard','livewire.superadmin.placeholders.logs','livewire.superadmin.placeholders.notifications','livewire.superadmin.placeholders.reports','livewire.superadmin.placeholders.users', +'livewire.superadmin.profile','livewire.superadmin.reports.index','livewire.superadmin.roles.index','livewire.superadmin.system-settings.content-section-editor','livewire.superadmin.system-settings.content-section-manager', +'livewire.superadmin.system-settings.course-manager','livewire.superadmin.system-settings.event-type-editor','livewire.superadmin.system-settings.event-type-manager','livewire.superadmin.system-settings.index','livewire.superadmin.system-settings.organization-manager', +'livewire.superadmin.system-settings.partials.organization-card','livewire.superadmin.tickets.index','livewire.superadmin.users.index','osa.welcome','reports.events-pdf', +'reports.gso.summary-pdf','reports.osa.summary-pdf','superadmin.archive','superadmin.dashboard','superadmin.logs', +'superadmin.reports','superadmin.roles','superadmin.settings','superadmin.users','volt-livewire::about-us', +'volt-livewire::avatar-selector','volt-livewire::components.event-calendar','volt-livewire::components.event-calendar-scripts','volt-livewire::components.ticket-comments','volt-livewire::gso.calendar', +'volt-livewire::gso.dashboard','volt-livewire::gso.details','volt-livewire::gso.notifications','volt-livewire::gso.profile','volt-livewire::gso.reports', +'volt-livewire::gso.ticket-review','volt-livewire::layout.navigation','volt-livewire::notification-dropdown','volt-livewire::osa.archive','volt-livewire::osa.archived-event-details', +'volt-livewire::osa.communication','volt-livewire::osa.dashboard','volt-livewire::osa.notifications','volt-livewire::osa.placeholders.communication','volt-livewire::osa.placeholders.dashboard', +'volt-livewire::osa.placeholders.event-calendar','volt-livewire::osa.placeholders.notifications','volt-livewire::osa.placeholders.stats-grid','volt-livewire::osa.placeholders.table','volt-livewire::osa.placeholders.ticket-cards', +'volt-livewire::osa.placeholders.ticket-management','volt-livewire::osa.placeholders.ticket-review','volt-livewire::osa.profile','volt-livewire::osa.reports','volt-livewire::osa.ticket-management', +'volt-livewire::osa.ticket-review.index','volt-livewire::osa.ticket-review.show','volt-livewire::pages.auth.confirm-password','volt-livewire::pages.auth.forgot-password','volt-livewire::pages.auth.login', +'volt-livewire::pages.auth.register','volt-livewire::pages.auth.reset-password','volt-livewire::pages.auth.verify-email','volt-livewire::placeholders.notification-list','volt-livewire::profile.delete-user-form', +'volt-livewire::profile.update-password-form','volt-livewire::profile.update-profile-information-form','volt-livewire::session-timeout','volt-livewire::student-org.dashboard','volt-livewire::student-org.edit-ticket', +'volt-livewire::student-org.history','volt-livewire::student-org.my-ticket','volt-livewire::student-org.notifications','volt-livewire::student-org.profile','volt-livewire::student-org.reschedule', +'volt-livewire::student-org.submit-ticket','volt-livewire::superadmin.admin-tools.index','volt-livewire::superadmin.archive.index','volt-livewire::superadmin.calendar.index','volt-livewire::superadmin.dashboard', +'volt-livewire::superadmin.logs','volt-livewire::superadmin.notifications','volt-livewire::superadmin.placeholders.dashboard','volt-livewire::superadmin.placeholders.logs','volt-livewire::superadmin.placeholders.notifications', +'volt-livewire::superadmin.placeholders.reports','volt-livewire::superadmin.placeholders.users','volt-livewire::superadmin.profile','volt-livewire::superadmin.reports.index','volt-livewire::superadmin.roles.index', +'volt-livewire::superadmin.system-settings.content-section-editor','volt-livewire::superadmin.system-settings.content-section-manager','volt-livewire::superadmin.system-settings.course-manager','volt-livewire::superadmin.system-settings.event-type-editor','volt-livewire::superadmin.system-settings.event-type-manager', +'volt-livewire::superadmin.system-settings.index','volt-livewire::superadmin.system-settings.organization-manager','volt-livewire::superadmin.system-settings.partials.organization-card','volt-livewire::superadmin.tickets.index','volt-livewire::superadmin.users.index', +'71ffe14e2608a75a2cb5aed52d105549::styles','laravel-exceptions-renderer::components.badge','laravel-exceptions-renderer::components.empty-state','laravel-exceptions-renderer::components.file-with-line','laravel-exceptions-renderer::components.formatted-source', +'laravel-exceptions-renderer::components.frame','laravel-exceptions-renderer::components.frame-code','laravel-exceptions-renderer::components.header','laravel-exceptions-renderer::components.http-method','laravel-exceptions-renderer::components.icons.alert', +'laravel-exceptions-renderer::components.icons.check','laravel-exceptions-renderer::components.icons.chevron-left','laravel-exceptions-renderer::components.icons.chevron-right','laravel-exceptions-renderer::components.icons.chevrons-down-up','laravel-exceptions-renderer::components.icons.chevrons-left', +'laravel-exceptions-renderer::components.icons.chevrons-right','laravel-exceptions-renderer::components.icons.chevrons-up-down','laravel-exceptions-renderer::components.icons.copy','laravel-exceptions-renderer::components.icons.database','laravel-exceptions-renderer::components.icons.folder', +'laravel-exceptions-renderer::components.icons.folder-open','laravel-exceptions-renderer::components.icons.globe','laravel-exceptions-renderer::components.icons.info','laravel-exceptions-renderer::components.icons.laravel-ascii','laravel-exceptions-renderer::components.laravel-ascii-spotlight', +'laravel-exceptions-renderer::components.layout','laravel-exceptions-renderer::components.query','laravel-exceptions-renderer::components.request-body','laravel-exceptions-renderer::components.request-header','laravel-exceptions-renderer::components.request-url', +'laravel-exceptions-renderer::components.routing','laravel-exceptions-renderer::components.routing-parameter','laravel-exceptions-renderer::components.section-container','laravel-exceptions-renderer::components.separator','laravel-exceptions-renderer::components.syntax-highlight', +'laravel-exceptions-renderer::components.topbar','laravel-exceptions-renderer::components.trace','laravel-exceptions-renderer::components.vendor-frame','laravel-exceptions-renderer::components.vendor-frames','laravel-exceptions-renderer::markdown', +'laravel-exceptions-renderer::show','laravel-exceptions::401','laravel-exceptions::402','laravel-exceptions::403','laravel-exceptions::404', +'laravel-exceptions::419','laravel-exceptions::429','laravel-exceptions::500','laravel-exceptions::503','laravel-exceptions::layout', +'laravel-exceptions::minimal','livewire::bootstrap','livewire::simple-bootstrap','livewire::simple-tailwind','livewire::tailwind', +'notifications::email','pagination::bootstrap-4','pagination::bootstrap-5','pagination::default','pagination::semantic-ui', +'pagination::simple-bootstrap-4','pagination::simple-bootstrap-5','pagination::simple-default','pagination::simple-tailwind','pagination::tailwind', +'rich-text-laravel::attachables._content','rich-text-laravel::attachables._image_gallery','rich-text-laravel::attachables._missing_attachable','rich-text-laravel::attachables._remote_file','rich-text-laravel::attachables._remote_image', +'rich-text-laravel::attachment_galleries._attachment_gallery','rich-text-laravel::components.styles','rich-text-laravel::content',); registerArgumentsSet('translations', 'D:.Coding.laragon.www.org-events-scheduling-system.vendor.laravel.framework.src.Illuminate.Translation.lang.en.auth.failed','D:.Coding.laragon.www.org-events-scheduling-system.vendor.laravel.framework.src.Illuminate.Translation.lang.en.auth.password','D:.Coding.laragon.www.org-events-scheduling-system.vendor.laravel.framework.src.Illuminate.Translation.lang.en.auth.throttle','D:.Coding.laragon.www.org-events-scheduling-system.vendor.laravel.framework.src.Illuminate.Translation.lang.en.pagination.previous','D:.Coding.laragon.www.org-events-scheduling-system.vendor.laravel.framework.src.Illuminate.Translation.lang.en.pagination.next', 'D:.Coding.laragon.www.org-events-scheduling-system.vendor.laravel.framework.src.Illuminate.Translation.lang.en.passwords.reset','D:.Coding.laragon.www.org-events-scheduling-system.vendor.laravel.framework.src.Illuminate.Translation.lang.en.passwords.sent','D:.Coding.laragon.www.org-events-scheduling-system.vendor.laravel.framework.src.Illuminate.Translation.lang.en.passwords.throttled','D:.Coding.laragon.www.org-events-scheduling-system.vendor.laravel.framework.src.Illuminate.Translation.lang.en.passwords.token','D:.Coding.laragon.www.org-events-scheduling-system.vendor.laravel.framework.src.Illuminate.Translation.lang.en.passwords.user', @@ -3057,9 +3083,9 @@ 'DB_PASSWORD','SESSION_DRIVER','SESSION_LIFETIME','SESSION_ENCRYPT','SESSION_PATH', 'SESSION_DOMAIN','SESSION_WARNING_TIME','BROADCAST_CONNECTION','FILESYSTEM_DISK','QUEUE_CONNECTION', 'CACHE_STORE','MEMCACHED_HOST','REDIS_CLIENT','REDIS_HOST','REDIS_PASSWORD', -'REDIS_PORT','MAIL_MAILER','MAIL_HOST','MAIL_PORT','MAIL_USERNAME', -'MAIL_PASSWORD','MAIL_ENCRYPTION','MAIL_FROM_ADDRESS','MAIL_FROM_NAME','AWS_ACCESS_KEY_ID', -'AWS_SECRET_ACCESS_KEY','AWS_DEFAULT_REGION','AWS_BUCKET','AWS_USE_PATH_STYLE_ENDPOINT','VITE_APP_NAME',); +'REDIS_PORT','MAIL_MAILER','RESEND_API_KEY','MAIL_FROM_ADDRESS','MAIL_FROM_NAME', +'AWS_ACCESS_KEY_ID','AWS_SECRET_ACCESS_KEY','AWS_DEFAULT_REGION','AWS_BUCKET','AWS_USE_PATH_STYLE_ENDPOINT', +'VITE_APP_NAME',); expectedArguments(\Illuminate\Support\Facades\Gate::has(), 0, argumentsSet('auth')); expectedArguments(\Illuminate\Support\Facades\Gate::allows(), 0, argumentsSet('auth')); diff --git a/_ide_helper.php b/_ide_helper.php index 85a3599..1b18b32 100644 --- a/_ide_helper.php +++ b/_ide_helper.php @@ -5,7 +5,7 @@ /** * A helper file for Laravel, to provide autocomplete information to your IDE - * Generated for Laravel 12.46.0. + * Generated for Laravel 12.49.0. * * This file should not be included in your code, only analyzed by your IDE! * @@ -4847,7 +4847,7 @@ public static function rememberForever($key, $callback) * Retrieve an item from the cache by key, refreshing it in the background if it is stale. * * @template TCacheValue - * @param string $key + * @param \BackedEnum|\UnitEnum|string $key * @param array{ 0: \DateTimeInterface|\DateInterval|int, 1: \DateTimeInterface|\DateInterval|int } $ttl * @param (callable(): TCacheValue) $callback * @param array{ seconds?: int, owner?: string }|null $lock @@ -4861,6 +4861,25 @@ public static function flexible($key, $ttl, $callback, $lock = null, $alwaysDefe return $instance->flexible($key, $ttl, $callback, $lock, $alwaysDefer); } + /** + * Execute a callback while holding an atomic lock on a cache mutex to prevent overlapping calls. + * + * @template TReturn + * @param \BackedEnum|\UnitEnum|string $key + * @param callable(): TReturn $callback + * @param int $lockFor + * @param int $waitFor + * @param string|null $owner + * @return TReturn + * @throws \Illuminate\Contracts\Cache\LockTimeoutException + * @static + */ + public static function withoutOverlapping($key, $callback, $lockFor = 0, $waitFor = 10, $owner = null) + { + /** @var \Illuminate\Cache\Repository $instance */ + return $instance->withoutOverlapping($key, $callback, $lockFor, $waitFor, $owner); + } + /** * Remove an item from the cache. * @@ -5033,7 +5052,7 @@ public static function setEventDispatcher($events) /** * Determine if a cached value exists. * - * @param string $key + * @param \BackedEnum|\UnitEnum|string $key * @return bool * @static */ @@ -5073,7 +5092,7 @@ public static function offsetSet($key, $value) /** * Remove an item from the cache. * - * @param string $key + * @param \BackedEnum|\UnitEnum|string $key * @return void * @static */ @@ -7883,6 +7902,20 @@ public static function setReadPdo($pdo) return $instance->setReadPdo($pdo); } + /** + * Set the read PDO connection configuration. + * + * @param array $config + * @return \Illuminate\Database\MySqlConnection + * @static + */ + public static function setReadPdoConfig($config) + { + //Method inherited from \Illuminate\Database\Connection + /** @var \Illuminate\Database\MySqlConnection $instance */ + return $instance->setReadPdoConfig($config); + } + /** * Get the database connection name. * @@ -12009,6 +12042,57 @@ public static function forgetDrivers() return $instance->forgetDrivers(); } + /** + * Register a custom macro. + * + * @param string $name + * @param object|callable $macro + * @param-closure-this static $macro + * @return void + * @static + */ + public static function macro($name, $macro) + { + \Illuminate\Notifications\ChannelManager::macro($name, $macro); + } + + /** + * Mix another object into the class. + * + * @param object $mixin + * @param bool $replace + * @return void + * @throws \ReflectionException + * @static + */ + public static function mixin($mixin, $replace = true) + { + \Illuminate\Notifications\ChannelManager::mixin($mixin, $replace); + } + + /** + * Checks if macro is registered. + * + * @param string $name + * @return bool + * @static + */ + public static function hasMacro($name) + { + return \Illuminate\Notifications\ChannelManager::hasMacro($name); + } + + /** + * Flush the existing macros. + * + * @return void + * @static + */ + public static function flushMacros() + { + \Illuminate\Notifications\ChannelManager::flushMacros(); + } + /** * Assert if a notification was sent on-demand based on a truth-test callback. * @@ -12192,57 +12276,6 @@ public static function sentNotifications() return $instance->sentNotifications(); } - /** - * Register a custom macro. - * - * @param string $name - * @param object|callable $macro - * @param-closure-this static $macro - * @return void - * @static - */ - public static function macro($name, $macro) - { - \Illuminate\Support\Testing\Fakes\NotificationFake::macro($name, $macro); - } - - /** - * Mix another object into the class. - * - * @param object $mixin - * @param bool $replace - * @return void - * @throws \ReflectionException - * @static - */ - public static function mixin($mixin, $replace = true) - { - \Illuminate\Support\Testing\Fakes\NotificationFake::mixin($mixin, $replace); - } - - /** - * Checks if macro is registered. - * - * @param string $name - * @return bool - * @static - */ - public static function hasMacro($name) - { - return \Illuminate\Support\Testing\Fakes\NotificationFake::hasMacro($name); - } - - /** - * Flush the existing macros. - * - * @return void - * @static - */ - public static function flushMacros() - { - \Illuminate\Support\Testing\Fakes\NotificationFake::flushMacros(); - } - } /** * @method static string sendResetLink(array $credentials, \Closure|null $callback = null) @@ -12931,6 +12964,20 @@ public static function assertPushed($job, $callback = null) $instance->assertPushed($job, $callback); } + /** + * Assert if a job was pushed a number of times. + * + * @param string $job + * @param int $times + * @return void + * @static + */ + public static function assertPushedTimes($job, $times = 1) + { + /** @var \Illuminate\Support\Testing\Fakes\QueueFake $instance */ + $instance->assertPushedTimes($job, $times); + } + /** * Assert if a job was pushed based on a truth-test callback. * @@ -18862,6 +18909,35 @@ public static function withoutForeignKeyConstraints($callback) return $instance->withoutForeignKeyConstraints($callback); } + /** + * Create the vector extension on the schema if it does not exist. + * + * @param string|null $schema + * @return void + * @static + */ + public static function ensureVectorExtensionExists($schema = null) + { + //Method inherited from \Illuminate\Database\Schema\Builder + /** @var \Illuminate\Database\Schema\MySqlBuilder $instance */ + $instance->ensureVectorExtensionExists($schema); + } + + /** + * Create a new extension on the schema if it does not exist. + * + * @param string $name + * @param string|null $schema + * @return void + * @static + */ + public static function ensureExtensionExists($name, $schema = null) + { + //Method inherited from \Illuminate\Database\Schema\Builder + /** @var \Illuminate\Database\Schema\MySqlBuilder $instance */ + $instance->ensureExtensionExists($name, $schema); + } + /** * Get the default schema name for the connection. * @@ -19222,7 +19298,7 @@ public static function except($keys) /** * Checks if a key exists. * - * @param string|array $key + * @param \BackedEnum|\UnitEnum|string|array $key * @return bool * @static */ @@ -19235,7 +19311,7 @@ public static function exists($key) /** * Determine if the given key is missing from the session data. * - * @param string|array $key + * @param \BackedEnum|\UnitEnum|string|array $key * @return bool * @static */ @@ -19248,7 +19324,7 @@ public static function missing($key) /** * Determine if a key is present and not null. * - * @param string|array $key + * @param \BackedEnum|\UnitEnum|string|array $key * @return bool * @static */ @@ -19261,7 +19337,7 @@ public static function has($key) /** * Determine if any of the given keys are present and not null. * - * @param string|array $key + * @param \BackedEnum|\UnitEnum|string|array $key * @return bool * @static */ @@ -19412,7 +19488,7 @@ public static function decrement($key, $amount = 1) /** * Flash a key / value pair to the session. * - * @param string $key + * @param \BackedEnum|\UnitEnum|string $key * @param mixed $value * @return void * @static @@ -19426,7 +19502,7 @@ public static function flash($key, $value = true) /** * Flash a key / value pair to the session for immediate use. * - * @param string $key + * @param \BackedEnum|\UnitEnum|string $key * @param mixed $value * @return void * @static @@ -26977,6 +27053,20 @@ public static function selectSub($query, $as) return $instance->selectSub($query, $as); } + /** + * Add a select expression to the query. + * + * @param \Illuminate\Contracts\Database\Query\Expression $expression + * @param string $as + * @return \Illuminate\Database\Eloquent\Builder + * @static + */ + public static function selectExpression($expression, $as) + { + /** @var \Illuminate\Database\Query\Builder $instance */ + return $instance->selectExpression($expression, $as); + } + /** * Add a new "raw" select expression to the query. * @@ -27032,6 +27122,21 @@ public static function addSelect($column) return $instance->addSelect($column); } + /** + * Add a vector-similarity selection to the query. + * + * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param \Illuminate\Support\Collection|\Illuminate\Contracts\Support\Arrayable|array|string $vector + * @param string|null $as + * @return \Illuminate\Database\Eloquent\Builder + * @static + */ + public static function selectVectorDistance($column, $vector, $as = null) + { + /** @var \Illuminate\Database\Query\Builder $instance */ + return $instance->selectVectorDistance($column, $vector, $as); + } + /** * Force the query to only return distinct results. * @@ -27367,6 +27472,53 @@ public static function orWhereColumn($first, $operator = null, $second = null) return $instance->orWhereColumn($first, $operator, $second); } + /** + * Add a vector similarity clause to the query, filtering by minimum similarity and ordering by similarity. + * + * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param \Illuminate\Support\Collection|\Illuminate\Contracts\Support\Arrayable|array|string $vector + * @param float $minSimilarity A value between 0.0 and 1.0, where 1.0 is identical. + * @param bool $order + * @return \Illuminate\Database\Eloquent\Builder + * @static + */ + public static function whereVectorSimilarTo($column, $vector, $minSimilarity = 0.6, $order = true) + { + /** @var \Illuminate\Database\Query\Builder $instance */ + return $instance->whereVectorSimilarTo($column, $vector, $minSimilarity, $order); + } + + /** + * Add a vector distance "where" clause to the query. + * + * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param \Illuminate\Support\Collection|\Illuminate\Contracts\Support\Arrayable|array|string $vector + * @param float $maxDistance + * @param string $boolean + * @return \Illuminate\Database\Eloquent\Builder + * @static + */ + public static function whereVectorDistanceLessThan($column, $vector, $maxDistance, $boolean = 'and') + { + /** @var \Illuminate\Database\Query\Builder $instance */ + return $instance->whereVectorDistanceLessThan($column, $vector, $maxDistance, $boolean); + } + + /** + * Add a vector distance "or where" clause to the query. + * + * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param \Illuminate\Support\Collection|\Illuminate\Contracts\Support\Arrayable|array|string $vector + * @param float $maxDistance + * @return \Illuminate\Database\Eloquent\Builder + * @static + */ + public static function orWhereVectorDistanceLessThan($column, $vector, $maxDistance) + { + /** @var \Illuminate\Database\Query\Builder $instance */ + return $instance->orWhereVectorDistanceLessThan($column, $vector, $maxDistance); + } + /** * Add a raw "where" clause to the query. * @@ -28689,6 +28841,20 @@ public static function orderByDesc($column) return $instance->orderByDesc($column); } + /** + * Add a vector-distance "order by" clause to the query. + * + * @param \Illuminate\Contracts\Database\Query\Expression|string $column + * @param \Illuminate\Support\Collection|\Illuminate\Contracts\Support\Arrayable|array $vector + * @return \Illuminate\Database\Eloquent\Builder + * @static + */ + public static function orderByVectorDistance($column, $vector) + { + /** @var \Illuminate\Database\Query\Builder $instance */ + return $instance->orderByVectorDistance($column, $vector); + } + /** * Put the query's results in random order. * diff --git a/app/Livewire/AvatarSelector.php b/app/Livewire/AvatarSelector.php index 21f2a9c..7ef794f 100644 --- a/app/Livewire/AvatarSelector.php +++ b/app/Livewire/AvatarSelector.php @@ -4,6 +4,7 @@ use App\Traits\WithProfilePhoto; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Log; use Livewire\Component; use Mary\Traits\Toast; @@ -81,19 +82,28 @@ public function getAvatarOptionsProperty() */ public function updatedPhoto() { - // Only validate the photo, don't save yet - $this->validate( - ['photo' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:10240']], - [ - 'photo.image' => 'The file must be an image.', - 'photo.mimes' => 'The image must be a JPG, PNG, or WebP file.', - 'photo.max' => 'The image must not exceed 10MB.', - ] - ); - // Photo preview will be shown automatically via $photo->temporaryUrl() - - // Dispatch event to trigger avatar re-initialization - $this->dispatch('photo-uploaded'); + try { + // Only validate the photo, don't save yet + $this->validate( + ['photo' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120']], + [ + 'photo.image' => 'The file must be an image.', + 'photo.mimes' => 'The image must be a JPG, PNG, or WebP file.', + 'photo.max' => 'The image must not exceed 5MB.', + ] + ); + // Photo preview will be shown automatically via $photo->temporaryUrl() + + // Dispatch event to trigger avatar re-initialization + $this->dispatch('photo-uploaded'); + } catch (\Illuminate\Validation\ValidationException $e) { + $this->photo = null; + throw $e; // Re-throw to let Livewire handle validation display + } catch (\Exception $e) { + Log::error('Photo upload failed: ' . $e->getMessage()); + $this->photo = null; + $this->error('The photo failed to upload. Please try again with a smaller file.', position: 'toast-top'); + } } /** @@ -109,14 +119,20 @@ public function cancelPhotoUpload() */ public function saveUploadedPhoto() { - if (!$this->photo) { - $this->error('No photo to save.', position: 'toast-top'); - return; - } + try { + if (!$this->photo) { + $this->error('No photo to save.', position: 'toast-top'); + return; + } - $this->saveProfilePhoto(); - $this->dispatch('avatar-updated'); - $this->js('window.dispatchEvent(new CustomEvent("avatar-changed"))'); + $this->saveProfilePhoto(); + $this->dispatch('avatar-updated'); + $this->js('window.dispatchEvent(new CustomEvent("avatar-changed"))'); + } catch (\Exception $e) { + Log::error('Failed to save profile photo: ' . $e->getMessage()); + $this->photo = null; + $this->error('Failed to save the photo. Please try again.', position: 'toast-top'); + } } /** diff --git a/app/Livewire/Components/EventCalendar.php b/app/Livewire/Components/EventCalendar.php index 018873e..2356db0 100644 --- a/app/Livewire/Components/EventCalendar.php +++ b/app/Livewire/Components/EventCalendar.php @@ -265,12 +265,12 @@ public function eventsForCalendar() ]) // Always show only approved event schedules ->where('status', 'approved') - // Filter by ticket status - empty means show both approved and rescheduled + // Filter by ticket status - 'all' or empty means show both approved and rescheduled ->whereHas('event.ticket', function ($query) { - if ($this->statusFilter) { + if ($this->statusFilter && $this->statusFilter !== 'all') { $query->where('status', $this->statusFilter); } else { - // Show both approved and rescheduled when no specific filter + // Show both approved and rescheduled when 'all' or no specific filter $query->whereIn('status', ['approved', 'rescheduled']); } }) @@ -336,7 +336,8 @@ public function eventsForCalendar() 'start' => $startISO, 'end' => $endISO, 'allDay' => false, - 'backgroundColor' => $this->getEventColor($event), // 50% opacity + 'display' => 'block', // Show as solid bar instead of dot + 'backgroundColor' => $this->getEventColor($event), 'textColor' => '#ffffff', 'extendedProps' => [ 'organization' => $event->ticket->user->studentOrganization->org_name ?? 'No Organization', @@ -492,12 +493,12 @@ public function uniqueEventsCount() { // Count unique events that match the current filters $query = Event_Schedule::query() - // Filter by ticket status - empty means show both approved and rescheduled + // Filter by ticket status - 'all' or empty means show both approved and rescheduled ->whereHas('event.ticket', function ($query) { - if ($this->statusFilter) { + if ($this->statusFilter && $this->statusFilter !== 'all') { $query->where('status', $this->statusFilter); } else { - // Show both approved and rescheduled when no specific filter + // Show both approved and rescheduled when 'all' or no specific filter $query->whereIn('status', ['approved', 'rescheduled']); } }) @@ -578,13 +579,13 @@ public function upcomingEventsThisMonth() $hexColor = $this->getEventColor($event); $colorName = $this->hexToTailwindColor($hexColor); - $orgLogo = $event->ticket->user->studentOrganization->logo ?? null; + $org = $event->ticket->user->studentOrganization ?? null; return [ 'title' => $event->ticket->title ?? 'Untitled Event', 'description' => $event->ticket->description ?? null, - 'organization' => $event->ticket->user->studentOrganization->org_name ?? 'No Organization', - 'organizationLogo' => $orgLogo ? $orgLogo->logo_url : asset('images/default-org-logo.svg'), + 'organization' => $org->org_name ?? 'No Organization', + 'organizationLogo' => $org ? $org->logo_url : asset('images/default-org-logo.svg'), 'eventType' => $event->eventType?->type_name ?? 'N/A', 'date' => $dateDisplay, 'time' => $timeRange, diff --git a/app/Livewire/Faq.php b/app/Livewire/Faq.php new file mode 100644 index 0000000..da1cece --- /dev/null +++ b/app/Livewire/Faq.php @@ -0,0 +1,34 @@ +exists(); + } + + public function render() + { + return view('livewire.faq'); + } +} diff --git a/app/Livewire/Gso/Notifications.php b/app/Livewire/Gso/Notifications.php index 5834317..a16fa49 100644 --- a/app/Livewire/Gso/Notifications.php +++ b/app/Livewire/Gso/Notifications.php @@ -2,6 +2,8 @@ namespace App\Livewire\Gso; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Log; use Livewire\Attributes\Layout; use Livewire\Attributes\On; use Livewire\Attributes\Title; @@ -123,63 +125,83 @@ public function getNotificationsProperty() public function markAsRead($notificationId) { - $user = auth()->user(); - $notification = $user->notifications()->find($notificationId); - - if ($notification) { - $notification->markAsRead(); - // Clear cache to refresh counts - \Illuminate\Support\Facades\Cache::forget("osa_notifications_counts_{$user->user_id}"); - $this->loadCounts(); + try { + $user = auth()->user(); + $notification = $user->notifications()->find($notificationId); + + if ($notification) { + $notification->markAsRead(); + // Clear cache to refresh counts + Cache::forget("gso_notifications_counts_{$user->user_id}"); + $this->loadCounts(); + } + } catch (\Exception $e) { + Log::error('Failed to mark notification as read: ' . $e->getMessage()); + $this->error('Failed to mark notification as read.', position: 'toast-top'); } } public function markAllAsRead() { - $user = auth()->user(); - $user->unreadNotifications->markAsRead(); - // Clear cache to refresh counts - \Illuminate\Support\Facades\Cache::forget("osa_notifications_counts_{$user->user_id}"); - $this->loadCounts(); + try { + $user = auth()->user(); + $user->unreadNotifications->markAsRead(); + // Clear cache to refresh counts + Cache::forget("gso_notifications_counts_{$user->user_id}"); + $this->loadCounts(); - $this->success('All notifications marked as read.', position: 'toast-top'); + $this->success('All notifications marked as read.', position: 'toast-top'); + } catch (\Exception $e) { + Log::error('Failed to mark all notifications as read: ' . $e->getMessage()); + $this->error('Failed to mark all notifications as read.', position: 'toast-top'); + } } public function deleteNotification($notificationId) { - $user = auth()->user(); - $notification = $user->notifications()->find($notificationId); + try { + $user = auth()->user(); + $notification = $user->notifications()->find($notificationId); - if ($notification) { - $notification->delete(); + if ($notification) { + $notification->delete(); - // Clear cache to refresh counts - \Illuminate\Support\Facades\Cache::forget("osa_notifications_counts_{$user->user_id}"); - $this->loadCounts(); - $this->resetPage(); + // Clear cache to refresh counts + Cache::forget("gso_notifications_counts_{$user->user_id}"); + $this->loadCounts(); + $this->resetPage(); - $this->success('Notification deleted.', position: 'toast-top'); + $this->success('Notification deleted.', position: 'toast-top'); + } + } catch (\Exception $e) { + Log::error('Failed to delete notification: ' . $e->getMessage()); + $this->error('Failed to delete notification.', position: 'toast-top'); } } public function clearAllRead() { - $user = auth()->user(); + try { + $user = auth()->user(); - // Only delete notifications that have been read - $deletedCount = $user->notifications() - ->whereNotNull('read_at') - ->delete(); + // Only delete notifications that have been read + $deletedCount = $user->notifications() + ->whereNotNull('read_at') + ->delete(); - // Clear cache to refresh counts - \Illuminate\Support\Facades\Cache::forget("osa_notifications_counts_{$user->user_id}"); - $this->loadCounts(); - $this->resetPage(); + // Clear cache to refresh counts + Cache::forget("gso_notifications_counts_{$user->user_id}"); + $this->loadCounts(); + $this->resetPage(); - if ($deletedCount > 0) { - $this->success("{$deletedCount} read notification(s) cleared.", position: 'toast-top'); - } else { - $this->info('No read notifications to clear.', position: 'toast-top'); + if ($deletedCount > 0) { + $this->success("{$deletedCount} read notification(s) cleared.", position: 'toast-top'); + } else { + $this->info('No read notifications to clear.', position: 'toast-top'); + } + } catch (\Exception $e) { + Log::error('Failed to clear read notifications: ' . $e->getMessage()); + $this->error('Failed to clear read notifications.', position: 'toast-top'); } } diff --git a/app/Livewire/Gso/Profile.php b/app/Livewire/Gso/Profile.php index 5b53ee0..910b261 100644 --- a/app/Livewire/Gso/Profile.php +++ b/app/Livewire/Gso/Profile.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Log; use Illuminate\Validation\Rules\Password; use Livewire\Attributes\Layout; use Livewire\Attributes\On; @@ -64,81 +65,95 @@ public function refreshUser(): void public function updateProfile() { - $this->validate([ - 'name' => [ - 'required', - 'string', - 'min:2', - 'max:255', - 'regex:/^[a-zA-Z\s\-\.]+$/', - ], - 'email' => [ - 'required', - 'email:rfc,dns', - 'max:255', - 'unique:users,email,' . Auth::id() . ',user_id', - ], - 'phone' => [ - 'nullable', - 'string', - 'regex:/^(\+63|0)?[9]\d{9}$/', - ], - ], [ - 'name.required' => 'Full name is required.', - 'name.min' => 'Full name must be at least 2 characters.', - 'name.max' => 'Full name must not exceed 255 characters.', - 'name.regex' => 'Full name may only contain letters, spaces, hyphens, and periods.', - 'email.required' => 'Email address is required.', - 'email.email' => 'Please provide a valid email address.', - 'email.unique' => 'This email address is already in use.', - 'phone.regex' => 'Please provide a valid Philippine mobile number (e.g., 09123456789 or +639123456789).', - ]); - - $user = Auth::user(); - $user->update([ - 'name' => $this->name, - 'email' => $this->email, - 'phone' => $this->phone, - ]); - - $this->success('Profile updated successfully!', position: 'toast-top'); - $this->dispatch('avatar-updated'); + try { + $this->validate([ + 'name' => [ + 'required', + 'string', + 'min:2', + 'max:255', + 'regex:/^[a-zA-Z\s\-\.]+$/', + ], + 'email' => [ + 'required', + 'email:rfc,dns', + 'max:255', + 'unique:users,email,' . Auth::id() . ',user_id', + ], + 'phone' => [ + 'nullable', + 'string', + 'regex:/^(\+63|0)?[9]\d{9}$/', + ], + ], [ + 'name.required' => 'Full name is required.', + 'name.min' => 'Full name must be at least 2 characters.', + 'name.max' => 'Full name must not exceed 255 characters.', + 'name.regex' => 'Full name may only contain letters, spaces, hyphens, and periods.', + 'email.required' => 'Email address is required.', + 'email.email' => 'Please provide a valid email address.', + 'email.unique' => 'This email address is already in use.', + 'phone.regex' => 'Please provide a valid Philippine mobile number (e.g., 09123456789 or +639123456789).', + ]); + + $user = Auth::user(); + $user->update([ + 'name' => $this->name, + 'email' => $this->email, + 'phone' => $this->phone, + ]); + + $this->success('Profile updated successfully!', position: 'toast-top'); + $this->dispatch('avatar-updated'); + } catch (\Illuminate\Validation\ValidationException $e) { + throw $e; + } catch (\Exception $e) { + Log::error('Failed to update profile: ' . $e->getMessage()); + $this->error('Failed to update profile. Please try again.', position: 'toast-top'); + } } public function updatePassword() { - $this->validate([ - 'current_password' => [ - 'required', - 'current_password', - ], - 'new_password' => [ - 'required', - 'confirmed', - 'different:current_password', - Password::min(8) - ->mixedCase() - ->numbers() - ->symbols() - ->uncompromised(), - ], - ], [ - 'current_password.required' => 'Current password is required.', - 'current_password.current_password' => 'The current password is incorrect.', - 'new_password.required' => 'New password is required.', - 'new_password.confirmed' => 'Password confirmation does not match.', - 'new_password.different' => 'New password must be different from current password.', - ]); - - $user = Auth::user(); - $user->update([ - 'password' => Hash::make($this->new_password), - ]); - - // Reset password fields - $this->reset(['current_password', 'new_password', 'new_password_confirmation']); - - $this->success('Password updated successfully!', position: 'toast-top'); + try { + $this->validate([ + 'current_password' => [ + 'required', + 'current_password', + ], + 'new_password' => [ + 'required', + 'confirmed', + 'different:current_password', + Password::min(8) + ->mixedCase() + ->numbers() + ->symbols() + ->uncompromised(), + ], + ], [ + 'current_password.required' => 'Current password is required.', + 'current_password.current_password' => 'The current password is incorrect.', + 'new_password.required' => 'New password is required.', + 'new_password.confirmed' => 'Password confirmation does not match.', + 'new_password.different' => 'New password must be different from current password.', + ]); + + $user = Auth::user(); + $user->update([ + 'password' => Hash::make($this->new_password), + ]); + + // Reset password fields + $this->reset(['current_password', 'new_password', 'new_password_confirmation']); + + $this->success('Password updated successfully!', position: 'toast-top'); + } catch (\Illuminate\Validation\ValidationException $e) { + throw $e; + } catch (\Exception $e) { + Log::error('Failed to update password: ' . $e->getMessage()); + $this->error('Failed to update password. Please try again.', position: 'toast-top'); + } } public function updatePreferences() diff --git a/app/Livewire/Osa/Dashboard.php b/app/Livewire/Osa/Dashboard.php index f51e18a..dd89980 100644 --- a/app/Livewire/Osa/Dashboard.php +++ b/app/Livewire/Osa/Dashboard.php @@ -4,6 +4,7 @@ use App\Models\Student_Organization; use App\Models\Ticket; +use App\Models\Transaction_Logs; use Illuminate\Support\Facades\Cache; use Livewire\Attributes\Computed; use Livewire\Attributes\Lazy; @@ -23,6 +24,9 @@ class Dashboard extends Component // Cache duration in seconds - optimized for better performance protected $cacheDuration = 300; // 5 minutes cache for better performance + // Statuses that require OSA action + protected array $osaActionStatuses = ['received', 'amended', 'pending_osa_approval']; + public function placeholder() { return view('livewire.osa.placeholders.dashboard'); @@ -41,9 +45,9 @@ public function stats(): array $currentMonth = $now->month; $currentYear = $now->year; - // Optimize: Use raw queries for faster counts + // Count tickets requiring OSA action return [ - 'pending' => Ticket::where('status', 'pending')->count(), + 'pending' => Ticket::whereIn('status', $this->osaActionStatuses)->count(), 'forwarded' => Ticket::whereHas('officeApprovals', function ($query) { $query->where('decision', 'pending'); })->count(), @@ -89,12 +93,12 @@ public function recentTickets(): array public function pendingApprovals(): array { return Cache::remember('osa_dashboard_pending_approvals', $this->cacheDuration, function () { - return Ticket::select(['ticket_id', 'ticket_number', 'title', 'created_at', 'user_id']) + return Ticket::select(['ticket_id', 'ticket_number', 'title', 'status', 'created_at', 'user_id']) ->with([ 'user' => fn($q) => $q->select(['user_id', 'org_id']) ->with('studentOrganization:org_id,org_name,logo') ]) - ->where('status', 'pending') + ->whereIn('status', $this->osaActionStatuses) ->orderBy('created_at', 'asc') ->limit(5) ->get() @@ -103,6 +107,9 @@ public function pendingApprovals(): array 'id' => $ticket->ticket_id, 'ticket_number' => $ticket->ticket_number, 'title' => $ticket->title, + 'status' => $ticket->status, + 'status_label' => $this->getStatusLabel($ticket->status), + 'status_class' => $this->getStatusClass($ticket->status), 'organization' => $ticket->user?->studentOrganization?->org_name ?? 'N/A', 'submitted' => $ticket->created_at->setTimezone('Asia/Manila')->diffForHumans(), ]; @@ -139,6 +146,52 @@ public function upcomingEvents(): array }); } + #[Computed(persist: true, seconds: 600)] + public function recentActivity(): array + { + return Cache::remember('osa_dashboard_recent_activity', $this->cacheDuration, function () { + return Transaction_Logs::with('user') + ->whereIn('action', [ + 'Ticket Approved', + 'Ticket Rejected', + 'Ticket Forwarded', + 'Ticket For Revision', + 'Ticket Status Updated', + 'New Ticket Submitted', + ]) + ->latest('created_at') + ->limit(5) + ->get() + ->map(function ($log) { + return [ + 'id' => $log->log_id, + 'action' => $log->action, + 'details' => $log->details, + 'time_ago' => $log->created_at?->diffForHumans() ?? 'Just now', + 'icon' => $this->getActivityIcon($log->action), + 'icon_class' => $this->getActivityIconClass($log->action), + ]; + }) + ->toArray(); + }); + } + + #[Computed(persist: true, seconds: 600)] + public function todaysSummary(): array + { + return Cache::remember('osa_dashboard_todays_summary', $this->cacheDuration, function () { + $today = now()->startOfDay(); + + return [ + 'newRequests' => Ticket::whereDate('created_at', $today)->count(), + 'processed' => Ticket::whereIn('status', ['approved', 'for_revision', 'gso_review']) + ->whereDate('updated_at', $today) + ->count(), + 'pending' => Ticket::whereIn('status', $this->osaActionStatuses)->count(), + ]; + }); + } + public function refreshData() { // Clear cache to force refresh @@ -146,9 +199,18 @@ public function refreshData() Cache::forget('osa_dashboard_recent_tickets'); Cache::forget('osa_dashboard_pending_approvals'); Cache::forget('osa_dashboard_upcoming_events'); + Cache::forget('osa_dashboard_recent_activity'); + Cache::forget('osa_dashboard_todays_summary'); // Unset computed properties to force re-render - unset($this->stats, $this->recentTickets, $this->pendingApprovals, $this->upcomingEvents); + unset( + $this->stats, + $this->recentTickets, + $this->pendingApprovals, + $this->upcomingEvents, + $this->recentActivity, + $this->todaysSummary + ); $this->success('Dashboard data refreshed!', position: 'toast-top', noProgress: true); } @@ -163,6 +225,8 @@ public function warmCache() $this->recentTickets; $this->pendingApprovals; $this->upcomingEvents; + $this->recentActivity; + $this->todaysSummary; $this->success('Cache warmed successfully!', position: 'toast-top'); } @@ -179,4 +243,58 @@ public function headers(): array ['key' => 'status', 'label' => 'Status'], ]; } + + /** + * Get status label for display + */ + protected function getStatusLabel(string $status): string + { + return match ($status) { + 'received' => 'Received', + 'amended' => 'Amended', + 'pending_osa_approval' => 'Final Approval', + default => ucfirst(str_replace('_', ' ', $status)), + }; + } + + /** + * Get status badge class + */ + protected function getStatusClass(string $status): string + { + return match ($status) { + 'received' => 'badge-info', + 'amended' => 'badge-warning', + 'pending_osa_approval' => 'badge-primary', + default => 'badge-ghost', + }; + } + + /** + * Get activity icon based on action + */ + protected function getActivityIcon(string $action): string + { + return match (true) { + str_contains($action, 'Approved') => 'o-check-circle', + str_contains($action, 'Rejected'), str_contains($action, 'Revision') => 'o-x-circle', + str_contains($action, 'Forwarded') => 'o-paper-airplane', + str_contains($action, 'Submitted') => 'o-document-plus', + default => 'o-information-circle', + }; + } + + /** + * Get activity icon color class + */ + protected function getActivityIconClass(string $action): string + { + return match (true) { + str_contains($action, 'Approved') => 'text-success', + str_contains($action, 'Rejected'), str_contains($action, 'Revision') => 'text-error', + str_contains($action, 'Forwarded') => 'text-info', + str_contains($action, 'Submitted') => 'text-primary', + default => 'text-gray-500', + }; + } } diff --git a/app/Livewire/Osa/Notifications.php b/app/Livewire/Osa/Notifications.php index 35fb563..9bea6be 100644 --- a/app/Livewire/Osa/Notifications.php +++ b/app/Livewire/Osa/Notifications.php @@ -2,6 +2,8 @@ namespace App\Livewire\Osa; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Log; use Livewire\Attributes\Layout; use Livewire\Attributes\On; use Livewire\Attributes\Title; @@ -123,63 +125,83 @@ public function getNotificationsProperty() public function markAsRead($notificationId) { - $user = auth()->user(); - $notification = $user->notifications()->find($notificationId); - - if ($notification) { - $notification->markAsRead(); - // Clear cache to refresh counts - \Illuminate\Support\Facades\Cache::forget("osa_notifications_counts_{$user->user_id}"); - $this->loadCounts(); + try { + $user = auth()->user(); + $notification = $user->notifications()->find($notificationId); + + if ($notification) { + $notification->markAsRead(); + // Clear cache to refresh counts + Cache::forget("osa_notifications_counts_{$user->user_id}"); + $this->loadCounts(); + } + } catch (\Exception $e) { + Log::error('Failed to mark notification as read: ' . $e->getMessage()); + $this->error('Failed to mark notification as read.', position: 'toast-top'); } } public function markAllAsRead() { - $user = auth()->user(); - $user->unreadNotifications->markAsRead(); - // Clear cache to refresh counts - \Illuminate\Support\Facades\Cache::forget("osa_notifications_counts_{$user->user_id}"); - $this->loadCounts(); + try { + $user = auth()->user(); + $user->unreadNotifications->markAsRead(); + // Clear cache to refresh counts + Cache::forget("osa_notifications_counts_{$user->user_id}"); + $this->loadCounts(); - $this->success('All notifications marked as read.', position: 'toast-top'); + $this->success('All notifications marked as read.', position: 'toast-top'); + } catch (\Exception $e) { + Log::error('Failed to mark all notifications as read: ' . $e->getMessage()); + $this->error('Failed to mark all notifications as read.', position: 'toast-top'); + } } public function deleteNotification($notificationId) { - $user = auth()->user(); - $notification = $user->notifications()->find($notificationId); + try { + $user = auth()->user(); + $notification = $user->notifications()->find($notificationId); - if ($notification) { - $notification->delete(); + if ($notification) { + $notification->delete(); - // Clear cache to refresh counts - \Illuminate\Support\Facades\Cache::forget("osa_notifications_counts_{$user->user_id}"); - $this->loadCounts(); - $this->resetPage(); + // Clear cache to refresh counts + Cache::forget("osa_notifications_counts_{$user->user_id}"); + $this->loadCounts(); + $this->resetPage(); - $this->success('Notification deleted.', position: 'toast-top'); + $this->success('Notification deleted.', position: 'toast-top'); + } + } catch (\Exception $e) { + Log::error('Failed to delete notification: ' . $e->getMessage()); + $this->error('Failed to delete notification.', position: 'toast-top'); } } public function clearAllRead() { - $user = auth()->user(); + try { + $user = auth()->user(); - // Only delete notifications that have been read - $deletedCount = $user->notifications() - ->whereNotNull('read_at') - ->delete(); + // Only delete notifications that have been read + $deletedCount = $user->notifications() + ->whereNotNull('read_at') + ->delete(); - // Clear cache to refresh counts - \Illuminate\Support\Facades\Cache::forget("osa_notifications_counts_{$user->user_id}"); - $this->loadCounts(); - $this->resetPage(); + // Clear cache to refresh counts + Cache::forget("osa_notifications_counts_{$user->user_id}"); + $this->loadCounts(); + $this->resetPage(); - if ($deletedCount > 0) { - $this->success("{$deletedCount} read notification(s) cleared.", position: 'toast-top'); - } else { - $this->info('No read notifications to clear.', position: 'toast-top'); + if ($deletedCount > 0) { + $this->success("{$deletedCount} read notification(s) cleared.", position: 'toast-top'); + } else { + $this->info('No read notifications to clear.', position: 'toast-top'); + } + } catch (\Exception $e) { + Log::error('Failed to clear read notifications: ' . $e->getMessage()); + $this->error('Failed to clear read notifications.', position: 'toast-top'); } } diff --git a/app/Livewire/Osa/Profile.php b/app/Livewire/Osa/Profile.php index 348bb21..63df2b0 100644 --- a/app/Livewire/Osa/Profile.php +++ b/app/Livewire/Osa/Profile.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Log; use Illuminate\Validation\Rules\Password; use Livewire\Attributes\Layout; use Livewire\Attributes\On; @@ -64,80 +65,94 @@ public function refreshUser(): void public function updateProfile() { - $this->validate([ - 'name' => [ - 'required', - 'string', - 'min:2', - 'max:255', - 'regex:/^[a-zA-Z\s\-\.]+$/', - ], - 'email' => [ - 'required', - 'email:rfc,dns', - 'max:255', - 'unique:users,email,' . Auth::id() . ',user_id', - ], - 'phone' => [ - 'nullable', - 'string', - 'regex:/^(\+63|0)?[9]\d{9}$/', - ], - ], [ - 'name.required' => 'Full name is required.', - 'name.min' => 'Full name must be at least 2 characters.', - 'name.max' => 'Full name must not exceed 255 characters.', - 'name.regex' => 'Full name may only contain letters, spaces, hyphens, and periods.', - 'email.required' => 'Email address is required.', - 'email.email' => 'Please provide a valid email address.', - 'email.unique' => 'This email address is already in use.', - 'phone.regex' => 'Please provide a valid Philippine mobile number (e.g., 09123456789 or +639123456789).', - ]); - - $user = Auth::user(); - $user->update([ - 'name' => $this->name, - 'email' => $this->email, - ]); - - $this->success('Profile updated successfully!', position: 'toast-top'); - $this->dispatch('avatar-updated'); + try { + $this->validate([ + 'name' => [ + 'required', + 'string', + 'min:2', + 'max:255', + 'regex:/^[a-zA-Z\s\-\.]+$/', + ], + 'email' => [ + 'required', + 'email:rfc,dns', + 'max:255', + 'unique:users,email,' . Auth::id() . ',user_id', + ], + 'phone' => [ + 'nullable', + 'string', + 'regex:/^(\+63|0)?[9]\d{9}$/', + ], + ], [ + 'name.required' => 'Full name is required.', + 'name.min' => 'Full name must be at least 2 characters.', + 'name.max' => 'Full name must not exceed 255 characters.', + 'name.regex' => 'Full name may only contain letters, spaces, hyphens, and periods.', + 'email.required' => 'Email address is required.', + 'email.email' => 'Please provide a valid email address.', + 'email.unique' => 'This email address is already in use.', + 'phone.regex' => 'Please provide a valid Philippine mobile number (e.g., 09123456789 or +639123456789).', + ]); + + $user = Auth::user(); + $user->update([ + 'name' => $this->name, + 'email' => $this->email, + ]); + + $this->success('Profile updated successfully!', position: 'toast-top'); + $this->dispatch('avatar-updated'); + } catch (\Illuminate\Validation\ValidationException $e) { + throw $e; + } catch (\Exception $e) { + Log::error('Failed to update profile: ' . $e->getMessage()); + $this->error('Failed to update profile. Please try again.', position: 'toast-top'); + } } public function updatePassword() { - $this->validate([ - 'current_password' => [ - 'required', - 'current_password', - ], - 'new_password' => [ - 'required', - 'confirmed', - 'different:current_password', - Password::min(8) - ->mixedCase() - ->numbers() - ->symbols() - ->uncompromised(), - ], - ], [ - 'current_password.required' => 'Current password is required.', - 'current_password.current_password' => 'The current password is incorrect.', - 'new_password.required' => 'New password is required.', - 'new_password.confirmed' => 'Password confirmation does not match.', - 'new_password.different' => 'New password must be different from current password.', - ]); - - $user = Auth::user(); - $user->update([ - 'password' => Hash::make($this->new_password), - ]); - - // Reset password fields - $this->reset(['current_password', 'new_password', 'new_password_confirmation']); - - $this->success('Password updated successfully!', position: 'toast-top'); + try { + $this->validate([ + 'current_password' => [ + 'required', + 'current_password', + ], + 'new_password' => [ + 'required', + 'confirmed', + 'different:current_password', + Password::min(8) + ->mixedCase() + ->numbers() + ->symbols() + ->uncompromised(), + ], + ], [ + 'current_password.required' => 'Current password is required.', + 'current_password.current_password' => 'The current password is incorrect.', + 'new_password.required' => 'New password is required.', + 'new_password.confirmed' => 'Password confirmation does not match.', + 'new_password.different' => 'New password must be different from current password.', + ]); + + $user = Auth::user(); + $user->update([ + 'password' => Hash::make($this->new_password), + ]); + + // Reset password fields + $this->reset(['current_password', 'new_password', 'new_password_confirmation']); + + $this->success('Password updated successfully!', position: 'toast-top'); + } catch (\Illuminate\Validation\ValidationException $e) { + throw $e; + } catch (\Exception $e) { + Log::error('Failed to update password: ' . $e->getMessage()); + $this->error('Failed to update password. Please try again.', position: 'toast-top'); + } } public function updatePreferences() diff --git a/app/Livewire/Osa/TicketReview/Index.php b/app/Livewire/Osa/TicketReview/Index.php index 615876b..74c2f52 100644 --- a/app/Livewire/Osa/TicketReview/Index.php +++ b/app/Livewire/Osa/TicketReview/Index.php @@ -2,7 +2,9 @@ namespace App\Livewire\Osa\TicketReview; +use App\Models\Student_Organization; use App\Models\Ticket; +use Illuminate\Support\Facades\Cache; use Livewire\Attributes\Computed; use Livewire\Attributes\Layout; use Livewire\Attributes\Title; @@ -23,9 +25,12 @@ class Index extends Component #[Url(except: '')] public $statusFilter = ''; + #[Url(except: '')] + public $organizationFilter = ''; + public function clearFilters() { - $this->reset(['search', 'statusFilter']); + $this->reset(['search', 'statusFilter', 'organizationFilter']); $this->resetPage(); } @@ -39,6 +44,22 @@ public function updatedStatusFilter() $this->resetPage(); } + public function updatedOrganizationFilter() + { + $this->resetPage(); + } + + #[Computed(persist: true, seconds: 3600)] + public function organizations() + { + return Cache::remember('osa_ticket_review_organizations', 3600, function () { + return Student_Organization::withTrashed() + ->select(['org_id', 'org_name', 'deleted_at']) + ->orderBy('org_name') + ->get(); + }); + } + public function render() { // Use the computed property @@ -64,16 +85,16 @@ public function tickets() 'created_at', ]) ->with([ - 'user' => fn ($q) => $q->select(['user_id', 'org_id']) + 'user' => fn($q) => $q->select(['user_id', 'org_id']) ->with('studentOrganization:org_id,org_name'), - 'events' => fn ($q) => $q->select(['event_id', 'ticket_id']) + 'events' => fn($q) => $q->select(['event_id', 'ticket_id']) ->with('eventSchedules:schedule_id,event_id,start_date,start_time,venue'), 'attachments:attachment_id,ticket_id,file_path,file_name', 'eventType:event_type_id,type_name', ]) ->when($this->search, function ($query) { // Optimize search - use index-friendly queries - $searchTerm = '%'.$this->search.'%'; + $searchTerm = '%' . $this->search . '%'; $query->where(function ($q) use ($searchTerm) { $q->where('title', 'like', $searchTerm) ->orWhere('ticket_number', 'like', $searchTerm); @@ -87,6 +108,9 @@ public function tickets() // When a specific filter is selected, show only those tickets $query->where('status', $this->statusFilter); }) + ->when($this->organizationFilter, fn($query) => $query->whereHas('user', function ($q) { + $q->where('org_id', $this->organizationFilter); + })) ->orderBy('created_at', 'desc') ->paginate(10); // Reduced from 12 to 10 for faster loads } diff --git a/app/Livewire/StudentOrg/EditTicket.php b/app/Livewire/StudentOrg/EditTicket.php index be09f7f..5c93925 100644 --- a/app/Livewire/StudentOrg/EditTicket.php +++ b/app/Livewire/StudentOrg/EditTicket.php @@ -147,7 +147,6 @@ public function loadTicket($ticketId) } $this->populateFormFields(); - } catch (Exception $e) { Log::error('Failed to load ticket for editing', [ 'ticket_id' => $ticketId, @@ -214,6 +213,14 @@ public function getExpectedParticipantsProperty() public function getRequiredDocuments() { + // Try to get from database first + $eventType = Event_Type::find($this->eventType); + if ($eventType && $eventType->documentary_requirements) { + // Return as HTML string for rich text display + return $eventType->documentary_requirements; + } + + // Fallback to config return config("event_requirements.documents.{$this->eventType}", [ 'Pick an event type to see needed attachments.' ]); @@ -427,7 +434,6 @@ public function updateTicket() // Close drawer and refresh parent $this->dispatch('close-edit-drawer'); $this->dispatch('ticket-updated')->to(MyTicket::class); - } catch (Exception $e) { DB::rollBack(); diff --git a/app/Livewire/StudentOrg/Notifications.php b/app/Livewire/StudentOrg/Notifications.php index 88c5dbc..a81d1b6 100644 --- a/app/Livewire/StudentOrg/Notifications.php +++ b/app/Livewire/StudentOrg/Notifications.php @@ -3,6 +3,7 @@ namespace App\Livewire\StudentOrg; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Log; use Livewire\Attributes\Layout; use Livewire\Attributes\On; use Livewire\Attributes\Title; @@ -117,63 +118,83 @@ public function getNotificationsProperty() public function markAsRead($notificationId) { - $user = auth()->user(); - $notification = $user->notifications()->find($notificationId); - - if ($notification) { - $notification->markAsRead(); - // Clear cache to refresh counts - Cache::forget("student_org_notifications_counts_{$user->user_id}"); - $this->loadCounts(); + try { + $user = auth()->user(); + $notification = $user->notifications()->find($notificationId); + + if ($notification) { + $notification->markAsRead(); + // Clear cache to refresh counts + Cache::forget("student_org_notifications_counts_{$user->user_id}"); + $this->loadCounts(); + } + } catch (\Exception $e) { + Log::error('Failed to mark notification as read: ' . $e->getMessage()); + $this->error('Failed to mark notification as read.', position: 'toast-top'); } } public function markAllAsRead() { - $user = auth()->user(); - $user->unreadNotifications->markAsRead(); - // Clear cache to refresh counts - Cache::forget("student_org_notifications_counts_{$user->user_id}"); - $this->loadCounts(); + try { + $user = auth()->user(); + $user->unreadNotifications->markAsRead(); + // Clear cache to refresh counts + Cache::forget("student_org_notifications_counts_{$user->user_id}"); + $this->loadCounts(); - $this->success('All notifications marked as read.', position: 'toast-top'); + $this->success('All notifications marked as read.', position: 'toast-top'); + } catch (\Exception $e) { + Log::error('Failed to mark all notifications as read: ' . $e->getMessage()); + $this->error('Failed to mark all notifications as read.', position: 'toast-top'); + } } public function deleteNotification($notificationId) { - $user = auth()->user(); - $notification = $user->notifications()->find($notificationId); + try { + $user = auth()->user(); + $notification = $user->notifications()->find($notificationId); - if ($notification) { - $notification->delete(); + if ($notification) { + $notification->delete(); - // Clear cache to refresh counts - Cache::forget("student_org_notifications_counts_{$user->user_id}"); - $this->loadCounts(); - $this->resetPage(); + // Clear cache to refresh counts + Cache::forget("student_org_notifications_counts_{$user->user_id}"); + $this->loadCounts(); + $this->resetPage(); - $this->success('Notification deleted.', position: 'toast-top'); + $this->success('Notification deleted.', position: 'toast-top'); + } + } catch (\Exception $e) { + Log::error('Failed to delete notification: ' . $e->getMessage()); + $this->error('Failed to delete notification.', position: 'toast-top'); } } public function clearAllRead() { - $user = auth()->user(); + try { + $user = auth()->user(); - // Only delete notifications that have been read - $deletedCount = $user->notifications() - ->whereNotNull('read_at') - ->delete(); + // Only delete notifications that have been read + $deletedCount = $user->notifications() + ->whereNotNull('read_at') + ->delete(); - // Clear cache to refresh counts - Cache::forget("student_org_notifications_counts_{$user->user_id}"); - $this->loadCounts(); - $this->resetPage(); + // Clear cache to refresh counts + Cache::forget("student_org_notifications_counts_{$user->user_id}"); + $this->loadCounts(); + $this->resetPage(); - if ($deletedCount > 0) { - $this->success("{$deletedCount} read notification(s) cleared.", position: 'toast-top'); - } else { - $this->info('No read notifications to clear.', position: 'toast-top'); + if ($deletedCount > 0) { + $this->success("{$deletedCount} read notification(s) cleared.", position: 'toast-top'); + } else { + $this->info('No read notifications to clear.', position: 'toast-top'); + } + } catch (\Exception $e) { + Log::error('Failed to clear read notifications: ' . $e->getMessage()); + $this->error('Failed to clear read notifications.', position: 'toast-top'); } } diff --git a/app/Livewire/StudentOrg/Profile.php b/app/Livewire/StudentOrg/Profile.php index 8e3740f..1e2a8a8 100644 --- a/app/Livewire/StudentOrg/Profile.php +++ b/app/Livewire/StudentOrg/Profile.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Log; use Illuminate\Validation\Rules\Password; use Livewire\Attributes\Layout; use Livewire\Attributes\On; @@ -57,81 +58,95 @@ public function refreshUser(): void public function updateProfile() { - $this->validate([ - 'name' => [ - 'required', - 'string', - 'min:2', - 'max:255', - 'regex:/^[a-zA-Z\s\-\.]+$/', - ], - 'email' => [ - 'required', - 'email:rfc,dns', - 'max:255', - 'unique:users,email,' . Auth::id() . ',user_id', - ], - 'phone' => [ - 'nullable', - 'string', - 'regex:/^(\+63|0)?[9]\d{9}$/', - ], - ], [ - 'name.required' => 'Full name is required.', - 'name.min' => 'Full name must be at least 2 characters.', - 'name.max' => 'Full name must not exceed 255 characters.', - 'name.regex' => 'Full name may only contain letters, spaces, hyphens, and periods.', - 'email.required' => 'Email address is required.', - 'email.email' => 'Please provide a valid email address.', - 'email.unique' => 'This email address is already in use.', - 'phone.regex' => 'Please provide a valid Philippine mobile number (e.g., 09123456789 or +639123456789).', - ]); - - $user = Auth::user(); - $user->update([ - 'name' => $this->name, - 'email' => $this->email, - 'phone' => $this->phone, - ]); - - $this->success('Profile updated successfully!', position: 'toast-top'); - $this->dispatch('avatar-updated'); + try { + $this->validate([ + 'name' => [ + 'required', + 'string', + 'min:2', + 'max:255', + 'regex:/^[a-zA-Z\s\-\.]+$/', + ], + 'email' => [ + 'required', + 'email:rfc,dns', + 'max:255', + 'unique:users,email,' . Auth::id() . ',user_id', + ], + 'phone' => [ + 'nullable', + 'string', + 'regex:/^(\+63|0)?[9]\d{9}$/', + ], + ], [ + 'name.required' => 'Full name is required.', + 'name.min' => 'Full name must be at least 2 characters.', + 'name.max' => 'Full name must not exceed 255 characters.', + 'name.regex' => 'Full name may only contain letters, spaces, hyphens, and periods.', + 'email.required' => 'Email address is required.', + 'email.email' => 'Please provide a valid email address.', + 'email.unique' => 'This email address is already in use.', + 'phone.regex' => 'Please provide a valid Philippine mobile number (e.g., 09123456789 or +639123456789).', + ]); + + $user = Auth::user(); + $user->update([ + 'name' => $this->name, + 'email' => $this->email, + 'phone' => $this->phone, + ]); + + $this->success('Profile updated successfully!', position: 'toast-top'); + $this->dispatch('avatar-updated'); + } catch (\Illuminate\Validation\ValidationException $e) { + throw $e; + } catch (\Exception $e) { + Log::error('Failed to update profile: ' . $e->getMessage()); + $this->error('Failed to update profile. Please try again.', position: 'toast-top'); + } } public function updatePassword() { - $this->validate([ - 'current_password' => [ - 'required', - 'current_password', - ], - 'new_password' => [ - 'required', - 'confirmed', - 'different:current_password', - Password::min(8) - ->mixedCase() - ->numbers() - ->symbols() - ->uncompromised(), - ], - ], [ - 'current_password.required' => 'Current password is required.', - 'current_password.current_password' => 'The current password is incorrect.', - 'new_password.required' => 'New password is required.', - 'new_password.confirmed' => 'Password confirmation does not match.', - 'new_password.different' => 'New password must be different from current password.', - ]); - - $user = Auth::user(); - $user->update([ - 'password' => Hash::make($this->new_password), - ]); - - // Reset password fields - $this->reset(['current_password', 'new_password', 'new_password_confirmation']); - - $this->success('Password updated successfully!', position: 'toast-top'); + try { + $this->validate([ + 'current_password' => [ + 'required', + 'current_password', + ], + 'new_password' => [ + 'required', + 'confirmed', + 'different:current_password', + Password::min(8) + ->mixedCase() + ->numbers() + ->symbols() + ->uncompromised(), + ], + ], [ + 'current_password.required' => 'Current password is required.', + 'current_password.current_password' => 'The current password is incorrect.', + 'new_password.required' => 'New password is required.', + 'new_password.confirmed' => 'Password confirmation does not match.', + 'new_password.different' => 'New password must be different from current password.', + ]); + + $user = Auth::user(); + $user->update([ + 'password' => Hash::make($this->new_password), + ]); + + // Reset password fields + $this->reset(['current_password', 'new_password', 'new_password_confirmation']); + + $this->success('Password updated successfully!', position: 'toast-top'); + } catch (\Illuminate\Validation\ValidationException $e) { + throw $e; + } catch (\Exception $e) { + Log::error('Failed to update password: ' . $e->getMessage()); + $this->error('Failed to update password. Please try again.', position: 'toast-top'); + } } public function updatePreferences() diff --git a/app/Livewire/StudentOrg/SubmitTicket.php b/app/Livewire/StudentOrg/SubmitTicket.php index 9b189b0..f91e8a8 100644 --- a/app/Livewire/StudentOrg/SubmitTicket.php +++ b/app/Livewire/StudentOrg/SubmitTicket.php @@ -47,7 +47,9 @@ class SubmitTicket extends Component public $proponent_contact = ''; - #[Validate('required|numeric|digits:11|regex:/^[0-9\s\-\+\(\)]+$/')] + #[Validate('required', message: 'The adviser contact number is required.')] + #[Validate('numeric', message: 'The adviser contact number must contain only numbers.')] + #[Validate('digits:11', message: 'The adviser contact number must be 11 digits.')] public $adviser_contact = '09'; #[Validate('required|boolean')] @@ -76,16 +78,25 @@ class SubmitTicket extends Component public $expectedNonPLVParticipants = 0; // Step 3: Schedule & Venue - #[Validate('required|date|after_or_equal:today')] + #[Validate('required', message: 'The event start date is required.')] + #[Validate('date', message: 'The event start date must be a valid date.')] + #[Validate('after_or_equal:today', message: 'The event start date must be today or later.')] public $eventStartDate = ''; - #[Validate('required|date|after_or_equal:eventStartDate')] + #[Validate('required', message: 'The event end date is required.')] + #[Validate('date', message: 'The event end date must be a valid date.')] + #[Validate('after_or_equal:eventStartDate', message: 'The event end date must be on or after the start date.')] public $eventEndDate = ''; - #[Validate('required|date_format:H:i')] + #[Validate('required', message: 'The event start time is required.')] + #[Validate('date_format:H:i', message: 'The event start time must be in HH:MM format.')] + #[Validate('after_or_equal:00:01', message: 'Event start time must be at or after 12:01 AM.')] public $eventStartTime = ''; - #[Validate('required|date_format:H:i|after:eventStartTime')] + #[Validate('required', message: 'The event end time is required.')] + #[Validate('date_format:H:i', message: 'The event end time must be in HH:MM format.')] + #[Validate('after:eventStartTime', message: 'The end time must be after the start time.')] + #[Validate('before_or_equal:21:00', message: 'Event end time must be at or before 9:00 PM.')] public $eventEndTime = ''; public $venues = []; @@ -241,8 +252,8 @@ protected function getCurrentStepRules(): array 3 => [ 'eventStartDate' => 'required|date|after_or_equal:today', 'eventEndDate' => 'required|date|after_or_equal:eventStartDate', - 'eventStartTime' => ['required', 'date_format:H:i'], - 'eventEndTime' => ['required', 'date_format:H:i', 'after:eventStartTime'], + 'eventStartTime' => 'required|date_format:H:i|after_or_equal:00:01', + 'eventEndTime' => 'required|date_format:H:i|after:eventStartTime|before_or_equal:21:00', 'preferredVenue' => 'required|integer|exists:venues,venue_id', 'preferredVenueOther' => $this->isOthersVenue($this->preferredVenue) ? 'required|string|max:255|min:3' : 'nullable', 'alternativeVenue' => 'nullable|integer|exists:venues,venue_id', @@ -293,20 +304,6 @@ public function updated($property) return; } - // Real-time validation for adviser_contact - if ($property === 'adviser_contact') { - try { - $this->validateOnly('adviser_contact', [ - 'adviser_contact' => 'required|string|digits:11|regex:/^[0-9]+$/', - ], [ - 'adviser_contact.digits' => 'The adviser contact number must be 11 digits.', - 'adviser_contact.regex' => 'The adviser contact number must contain only numbers.', - ]); - } catch (ValidationException $e) { - // Validation error handled by Livewire - } - } - // Exclude certain properties from auto-save if (in_array($property, ['newAttachments', 'isProcessing'])) { return; @@ -334,32 +331,35 @@ protected function validateCurrentStep() 'expectedNonPLVParticipants.min' => 'The number of non-PLV participants must be at least 0.', 'expectedPLVParticipants.max' => 'The number of PLV participants must be less than 100000.', 'expectedNonPLVParticipants.max' => 'The number of non-PLV participants must be less than 100000.', + 'eventStartTime.after_or_equal' => 'Event start time must be at or after 12:01 AM.', + 'eventEndTime.before_or_equal' => 'Event end time must be at or before 9:00 PM.', + 'eventEndTime.after' => 'The end time must be after the start time.', ]); } // Custom time range validation (runs for step 3 or when validating all steps) - if (($this->currentStep === 3 || $this->currentStep === $this->totalSteps) && $this->eventStartTime && $this->eventEndTime) { - try { - $startTime = \Carbon\Carbon::createFromFormat('H:i', $this->eventStartTime); - $endTime = \Carbon\Carbon::createFromFormat('H:i', $this->eventEndTime); - $minTime = \Carbon\Carbon::createFromFormat('H:i', '00:01'); - $maxTime = \Carbon\Carbon::createFromFormat('H:i', '21:00'); - - if ($startTime->lt($minTime)) { - throw \Illuminate\Validation\ValidationException::withMessages([ - 'eventStartTime' => 'Event start time must be at or after 12:01 AM.', - ]); - } - - if ($endTime->gt($maxTime)) { - throw \Illuminate\Validation\ValidationException::withMessages([ - 'eventEndTime' => 'Event end time must be at or before 9:00 PM.', - ]); - } - } catch (\Carbon\Exceptions\InvalidFormatException $e) { - // Invalid time format - let the regular validation handle it - } - } + // if (($this->currentStep === 3 || $this->currentStep === $this->totalSteps) && $this->eventStartTime && $this->eventEndTime) { + // try { + // $startTime = \Carbon\Carbon::createFromFormat('H:i', $this->eventStartTime); + // $endTime = \Carbon\Carbon::createFromFormat('H:i', $this->eventEndTime); + // $minTime = \Carbon\Carbon::createFromFormat('H:i', '00:01'); + // $maxTime = \Carbon\Carbon::createFromFormat('H:i', '21:00'); + + // if ($startTime->lt($minTime)) { + // throw \Illuminate\Validation\ValidationException::withMessages([ + // 'eventStartTime' => 'Event start time must be at or after 12:01 AM.', + // ]); + // } + + // if ($endTime->gt($maxTime)) { + // throw \Illuminate\Validation\ValidationException::withMessages([ + // 'eventEndTime' => 'Event end time must be at or before 9:00 PM.', + // ]); + // } + // } catch (\Carbon\Exceptions\InvalidFormatException $e) { + // // Invalid time format - let the regular validation handle it + // } + // } } protected function validateAllSteps() @@ -455,7 +455,7 @@ public function mount() $this->adviser = $currentUserinfo->adviser_name ?? ''; $this->contactEmail = $currentUser->email ?? ''; $this->proponentName = $currentUser->name ?? ''; - $this->organizationCourse = $currentUserinfo->course->course_name ?? ''; + $this->organizationCourse = $currentUserinfo->course->course_name ?? 'Non Academic Org'; $this->proponentPosition = $currentUserPosition->position_name ?? ''; $this->proponent_contact = $currentUser->phone ?? ''; $this->venues = \App\Models\Venue::where('is_active', true)->get(); @@ -787,6 +787,14 @@ public function getIsCurrentStepCompleteProperty(): bool public function getRequiredDocuments() { + // Try to get from database first + $eventType = Event_Type::find($this->eventType); + if ($eventType && $eventType->documentary_requirements) { + // Return as HTML string for rich text display + return $eventType->documentary_requirements; + } + + // Fallback to config return config("event_requirements.documents.{$this->eventType}", ['Pick an event type to see needed attachments.']); } diff --git a/app/Livewire/Superadmin/Archive/Index.php b/app/Livewire/Superadmin/Archive/Index.php index 14dbdab..071eb4a 100644 --- a/app/Livewire/Superadmin/Archive/Index.php +++ b/app/Livewire/Superadmin/Archive/Index.php @@ -87,12 +87,13 @@ public function archivedItems() ->whereBetween('created_at', [$this->dateFrom, $this->dateTo]) ->get() ->map(function ($ticket) { + $orgDeleted = $ticket->user?->studentOrganization?->trashed(); return [ 'id' => $ticket->ticket_id, 'type' => 'ticket', 'title' => $ticket->title, 'identifier' => $ticket->ticket_number, - 'organization' => $ticket->user->studentOrganization->org_name ?? 'N/A', + 'organization' => $orgDeleted ? 'Deleted Organization' : ($ticket->user?->studentOrganization?->org_name ?? 'N/A'), 'status' => $ticket->status, 'date' => $ticket->created_at, 'archived_at' => $ticket->updated_at, diff --git a/app/Livewire/Superadmin/Calendar/Index.php b/app/Livewire/Superadmin/Calendar/Index.php index 49d345a..636eb2e 100644 --- a/app/Livewire/Superadmin/Calendar/Index.php +++ b/app/Livewire/Superadmin/Calendar/Index.php @@ -20,465 +20,472 @@ class Index extends ComponentsEventCalendar #[Title('Event Calendar - SuperAdmin')] #[Layout('components.layouts.superadmin')] - // SuperAdmin can view all statuses - // public $statusFilter = 'all'; - - // Additional SuperAdmin controls - public $selectedEventForAction = null; - - public $showActionModal = false; - - public $actionType = ''; // cancel, reschedule, delete - - // Override to include all events (not just approved) - // public function mount() - // { - // parent::mount(); - // $this->statusFilter = 'all'; // SuperAdmin sees all - // } - protected function getRoleSpecificData(): array { - return [ - 'canEdit' => true, - 'canDelete' => true, - 'canForceApprove' => true, - ]; - } - - public function openActionModal($eventId, $action) - { - $this->selectedEventForAction = Event::with(['ticket', 'eventSchedules'])->find($eventId); - - if (! $this->selectedEventForAction) { - $this->error('Event not found!', position: 'toast-top'); - - return; - } - - $this->actionType = $action; - $this->showActionModal = true; - } - - public function closeActionModal() - { - $this->showActionModal = false; - $this->selectedEventForAction = null; - $this->actionType = ''; - } - - public function forceApproveEvent($eventId) - { - try { - $event = Event::with('ticket')->find($eventId); - - if (! $event) { - $this->error('Event not found!', position: 'toast-top'); - - return; - } - - // Update ticket status - if ($event->ticket) { - $event->ticket->update(['status' => 'approved']); - } - - // Update event schedules - Event_Schedule::where('event_id', $eventId)->update(['status' => 'approved']); - - // Log action - TransactionLogService::log( - 'APPROVE', - "SuperAdmin force-approved event: {$event->ticket->title}", - Auth::user()->user_id - ); - - $this->success('Event force-approved successfully!', position: 'toast-top'); - $this->dispatch('calendar-refetch'); - $this->closeActionModal(); - } catch (\Exception $e) { - $this->error('Failed to approve event: ' . $e->getMessage(), position: 'toast-top'); - } - } - - public function cancelEvent($eventId) - { - try { - $event = Event::with('ticket')->find($eventId); - - if (! $event) { - $this->error('Event not found!', position: 'toast-top'); - - return; - } - - // Update ticket status - if ($event->ticket) { - $event->ticket->update(['status' => 'cancelled']); - } - - // Update event schedules - Event_Schedule::where('event_id', $eventId)->update(['status' => 'cancelled']); - - // Log action - TransactionLogService::log( - 'CANCEL', - "SuperAdmin cancelled event: {$event->ticket->title}", - Auth::user()->user_id - ); - - $this->success('Event cancelled successfully!', position: 'toast-top'); - $this->dispatch('calendar-refetch'); - $this->closeActionModal(); - } catch (\Exception $e) { - $this->error('Failed to cancel event: ' . $e->getMessage(), position: 'toast-top'); - } - } - - public function forceDeleteEvent($eventId) - { - try { - $event = Event::with('ticket')->find($eventId); - - if (! $event) { - $this->error('Event not found!', position: 'toast-top'); - - return; - } - - $eventTitle = $event->ticket->title ?? 'Unknown Event'; - - // Delete event schedules - Event_Schedule::where('event_id', $eventId)->delete(); - - // Delete ticket - if ($event->ticket) { - $event->ticket->delete(); - } - - // Delete event - $event->delete(); - - // Log action - TransactionLogService::log( - 'DELETE', - "SuperAdmin force-deleted event: {$eventTitle}", - Auth::user()->user_id - ); - - $this->success('Event deleted successfully!', position: 'toast-top'); - $this->dispatch('calendar-refetch'); - $this->closeActionModal(); - } catch (\Exception $e) { - $this->error('Failed to delete event: ' . $e->getMessage(), position: 'toast-top'); - } - } - - // Override the eventsForCalendar computed property to show all statuses for SuperAdmin - #[Computed] - public function eventsForCalendar() - { - // If statusFilter is 'all', don't filter by status - if ($this->statusFilter === 'all') { - $eventSchedules = Event_Schedule::select(['schedule_id', 'event_id', 'start_date', 'end_date', 'start_time', 'end_time', 'venue', 'status']) - ->with([ - 'event' => fn($q) => $q->select(['event_id', 'ticket_id', 'event__type_id']) - ->with([ - 'ticket' => fn($q) => $q->select(['ticket_id', 'title', 'description', 'venue_requested', 'user_id', 'status', 'ticket_number']) - ->with([ - 'user' => fn($q) => $q->select(['user_id', 'org_id']) - ->with('studentOrganization:org_id,org_name,logo'), - ]), - 'eventType:event_type_id,type_name', - ]), - ]) - // Only show approved event schedules - ->where('status', 'approved') - // Apply organization filter if set - ->when($this->organizationFilter, fn($query) => $query->whereHas('event.ticket.user', fn($q) => $q->where('org_id', $this->organizationFilter))) - // Apply event type filter if set - ->when($this->eventTypeFilter, fn($query) => $query->whereHas('event', fn($q) => $q->where('event__type_id', $this->eventTypeFilter))) - ->get(); - } else { - // Use parent method for specific status filtering - return parent::eventsForCalendar(); - } - - // Process events (similar logic to parent) - $allEvents = []; - - foreach ($eventSchedules as $schedule) { - $event = $schedule->event; - - $rawStartTime = $schedule->getRawOriginal('start_time'); - $rawEndTime = $schedule->getRawOriginal('end_time'); - - $startTime = $rawStartTime ?: '09:00:00'; - $endTime = $rawEndTime ?: '17:00:00'; - - if (strlen($startTime) > 8) { - $startTime = substr($startTime, 0, 8); - } - if (strlen($endTime) > 8) { - $endTime = substr($endTime, 0, 8); - } - - $startDate = $schedule->start_date->format('Y-m-d'); - $endDate = $schedule->end_date ? $schedule->end_date->format('Y-m-d') : $startDate; - - if ($startDate === $endDate) { - $startISO = $startDate . 'T' . $startTime . '+08:00'; - $endISO = $endDate . 'T' . $endTime . '+08:00'; - - $allEvents[] = [ - 'id' => $event->event_id, - 'title' => $event->ticket->title, - 'start' => $startISO, - 'end' => $endISO, - 'allDay' => false, - 'backgroundColor' => $this->getEventColor($event), - 'borderColor' => $this->getEventColor($event), - 'textColor' => '#ffffff', - 'extendedProps' => [ - 'organization' => $event->ticket->user->studentOrganization->org_name ?? 'No Organization', - 'eventType' => $event->eventType?->type_name ?? 'N/A', - 'venue' => $schedule->venue ?? $event->ticket->venue_requested ?? 'TBD', - 'description' => $event->ticket->description, - 'ticketNumber' => $event->ticket->ticket_number, - 'status' => $event->ticket->status ?? 'pending', - 'rawStartTime' => $startTime, - 'rawEndTime' => $endTime, - 'rawStartDate' => $startDate, - 'rawEndDate' => $endDate, - ], - ]; - } else { - // Multi-day event logic (same as parent) - // Calculate days of week from start_date to end_date - $startCarbon = \Carbon\Carbon::parse($startDate); - $endCarbon = \Carbon\Carbon::parse($endDate); - $daysOfWeek = []; - - $current = $startCarbon->copy(); - while ($current->lte($endCarbon)) { - $dayOfWeek = $current->dayOfWeek; - if (! in_array($dayOfWeek, $daysOfWeek)) { - $daysOfWeek[] = $dayOfWeek; - } - $current->addDay(); - } - - if (count($daysOfWeek) >= 5) { - $daysOfWeek = [0, 1, 2, 3, 4, 5, 6]; - } - - $endRecurDate = \Carbon\Carbon::parse($endDate)->addDay()->format('Y-m-d'); - - $commonProps = [ - 'title' => $event->ticket->title, - 'backgroundColor' => $this->getEventColor($event), - 'borderColor' => $this->getEventColor($event), - 'textColor' => '#ffffff', - 'extendedProps' => [ - 'organization' => $event->ticket->user->studentOrganization->org_name ?? 'No Organization', - 'eventType' => $event->eventType?->type_name ?? 'N/A', - 'venue' => $schedule->venue ?? $event->ticket->venue_requested ?? 'TBD', - 'description' => $event->ticket->description, - 'ticketNumber' => $event->ticket->ticket_number, - 'status' => $event->ticket->status ?? 'pending', - 'rawStartTime' => $startTime, - 'rawEndTime' => $endTime, - 'rawStartDate' => $startDate, - 'rawEndDate' => $endDate, - ], - ]; - - // Spanning event for month view - $allEvents[] = array_merge($commonProps, [ - 'id' => $event->event_id . '_span', - 'groupId' => $event->event_id, - 'start' => $startDate . 'T' . $startTime . '+08:00', - 'end' => $endDate . 'T' . $endTime . '+08:00', - 'allDay' => false, - 'display' => 'block', - 'extendedProps' => array_merge($commonProps['extendedProps'], [ - 'forMonthView' => true, - ]), - ]); - - // Recurring event for week/day views - $allEvents[] = array_merge($commonProps, [ - 'id' => $event->event_id . '_recur', - 'groupId' => $event->event_id, - 'startTime' => $startTime, - 'endTime' => $endTime, - 'startRecur' => $startDate, - 'endRecur' => $endRecurDate, - 'daysOfWeek' => $daysOfWeek, - 'allDay' => false, - 'display' => 'block', - 'extendedProps' => array_merge($commonProps['extendedProps'], [ - 'forTimeView' => true, - ]), - ]); - } - } - - return $allEvents; - } - - // Override uniqueEventsCount to handle 'all' status for SuperAdmin - #[Computed] - public function uniqueEventsCount() - { - // Count unique events that match the current filters - $query = Event_Schedule::query() - ->where('status', 'approved'); - - // For SuperAdmin: If statusFilter is 'all', don't filter by ticket status - if ($this->statusFilter !== 'all') { - $query->whereHas('event.ticket', fn($q) => $q->where('status', $this->statusFilter)); - } - - // Apply organization filter if set - $query->when($this->organizationFilter, fn($query) => $query->whereHas('event.ticket.user', fn($q) => $q->where('org_id', $this->organizationFilter))); - - // Apply event type filter if set - $query->when($this->eventTypeFilter, fn($query) => $query->whereHas('event', fn($q) => $q->where('event__type_id', $this->eventTypeFilter))); - - // Count distinct event_ids to get unique events (MySQL compatible) - return (int) $query->selectRaw('COUNT(DISTINCT event_id) as count')->value('count'); - } - - // Override upcomingEventsThisMonth to handle 'all' status for SuperAdmin - #[Computed] - public function upcomingEventsThisMonth() - { - $startOfMonth = $this->currentDate->copy()->startOfMonth(); - $endOfMonth = $this->currentDate->copy()->endOfMonth(); - - $query = Event_Schedule::select(['schedule_id', 'event_id', 'start_date', 'end_date', 'start_time', 'end_time', 'venue', 'status']) - ->with([ - 'event' => fn($q) => $q->select(['event_id', 'ticket_id', 'event__type_id']) - ->with([ - 'ticket' => fn($q) => $q->select(['ticket_id', 'title', 'description', 'venue_requested', 'user_id', 'status', 'ticket_number']) - ->with([ - 'user' => fn($q) => $q->select(['user_id', 'org_id']) - ->with('studentOrganization:org_id,org_name'), - ]), - 'eventType:event_type_id,type_name', - ]), - ]) - ->where('status', 'approved') - ->whereBetween('start_date', [$startOfMonth, $endOfMonth]); - - // For SuperAdmin: If statusFilter is 'all', show all ticket statuses - if ($this->statusFilter !== 'all') { - $query->whereHas('event.ticket', fn($q) => $q->where('status', $this->statusFilter)); - } - - $eventSchedules = $query->orderBy('start_date') - ->orderBy('start_time') - ->get(); - - return $eventSchedules->map(function ($schedule) { - $event = $schedule->event; - $rawStartTime = $schedule->getRawOriginal('start_time'); - $rawEndTime = $schedule->getRawOriginal('end_time'); - - // Format date - $startDate = \Carbon\Carbon::parse($schedule->start_date); - $endDate = $schedule->end_date ? \Carbon\Carbon::parse($schedule->end_date) : $startDate; - - // Format time - $formatTime = function ($timeStr) { - if (! $timeStr) { - return ''; - } - try { - $parts = explode(':', $timeStr); - $hours = (int) $parts[0]; - $minutes = (int) ($parts[1] ?? 0); - $period = $hours >= 12 ? 'PM' : 'AM'; - $hour12 = $hours % 12 ?: 12; - - return sprintf('%d:%02d %s', $hour12, $minutes, $period); - } catch (\Exception $e) { - return $timeStr; - } - }; - - $startTimeFormatted = $formatTime($rawStartTime); - $endTimeFormatted = $formatTime($rawEndTime); - $timeRange = $startTimeFormatted && $endTimeFormatted - ? "{$startTimeFormatted} - {$endTimeFormatted}" - : ($startTimeFormatted ?: 'TBD'); - - // Date range formatting - $dateDisplay = $startDate->format('M d, Y'); - if ($startDate->format('Y-m-d') !== $endDate->format('Y-m-d')) { - $dateDisplay = $startDate->format('M d') . ' - ' . $endDate->format('M d, Y'); - } - - // Get event color using the same method as calendar - $hexColor = $this->getEventColor($event); - $colorName = $this->hexToTailwindColor($hexColor); - - return [ - 'title' => $event->ticket->title ?? 'Untitled Event', - 'description' => $event->ticket->description ?? null, - 'organization' => $event->ticket->user->studentOrganization->org_name ?? 'No Organization', - 'organizationLogo' => $event->ticket->user->studentOrganization->logo ?? asset('images/default-org-logo.svg'), - 'eventType' => $event->eventType?->type_name ?? 'N/A', - 'date' => $dateDisplay, - 'time' => $timeRange, - 'datetime' => "{$dateDisplay} • {$timeRange}", - 'venue' => $schedule->venue ?? $event->ticket->venue_requested ?? 'TBD', - 'color' => $colorName, - 'hexColor' => $hexColor, - 'icon' => $this->getEventTypeIcon($event->eventType?->type_name ?? ''), - 'start_date' => $schedule->start_date, - 'event_id' => $event->event_id, - ]; - })->toArray(); + return []; } +} - // Helper method from parent (needed for upcomingEventsThisMonth) - private function hexToTailwindColor($hexColor): string - { - $colorMap = [ - '#10b981' => 'green', - '#3b82f6' => 'blue', - '#8b5cf6' => 'purple', - '#f59e0b' => 'yellow', - '#ef4444' => 'red', - '#06b6d4' => 'cyan', - '#84cc16' => 'lime', - '#f97316' => 'orange', - ]; - - return $colorMap[$hexColor] ?? 'blue'; - } +// // SuperAdmin can view all statuses +// // public $statusFilter = 'all'; + +// // Additional SuperAdmin controls +// public $selectedEventForAction = null; + +// public $showActionModal = false; + +// public $actionType = ''; // cancel, reschedule, delete + +// // Override to include all events (not just approved) +// // public function mount() +// // { +// // parent::mount(); +// // $this->statusFilter = 'all'; // SuperAdmin sees all +// // } + +// protected function getRoleSpecificData(): array +// { +// return [ +// 'canEdit' => true, +// 'canDelete' => true, +// 'canForceApprove' => true, +// ]; +// } + +// public function openActionModal($eventId, $action) +// { +// $this->selectedEventForAction = Event::with(['ticket', 'eventSchedules'])->find($eventId); + +// if (! $this->selectedEventForAction) { +// $this->error('Event not found!', position: 'toast-top'); + +// return; +// } + +// $this->actionType = $action; +// $this->showActionModal = true; +// } + +// public function closeActionModal() +// { +// $this->showActionModal = false; +// $this->selectedEventForAction = null; +// $this->actionType = ''; +// } + +// public function forceApproveEvent($eventId) +// { +// try { +// $event = Event::with('ticket')->find($eventId); + +// if (! $event) { +// $this->error('Event not found!', position: 'toast-top'); + +// return; +// } + +// // Update ticket status +// if ($event->ticket) { +// $event->ticket->update(['status' => 'approved']); +// } + +// // Update event schedules +// Event_Schedule::where('event_id', $eventId)->update(['status' => 'approved']); + +// // Log action +// TransactionLogService::log( +// 'APPROVE', +// "SuperAdmin force-approved event: {$event->ticket->title}", +// Auth::user()->user_id +// ); + +// $this->success('Event force-approved successfully!', position: 'toast-top'); +// $this->dispatch('calendar-refetch'); +// $this->closeActionModal(); +// } catch (\Exception $e) { +// $this->error('Failed to approve event: ' . $e->getMessage(), position: 'toast-top'); +// } +// } + +// public function cancelEvent($eventId) +// { +// try { +// $event = Event::with('ticket')->find($eventId); + +// if (! $event) { +// $this->error('Event not found!', position: 'toast-top'); + +// return; +// } + +// // Update ticket status +// if ($event->ticket) { +// $event->ticket->update(['status' => 'cancelled']); +// } + +// // Update event schedules +// Event_Schedule::where('event_id', $eventId)->update(['status' => 'cancelled']); + +// // Log action +// TransactionLogService::log( +// 'CANCEL', +// "SuperAdmin cancelled event: {$event->ticket->title}", +// Auth::user()->user_id +// ); + +// $this->success('Event cancelled successfully!', position: 'toast-top'); +// $this->dispatch('calendar-refetch'); +// $this->closeActionModal(); +// } catch (\Exception $e) { +// $this->error('Failed to cancel event: ' . $e->getMessage(), position: 'toast-top'); +// } +// } + +// public function forceDeleteEvent($eventId) +// { +// try { +// $event = Event::with('ticket')->find($eventId); + +// if (! $event) { +// $this->error('Event not found!', position: 'toast-top'); + +// return; +// } + +// $eventTitle = $event->ticket->title ?? 'Unknown Event'; + +// // Delete event schedules +// Event_Schedule::where('event_id', $eventId)->delete(); + +// // Delete ticket +// if ($event->ticket) { +// $event->ticket->delete(); +// } + +// // Delete event +// $event->delete(); + +// // Log action +// TransactionLogService::log( +// 'DELETE', +// "SuperAdmin force-deleted event: {$eventTitle}", +// Auth::user()->user_id +// ); + +// $this->success('Event deleted successfully!', position: 'toast-top'); +// $this->dispatch('calendar-refetch'); +// $this->closeActionModal(); +// } catch (\Exception $e) { +// $this->error('Failed to delete event: ' . $e->getMessage(), position: 'toast-top'); +// } +// } + +// // Override the eventsForCalendar computed property to show all statuses for SuperAdmin +// #[Computed] +// public function eventsForCalendar() +// { +// // If statusFilter is 'all', don't filter by status +// if ($this->statusFilter === 'all') { +// $eventSchedules = Event_Schedule::select(['schedule_id', 'event_id', 'start_date', 'end_date', 'start_time', 'end_time', 'venue', 'status']) +// ->with([ +// 'event' => fn($q) => $q->select(['event_id', 'ticket_id', 'event__type_id']) +// ->with([ +// 'ticket' => fn($q) => $q->select(['ticket_id', 'title', 'description', 'venue_requested', 'user_id', 'status', 'ticket_number']) +// ->with([ +// 'user' => fn($q) => $q->select(['user_id', 'org_id']) +// ->with('studentOrganization:org_id,org_name,logo'), +// ]), +// 'eventType:event_type_id,type_name', +// ]), +// ]) +// // Only show approved event schedules +// ->where('status', 'approved') +// // Apply organization filter if set +// ->when($this->organizationFilter, fn($query) => $query->whereHas('event.ticket.user', fn($q) => $q->where('org_id', $this->organizationFilter))) +// // Apply event type filter if set +// ->when($this->eventTypeFilter, fn($query) => $query->whereHas('event', fn($q) => $q->where('event__type_id', $this->eventTypeFilter))) +// ->get(); +// } else { +// // Use parent method for specific status filtering +// return parent::eventsForCalendar(); +// } + +// // Process events (similar logic to parent) +// $allEvents = []; + +// foreach ($eventSchedules as $schedule) { +// $event = $schedule->event; + +// $rawStartTime = $schedule->getRawOriginal('start_time'); +// $rawEndTime = $schedule->getRawOriginal('end_time'); + +// $startTime = $rawStartTime ?: '09:00:00'; +// $endTime = $rawEndTime ?: '17:00:00'; + +// if (strlen($startTime) > 8) { +// $startTime = substr($startTime, 0, 8); +// } +// if (strlen($endTime) > 8) { +// $endTime = substr($endTime, 0, 8); +// } + +// $startDate = $schedule->start_date->format('Y-m-d'); +// $endDate = $schedule->end_date ? $schedule->end_date->format('Y-m-d') : $startDate; + +// if ($startDate === $endDate) { +// $startISO = $startDate . 'T' . $startTime . '+08:00'; +// $endISO = $endDate . 'T' . $endTime . '+08:00'; + +// $allEvents[] = [ +// 'id' => $event->event_id, +// 'title' => $event->ticket->title, +// 'start' => $startISO, +// 'end' => $endISO, +// 'allDay' => false, +// 'backgroundColor' => $this->getEventColor($event), +// 'borderColor' => $this->getEventColor($event), +// 'textColor' => '#ffffff', +// 'extendedProps' => [ +// 'organization' => $event->ticket->user->studentOrganization->org_name ?? 'No Organization', +// 'eventType' => $event->eventType?->type_name ?? 'N/A', +// 'venue' => $schedule->venue ?? $event->ticket->venue_requested ?? 'TBD', +// 'description' => $event->ticket->description, +// 'ticketNumber' => $event->ticket->ticket_number, +// 'status' => $event->ticket->status ?? 'pending', +// 'rawStartTime' => $startTime, +// 'rawEndTime' => $endTime, +// 'rawStartDate' => $startDate, +// 'rawEndDate' => $endDate, +// ], +// ]; +// } else { +// // Multi-day event logic (same as parent) +// // Calculate days of week from start_date to end_date +// $startCarbon = \Carbon\Carbon::parse($startDate); +// $endCarbon = \Carbon\Carbon::parse($endDate); +// $daysOfWeek = []; + +// $current = $startCarbon->copy(); +// while ($current->lte($endCarbon)) { +// $dayOfWeek = $current->dayOfWeek; +// if (! in_array($dayOfWeek, $daysOfWeek)) { +// $daysOfWeek[] = $dayOfWeek; +// } +// $current->addDay(); +// } + +// if (count($daysOfWeek) >= 5) { +// $daysOfWeek = [0, 1, 2, 3, 4, 5, 6]; +// } + +// $endRecurDate = \Carbon\Carbon::parse($endDate)->addDay()->format('Y-m-d'); + +// $commonProps = [ +// 'title' => $event->ticket->title, +// 'backgroundColor' => $this->getEventColor($event), +// 'borderColor' => $this->getEventColor($event), +// 'textColor' => '#ffffff', +// 'extendedProps' => [ +// 'organization' => $event->ticket->user->studentOrganization->org_name ?? 'No Organization', +// 'eventType' => $event->eventType?->type_name ?? 'N/A', +// 'venue' => $schedule->venue ?? $event->ticket->venue_requested ?? 'TBD', +// 'description' => $event->ticket->description, +// 'ticketNumber' => $event->ticket->ticket_number, +// 'status' => $event->ticket->status ?? 'pending', +// 'rawStartTime' => $startTime, +// 'rawEndTime' => $endTime, +// 'rawStartDate' => $startDate, +// 'rawEndDate' => $endDate, +// ], +// ]; + +// // Spanning event for month view +// $allEvents[] = array_merge($commonProps, [ +// 'id' => $event->event_id . '_span', +// 'groupId' => $event->event_id, +// 'start' => $startDate . 'T' . $startTime . '+08:00', +// 'end' => $endDate . 'T' . $endTime . '+08:00', +// 'allDay' => false, +// 'display' => 'block', +// 'extendedProps' => array_merge($commonProps['extendedProps'], [ +// 'forMonthView' => true, +// ]), +// ]); + +// // Recurring event for week/day views +// $allEvents[] = array_merge($commonProps, [ +// 'id' => $event->event_id . '_recur', +// 'groupId' => $event->event_id, +// 'startTime' => $startTime, +// 'endTime' => $endTime, +// 'startRecur' => $startDate, +// 'endRecur' => $endRecurDate, +// 'daysOfWeek' => $daysOfWeek, +// 'allDay' => false, +// 'display' => 'block', +// 'extendedProps' => array_merge($commonProps['extendedProps'], [ +// 'forTimeView' => true, +// ]), +// ]); +// } +// } + +// return $allEvents; +// } + +// // Override uniqueEventsCount to handle 'all' status for SuperAdmin +// #[Computed] +// public function uniqueEventsCount() +// { +// // Count unique events that match the current filters +// $query = Event_Schedule::query() +// ->where('status', 'approved'); + +// // For SuperAdmin: If statusFilter is 'all', don't filter by ticket status +// if ($this->statusFilter !== 'all') { +// $query->whereHas('event.ticket', fn($q) => $q->where('status', $this->statusFilter)); +// } + +// // Apply organization filter if set +// $query->when($this->organizationFilter, fn($query) => $query->whereHas('event.ticket.user', fn($q) => $q->where('org_id', $this->organizationFilter))); + +// // Apply event type filter if set +// $query->when($this->eventTypeFilter, fn($query) => $query->whereHas('event', fn($q) => $q->where('event__type_id', $this->eventTypeFilter))); + +// // Count distinct event_ids to get unique events (MySQL compatible) +// return (int) $query->selectRaw('COUNT(DISTINCT event_id) as count')->value('count'); +// } + +// // Override upcomingEventsThisMonth to handle 'all' status for SuperAdmin +// #[Computed] +// public function upcomingEventsThisMonth() +// { +// $startOfMonth = $this->currentDate->copy()->startOfMonth(); +// $endOfMonth = $this->currentDate->copy()->endOfMonth(); + +// $query = Event_Schedule::select(['schedule_id', 'event_id', 'start_date', 'end_date', 'start_time', 'end_time', 'venue', 'status']) +// ->with([ +// 'event' => fn($q) => $q->select(['event_id', 'ticket_id', 'event__type_id']) +// ->with([ +// 'ticket' => fn($q) => $q->select(['ticket_id', 'title', 'description', 'venue_requested', 'user_id', 'status', 'ticket_number']) +// ->with([ +// 'user' => fn($q) => $q->select(['user_id', 'org_id']) +// ->with('studentOrganization:org_id,org_name'), +// ]), +// 'eventType:event_type_id,type_name', +// ]), +// ]) +// ->where('status', 'approved') +// ->whereBetween('start_date', [$startOfMonth, $endOfMonth]); + +// // For SuperAdmin: If statusFilter is 'all', show all ticket statuses +// if ($this->statusFilter !== 'all') { +// $query->whereHas('event.ticket', fn($q) => $q->where('status', $this->statusFilter)); +// } + +// $eventSchedules = $query->orderBy('start_date') +// ->orderBy('start_time') +// ->get(); + +// return $eventSchedules->map(function ($schedule) { +// $event = $schedule->event; +// $rawStartTime = $schedule->getRawOriginal('start_time'); +// $rawEndTime = $schedule->getRawOriginal('end_time'); + +// // Format date +// $startDate = \Carbon\Carbon::parse($schedule->start_date); +// $endDate = $schedule->end_date ? \Carbon\Carbon::parse($schedule->end_date) : $startDate; + +// // Format time +// $formatTime = function ($timeStr) { +// if (! $timeStr) { +// return ''; +// } +// try { +// $parts = explode(':', $timeStr); +// $hours = (int) $parts[0]; +// $minutes = (int) ($parts[1] ?? 0); +// $period = $hours >= 12 ? 'PM' : 'AM'; +// $hour12 = $hours % 12 ?: 12; + +// return sprintf('%d:%02d %s', $hour12, $minutes, $period); +// } catch (\Exception $e) { +// return $timeStr; +// } +// }; + +// $startTimeFormatted = $formatTime($rawStartTime); +// $endTimeFormatted = $formatTime($rawEndTime); +// $timeRange = $startTimeFormatted && $endTimeFormatted +// ? "{$startTimeFormatted} - {$endTimeFormatted}" +// : ($startTimeFormatted ?: 'TBD'); + +// // Date range formatting +// $dateDisplay = $startDate->format('M d, Y'); +// if ($startDate->format('Y-m-d') !== $endDate->format('Y-m-d')) { +// $dateDisplay = $startDate->format('M d') . ' - ' . $endDate->format('M d, Y'); +// } + +// // Get event color using the same method as calendar +// $hexColor = $this->getEventColor($event); +// $colorName = $this->hexToTailwindColor($hexColor); + +// return [ +// 'title' => $event->ticket->title ?? 'Untitled Event', +// 'description' => $event->ticket->description ?? null, +// 'organization' => $event->ticket->user->studentOrganization->org_name ?? 'No Organization', +// 'organizationLogo' => $event->ticket->user->studentOrganization->logo ?? asset('images/default-org-logo.svg'), +// 'eventType' => $event->eventType?->type_name ?? 'N/A', +// 'date' => $dateDisplay, +// 'time' => $timeRange, +// 'datetime' => "{$dateDisplay} • {$timeRange}", +// 'venue' => $schedule->venue ?? $event->ticket->venue_requested ?? 'TBD', +// 'color' => $colorName, +// 'hexColor' => $hexColor, +// 'icon' => $this->getEventTypeIcon($event->eventType?->type_name ?? ''), +// 'start_date' => $schedule->start_date, +// 'event_id' => $event->event_id, +// ]; +// })->toArray(); +// } + +// // Helper method from parent (needed for upcomingEventsThisMonth) +// private function hexToTailwindColor($hexColor): string +// { +// $colorMap = [ +// '#10b981' => 'green', +// '#3b82f6' => 'blue', +// '#8b5cf6' => 'purple', +// '#f59e0b' => 'yellow', +// '#ef4444' => 'red', +// '#06b6d4' => 'cyan', +// '#84cc16' => 'lime', +// '#f97316' => 'orange', +// ]; + +// return $colorMap[$hexColor] ?? 'blue'; +// } + +// // Helper method from parent (needed for upcomingEventsThisMonth) +// private function getEventTypeIcon($typeName): string +// { +// $iconMap = [ +// 'cultural' => 's-musical-note', +// 'competition' => 's-trophy', +// 'academic' => 's-academic-cap', +// 'workshop' => 's-academic-cap', +// 'meeting' => 's-building-office', +// 'social' => 's-sparkles', +// ]; + +// $lowerType = strtolower($typeName); +// foreach ($iconMap as $key => $icon) { +// if (str_contains($lowerType, $key)) { +// return $icon; +// } +// } + +// return 's-calendar-days'; +// } +// } - // Helper method from parent (needed for upcomingEventsThisMonth) - private function getEventTypeIcon($typeName): string - { - $iconMap = [ - 'cultural' => 's-musical-note', - 'competition' => 's-trophy', - 'academic' => 's-academic-cap', - 'workshop' => 's-academic-cap', - 'meeting' => 's-building-office', - 'social' => 's-sparkles', - ]; - - $lowerType = strtolower($typeName); - foreach ($iconMap as $key => $icon) { - if (str_contains($lowerType, $key)) { - return $icon; - } - } - - return 's-calendar-days'; - } -} diff --git a/app/Livewire/Superadmin/Dashboard.php b/app/Livewire/Superadmin/Dashboard.php index 804bf7d..1f1f3a3 100644 --- a/app/Livewire/Superadmin/Dashboard.php +++ b/app/Livewire/Superadmin/Dashboard.php @@ -6,27 +6,39 @@ use App\Models\Event; use App\Models\Ticket; use App\Models\Transaction_Logs; +use App\Models\Event_Schedule; use App\Services\TransactionLogService; use Livewire\Attributes\Layout; use Livewire\Attributes\Title; use Livewire\Component; use Livewire\Attributes\Computed; +use Livewire\Attributes\Lazy; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; +use Carbon\Carbon; use Mary\Traits\Toast; +#[Lazy] class Dashboard extends Component { use Toast; #[Title('Superadmin - Dashboard')] #[Layout('components.layouts.superadmin')] + public function placeholder() + { + return view('livewire.superadmin.placeholders.dashboard'); + } + public function render() { return view('livewire.superadmin.dashboard')->with([ 'stats' => $this->stats, + 'todaySnapshot' => $this->todaySnapshot, + 'attentionRequired' => $this->attentionRequired, 'pendingApprovals' => $this->pendingApprovals, - 'recentLogs' => $this->recentLogs, + 'recentActivity' => $this->recentActivity, + 'upcomingEvents' => $this->upcomingEvents, 'headers' => $this->headers, ]); } @@ -35,11 +47,106 @@ public function render() public function stats(): array { return Cache::remember('superadmin_dashboard_stats', 300, function () { + $pendingTickets = Ticket::where('status', 'pending')->count(); + $gsoReviewTickets = Ticket::where('status', 'gso_review')->count(); + $forRevisionTickets = Ticket::where('status', 'for_revision')->count(); + return [ 'totalUsers' => User::count(), 'totalTickets' => Ticket::count(), 'totalEvents' => Event::count(), - 'pendingTickets' => Ticket::where('status', 'pending')->count(), + 'pendingTickets' => $pendingTickets, + 'gsoReviewTickets' => $gsoReviewTickets, + 'forRevisionTickets' => $forRevisionTickets, + 'eventsThisWeek' => Event::whereHas('eventSchedules', function ($q) { + $q->whereBetween('start_date', [now()->startOfWeek(), now()->endOfWeek()]); + })->count(), + 'upcomingEventsCount' => Event::whereHas('eventSchedules', function ($q) { + $q->where('start_date', '>=', now()->toDateString()) + ->where('start_date', '<=', now()->addDays(7)->toDateString()); + })->count(), + ]; + }); + } + + #[Computed(persist: true, seconds: 180)] // 3 minutes cache + public function todaySnapshot(): array + { + return Cache::remember('superadmin_dashboard_today_snapshot', 180, function () { + $today = now()->toDateString(); + $todayStart = now()->startOfDay(); + $todayEnd = now()->endOfDay(); + + return [ + 'eventsToday' => Event::whereHas('eventSchedules', function ($q) use ($today) { + $q->whereDate('start_date', $today); + })->count(), + 'ticketsSubmittedToday' => Ticket::whereDate('created_at', $today)->count(), + 'ticketsApprovedToday' => Ticket::where('status', 'approved') + ->whereDate('updated_at', $today)->count(), + 'ticketsRejectedToday' => Ticket::where('status', 'rejected') + ->whereDate('updated_at', $today)->count(), + 'newUsersToday' => User::whereDate('created_at', $today)->count(), + ]; + }); + } + + #[Computed(persist: true, seconds: 120)] // 2 minutes cache + public function attentionRequired(): array + { + return Cache::remember('superadmin_dashboard_attention', 120, function () { + // Tickets awaiting OSA review (pending status) + $pendingOsaReview = Ticket::where('status', 'pending') + ->orderBy('created_at', 'asc') + ->limit(5) + ->get() + ->map(function ($ticket) { + return [ + 'id' => $ticket->ticket_id, + 'title' => $ticket->title, + 'type' => 'pending_review', + 'days_waiting' => $ticket->created_at->diffInDays(now()), + 'created_at' => $ticket->created_at->format('M d, Y'), + ]; + }); + + // Tickets stuck in GSO review for more than 3 days + $stuckGsoReview = Ticket::where('status', 'gso_review') + ->where('updated_at', '<', now()->subDays(3)) + ->orderBy('updated_at', 'asc') + ->limit(5) + ->get() + ->map(function ($ticket) { + return [ + 'id' => $ticket->ticket_id, + 'title' => $ticket->title, + 'type' => 'stuck_gso', + 'days_waiting' => $ticket->updated_at->diffInDays(now()), + 'created_at' => $ticket->created_at->format('M d, Y'), + ]; + }); + + // Tickets needing revision follow-up (for_revision for more than 5 days) + $revisionFollowup = Ticket::where('status', 'for_revision') + ->where('updated_at', '<', now()->subDays(5)) + ->orderBy('updated_at', 'asc') + ->limit(5) + ->get() + ->map(function ($ticket) { + return [ + 'id' => $ticket->ticket_id, + 'title' => $ticket->title, + 'type' => 'revision_overdue', + 'days_waiting' => $ticket->updated_at->diffInDays(now()), + 'created_at' => $ticket->created_at->format('M d, Y'), + ]; + }); + + return [ + 'pending_osa_review' => $pendingOsaReview->toArray(), + 'stuck_gso_review' => $stuckGsoReview->toArray(), + 'revision_followup' => $revisionFollowup->toArray(), + 'total_attention' => $pendingOsaReview->count() + $stuckGsoReview->count() + $revisionFollowup->count(), ]; }); } @@ -53,18 +160,20 @@ public function pendingApprovals(): array 'eventType:event_type_id,type_name', 'user:user_id,name' ]) - ->where('status', 'pending') + ->whereIn('status', ['pending', 'gso_review', 'pending_osa_approval']) ->orderBy('created_at', 'desc') ->limit(5) ->get() ->map(function ($ticket) { + $userDeleted = $ticket->user?->trashed(); return [ 'id' => $ticket->ticket_id, 'request' => $ticket->title, 'type' => $ticket->eventType ? $ticket->eventType->type_name : 'N/A', 'submitted' => $ticket->created_at->setTimezone('Asia/Manila')->format('M d, Y g:i A'), - 'status' => ucfirst($ticket->status), - 'user' => $ticket->user ? $ticket->user->name : 'Unknown', + 'status' => ucfirst(str_replace('_', ' ', $ticket->status)), + 'raw_status' => $ticket->status, + 'user' => $userDeleted ? 'Deleted User' : ($ticket->user ? $ticket->user->name : 'Unknown'), ]; }) ->toArray(); @@ -72,20 +181,57 @@ public function pendingApprovals(): array } #[Computed(persist: true, seconds: 120)] // 2 minutes cache - public function recentLogs(): array + public function recentActivity(): array { - return Cache::remember('superadmin_dashboard_recent_logs', 120, function () { + return Cache::remember('superadmin_dashboard_recent_activity', 120, function () { return Transaction_Logs::select(['log_id', 'action', 'details', 'created_at', 'user_id']) - ->with('user:user_id,email') + ->with('user:user_id,email,name') ->orderBy('created_at', 'desc') - ->limit(5) + ->limit(8) ->get() ->map(function ($log) { return [ - 'user' => $log->user ? $log->user->email : 'System', + 'id' => $log->log_id, + 'user' => $log->user ? $log->user->name : 'System', + 'email' => $log->user ? $log->user->email : 'system@app', 'action' => $log->action, - 'target' => $log->details, + 'details' => $log->details, 'timestamp' => $log->created_at->setTimezone('Asia/Manila')->format('M d, Y g:i A'), + 'time_ago' => $log->created_at->diffForHumans(), + ]; + }) + ->toArray(); + }); + } + + #[Computed(persist: true, seconds: 300)] // 5 minutes cache + public function upcomingEvents(): array + { + return Cache::remember('superadmin_dashboard_upcoming_events', 300, function () { + $today = now()->toDateString(); + $nextWeek = now()->addDays(7)->toDateString(); + + return Event_Schedule::select(['schedule_id', 'event_id', 'start_date', 'start_time', 'end_time', 'venue']) + ->with(['event:event_id,event_name']) + ->whereBetween('start_date', [$today, $nextWeek]) + ->orderBy('start_date', 'asc') + ->orderBy('start_time', 'asc') + ->limit(7) + ->get() + ->map(function ($schedule) { + $eventDate = Carbon::parse($schedule->start_date); + $isToday = $eventDate->isToday(); + $isTomorrow = $eventDate->isTomorrow(); + + return [ + 'id' => $schedule->schedule_id, + 'event_name' => $schedule->event ? $schedule->event->event_name : 'Unknown Event', + 'venue' => $schedule->venue ?? 'TBD', + 'date' => $eventDate->format('M d, Y'), + 'day_label' => $isToday ? 'Today' : ($isTomorrow ? 'Tomorrow' : $eventDate->format('l')), + 'time' => $schedule->start_time ? Carbon::parse($schedule->start_time)->format('g:i A') : 'All Day', + 'is_today' => $isToday, + 'is_tomorrow' => $isTomorrow, ]; }) ->toArray(); @@ -95,10 +241,20 @@ public function recentLogs(): array public function refreshData() { // Clear computed properties and cache - unset($this->stats, $this->pendingApprovals, $this->recentLogs); + unset( + $this->stats, + $this->todaySnapshot, + $this->attentionRequired, + $this->pendingApprovals, + $this->recentActivity, + $this->upcomingEvents + ); Cache::forget('superadmin_dashboard_stats'); + Cache::forget('superadmin_dashboard_today_snapshot'); + Cache::forget('superadmin_dashboard_attention'); Cache::forget('superadmin_dashboard_pending_approvals'); - Cache::forget('superadmin_dashboard_recent_logs'); + Cache::forget('superadmin_dashboard_recent_activity'); + Cache::forget('superadmin_dashboard_upcoming_events'); $this->success('Dashboard data refreshed!', position: 'toast-top'); } diff --git a/app/Livewire/Superadmin/FaqEditor.php b/app/Livewire/Superadmin/FaqEditor.php new file mode 100644 index 0000000..ecc2fd5 --- /dev/null +++ b/app/Livewire/Superadmin/FaqEditor.php @@ -0,0 +1,401 @@ +faqId = $faq->id; + $this->question = $faq->question; + $this->answer = $faq->answer; + $this->category = $faq->category; + $this->displayOrder = $faq->display_order; + $this->isActive = (bool) $faq->is_active; + $this->isEditing = true; + $this->categorySearch = $faq->category ?? ''; + } else { + // Set default display order for new FAQs + $maxOrder = Faq::max('display_order') ?? 0; + $this->displayOrder = $maxOrder + 1; + } + } + + public function render() + { + return view('livewire.superadmin.faq-editor', [ + 'categories' => Faq::getCategories(), + ]); + } + + /** + * Validation rules + */ + protected function rules(): array + { + return [ + 'question' => 'required|string|max:500', + 'answer' => 'required|string|max:5000', + 'category' => 'nullable|string|max:100', + 'displayOrder' => 'required|integer|min:0', + 'isActive' => 'boolean', + ]; + } + + /** + * Custom validation messages + */ + protected function messages(): array + { + return [ + 'question.required' => 'The question is required.', + 'question.max' => 'The question must not exceed 500 characters.', + 'answer.required' => 'The answer is required.', + 'answer.max' => 'The answer must not exceed 5000 characters.', + 'category.max' => 'The category must not exceed 100 characters.', + ]; + } + + /** + * Save the FAQ + */ + public function save(): mixed + { + $this->validate(); + + DB::beginTransaction(); + try { + if ($this->isEditing) { + $faq = Faq::find($this->faqId); + $changes = []; + + // Track changes for logging + if ($faq->question !== $this->question) { + $changes[] = "Question updated"; + } + if ($faq->answer !== $this->answer) { + $changes[] = "Answer updated"; + } + if ($faq->category !== $this->category) { + $oldCat = $faq->category ?: '(none)'; + $newCat = $this->category ?: '(none)'; + $changes[] = "Category: {$oldCat} → {$newCat}"; + } + if ($faq->display_order !== $this->displayOrder) { + $changes[] = "Order: {$faq->display_order} → {$this->displayOrder}"; + } + if ((bool) $faq->is_active !== $this->isActive) { + $oldStatus = $faq->is_active ? 'Active' : 'Inactive'; + $newStatus = $this->isActive ? 'Active' : 'Inactive'; + $changes[] = "Status: {$oldStatus} → {$newStatus}"; + } + + $faq->update([ + 'question' => $this->question, + 'answer' => $this->answer, + 'category' => $this->category ?: null, + 'display_order' => $this->displayOrder, + 'is_active' => $this->isActive, + ]); + + if (!empty($changes)) { + TransactionLogService::log( + 'faq_updated', + "FAQ updated (ID: {$faq->id}): " . implode(', ', $changes) + ); + } + + DB::commit(); + Faq::clearCache(); + $this->success('FAQ updated successfully!', position: 'toast-bottom'); + } else { + $faq = Faq::create([ + 'question' => $this->question, + 'answer' => $this->answer, + 'category' => $this->category ?: null, + 'display_order' => $this->displayOrder, + 'is_active' => $this->isActive, + ]); + + TransactionLogService::log( + 'faq_created', + "FAQ created: {$faq->question} (ID: {$faq->id})" + ); + + DB::commit(); + + // Send notification to superadmins + $superadmins = User::where('role_id', User::getRoleId('superadmin'))->get(); + foreach ($superadmins as $admin) { + $admin->notify(new SystemSettingsUpdatedNotification( + 'faq', + $faq->question, + 'created', + auth()->user() + )); + } + + Faq::clearCache(); + $this->success('FAQ created successfully!', position: 'toast-bottom'); + } + + return $this->redirect(route('superadmin.faqs'), navigate: true); + } catch (\Exception $e) { + DB::rollBack(); + \Log::error('FAQ save failed', ['error' => $e->getMessage()]); + $this->error('Failed to save FAQ: ' . $e->getMessage(), position: 'toast-bottom'); + return null; + } + } + + /** + * Cancel and go back + */ + public function cancel(): mixed + { + return $this->redirect(route('superadmin.faqs'), navigate: true); + } + + /** + * Get filtered categories based on search + */ + public function getFilteredCategoriesProperty(): array + { + $allCategories = Faq::getCategories(); + + if (empty($this->categorySearch)) { + return $allCategories; + } + + return array_values(array_filter($allCategories, function ($cat) { + return stripos($cat, $this->categorySearch) !== false; + })); + } + + /** + * Check if search matches any existing category exactly + */ + public function getIsExactMatchProperty(): bool + { + $allCategories = Faq::getCategories(); + return in_array($this->categorySearch, $allCategories, true); + } + + /** + * Open category dropdown + */ + public function openCategoryDropdown(): void + { + $this->showCategoryDropdown = true; + $this->categoryMode = 'selecting'; + } + + /** + * Close category dropdown + */ + public function closeCategoryDropdown(): void + { + $this->showCategoryDropdown = false; + $this->categoryMode = 'selecting'; + $this->editingCategory = null; + $this->editCategoryName = ''; + } + + /** + * Handle category search input change + */ + public function updatedCategorySearch(): void + { + $this->showCategoryDropdown = true; + $this->categoryMode = 'selecting'; + + // Update the actual category value + $this->category = !empty($this->categorySearch) ? $this->categorySearch : null; + } + + /** + * Select a category from the dropdown + */ + public function selectCategory(string $categoryName): void + { + $this->category = $categoryName; + $this->categorySearch = $categoryName; + $this->closeCategoryDropdown(); + } + + /** + * Create new category (just sets the value, saved with FAQ) + */ + public function createNewCategory(): void + { + $trimmed = trim($this->categorySearch); + if (empty($trimmed)) { + $this->error('Category name cannot be empty.', position: 'toast-bottom'); + return; + } + + if (strlen($trimmed) > 100) { + $this->error('Category name must not exceed 100 characters.', position: 'toast-bottom'); + return; + } + + $this->category = $trimmed; + $this->categorySearch = $trimmed; + $this->closeCategoryDropdown(); + $this->success('New category set! It will be created when you save the FAQ.', position: 'toast-bottom'); + } + + /** + * Clear selected category + */ + public function clearCategory(): void + { + $this->category = null; + $this->categorySearch = ''; + $this->closeCategoryDropdown(); + } + + /** + * Start editing a category + */ + public function startEditCategory(string $categoryName): void + { + $this->categoryMode = 'editing'; + $this->editingCategory = $categoryName; + $this->editCategoryName = $categoryName; + } + + /** + * Cancel editing category + */ + public function cancelEditCategory(): void + { + $this->categoryMode = 'selecting'; + $this->editingCategory = null; + $this->editCategoryName = ''; + } + + /** + * Save edited category (renames across all FAQs) + */ + public function saveEditCategory(): void + { + $newName = trim($this->editCategoryName); + + if (empty($newName)) { + $this->error('Category name cannot be empty.', position: 'toast-bottom'); + return; + } + + if (strlen($newName) > 100) { + $this->error('Category name must not exceed 100 characters.', position: 'toast-bottom'); + return; + } + + if ($newName === $this->editingCategory) { + $this->cancelEditCategory(); + return; + } + + // Check if new name already exists + $existingCategories = Faq::getCategories(); + if (in_array($newName, $existingCategories)) { + $this->error('A category with this name already exists.', position: 'toast-bottom'); + return; + } + + DB::beginTransaction(); + try { + $affectedCount = Faq::where('category', $this->editingCategory) + ->update(['category' => $newName]); + + TransactionLogService::log( + 'faq_category_renamed', + "FAQ category renamed: '{$this->editingCategory}' → '{$newName}' ({$affectedCount} FAQs affected)" + ); + + DB::commit(); + Faq::clearCache(); + + // Update current selection if it was the renamed category + if ($this->category === $this->editingCategory) { + $this->category = $newName; + $this->categorySearch = $newName; + } + + $this->success("Category renamed! {$affectedCount} FAQ(s) updated.", position: 'toast-bottom'); + $this->cancelEditCategory(); + } catch (\Exception $e) { + DB::rollBack(); + \Log::error('Category rename failed', ['error' => $e->getMessage()]); + $this->error('Failed to rename category.', position: 'toast-bottom'); + } + } + + /** + * Delete a category (sets to null for all FAQs using it) + */ + public function deleteCategory(string $categoryName): void + { + DB::beginTransaction(); + try { + $affectedCount = Faq::where('category', $categoryName) + ->update(['category' => null]); + + TransactionLogService::log( + 'faq_category_deleted', + "FAQ category deleted: '{$categoryName}' ({$affectedCount} FAQs moved to uncategorized)" + ); + + DB::commit(); + Faq::clearCache(); + + // Clear current selection if it was the deleted category + if ($this->category === $categoryName) { + $this->category = null; + $this->categorySearch = ''; + } + + $this->success("Category deleted! {$affectedCount} FAQ(s) are now uncategorized.", position: 'toast-bottom'); + } catch (\Exception $e) { + DB::rollBack(); + \Log::error('Category delete failed', ['error' => $e->getMessage()]); + $this->error('Failed to delete category.', position: 'toast-bottom'); + } + } +} diff --git a/app/Livewire/Superadmin/FaqManager.php b/app/Livewire/Superadmin/FaqManager.php new file mode 100644 index 0000000..7607917 --- /dev/null +++ b/app/Livewire/Superadmin/FaqManager.php @@ -0,0 +1,226 @@ + $this->getFaqs(), + 'categories' => $this->getCategories(), + ]); + } + + protected function getFaqs() + { + $query = Faq::query()->ordered(); + + // Apply search filter + if ($this->search) { + $query->where(function ($q) { + $q->where('question', 'like', '%' . $this->search . '%') + ->orWhere('answer', 'like', '%' . $this->search . '%') + ->orWhere('category', 'like', '%' . $this->search . '%'); + }); + } + + // Apply category filter + if ($this->filterCategory) { + if ($this->filterCategory === '__none__') { + $query->whereNull('category'); + } else { + $query->where('category', $this->filterCategory); + } + } + + // Apply status filter + if ($this->filterStatus !== '') { + $query->where('is_active', $this->filterStatus === 'active'); + } + + return $query->paginate(10); + } + + protected function getCategories(): array + { + return Faq::whereNotNull('category') + ->distinct() + ->pluck('category') + ->filter() + ->values() + ->toArray(); + } + + public function updatedSearch() + { + $this->resetPage(); + } + + public function updatedFilterCategory() + { + $this->resetPage(); + } + + public function updatedFilterStatus() + { + $this->resetPage(); + } + + public function clearFilters() + { + $this->reset(['search', 'filterCategory', 'filterStatus']); + $this->resetPage(); + } + + /** + * Open delete confirmation modal + */ + public function openDeleteModal(int $faqId): void + { + $faq = Faq::find($faqId); + if ($faq) { + $this->deletingFaqId = $faqId; + $this->deletingFaqQuestion = $faq->question; + $this->deleteModalOpen = true; + } + } + + /** + * Reset delete modal state + */ + public function resetDeleteModal(): void + { + $this->reset(['deletingFaqId', 'deletingFaqQuestion']); + $this->deleteModalOpen = false; + } + + /** + * Confirm and execute deletion + */ + public function confirmDelete(): void + { + $faq = Faq::find($this->deletingFaqId); + + if (!$faq) { + $this->error('FAQ not found!', position: 'toast-top'); + return; + } + + DB::beginTransaction(); + try { + // Log the deletion + TransactionLogService::log( + 'faq_deleted', + "FAQ deleted: {$faq->question} (ID: {$faq->id})" + ); + + $faq->delete(); + + DB::commit(); + + $this->resetDeleteModal(); + Faq::clearCache(); + $this->success('FAQ deleted successfully!', position: 'toast-top'); + } catch (\Exception $e) { + DB::rollBack(); + \Log::error('FAQ deletion failed', ['error' => $e->getMessage()]); + $this->error('Failed to delete FAQ: ' . $e->getMessage(), position: 'toast-top'); + } + } + + /** + * Toggle FAQ active status + */ + public function toggleActive(int $faqId): void + { + $faq = Faq::find($faqId); + + if (!$faq) { + $this->error('FAQ not found!', position: 'toast-top'); + return; + } + + DB::beginTransaction(); + try { + $previousStatus = $faq->is_active ? 'active' : 'inactive'; + $faq->is_active = !$faq->is_active; + $faq->save(); + + $newStatus = $faq->is_active ? 'active' : 'inactive'; + TransactionLogService::log( + 'faq_status_changed', + "FAQ status changed: {$previousStatus} → {$newStatus} - {$faq->question} (ID: {$faq->id})" + ); + + DB::commit(); + + Faq::clearCache(); + $statusLabel = $faq->is_active ? 'activated' : 'deactivated'; + $this->success("FAQ {$statusLabel} successfully!", position: 'toast-bottom'); + } catch (\Exception $e) { + DB::rollBack(); + \Log::error('FAQ status toggle failed', ['error' => $e->getMessage()]); + $this->error('Failed to update FAQ status: ' . $e->getMessage(), position: 'toast-bottom'); + } + } + + /** + * Update FAQ order after drag and drop + */ + public function updateOrder(array $orderedIds): void + { + DB::beginTransaction(); + try { + foreach ($orderedIds as $index => $faqId) { + Faq::where('id', $faqId)->update(['display_order' => $index + 1]); + } + + TransactionLogService::log( + 'faq_reordered', + "FAQ display order updated via drag and drop" + ); + + DB::commit(); + Faq::clearCache(); + // $this->success('FAQ order updated!', position: 'toast-bottom'); + } catch (\Exception $e) { + DB::rollBack(); + \Log::error('FAQ reorder failed', ['error' => $e->getMessage()]); + $this->error('Failed to update order.', position: 'toast-bottom'); + } + } +} diff --git a/app/Livewire/Superadmin/Logs.php b/app/Livewire/Superadmin/Logs.php index 13a9133..aaaef0f 100644 --- a/app/Livewire/Superadmin/Logs.php +++ b/app/Livewire/Superadmin/Logs.php @@ -9,10 +9,12 @@ use Livewire\Attributes\Title; use Livewire\Attributes\Layout; use Livewire\Attributes\Computed; +use Livewire\Attributes\Lazy; use Livewire\WithPagination; use Illuminate\Support\Facades\Cache; use Mary\Traits\Toast; +#[Lazy] class Logs extends Component { use WithPagination, Toast; @@ -20,6 +22,11 @@ class Logs extends Component #[Title('Superadmin - Transaction Logs')] #[Layout('components.layouts.superadmin')] + public function placeholder() + { + return view('livewire.superadmin.placeholders.logs'); + } + // Search and filter properties with URL state #[Url(except: '')] public $search = ''; diff --git a/app/Livewire/Superadmin/Notifications.php b/app/Livewire/Superadmin/Notifications.php index 6b9d8e2..fabcd0d 100644 --- a/app/Livewire/Superadmin/Notifications.php +++ b/app/Livewire/Superadmin/Notifications.php @@ -3,6 +3,8 @@ namespace App\Livewire\Superadmin; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Log; +use Livewire\Attributes\Lazy; use Livewire\Attributes\Layout; use Livewire\Attributes\On; use Livewire\Attributes\Title; @@ -11,6 +13,7 @@ use Livewire\WithPagination; use Mary\Traits\Toast; +#[Lazy] class Notifications extends Component { use Toast, WithPagination; @@ -18,6 +21,11 @@ class Notifications extends Component #[Title('Notifications - SuperAdmin')] #[Layout('components.layouts.superadmin')] + public function placeholder() + { + return view('livewire.superadmin.placeholders.notifications'); + } + #[Url(except: '')] public $search = ''; @@ -123,70 +131,90 @@ public function getNotificationsProperty() public function markAsRead($notificationId) { - $user = auth()->user(); - $notification = $user->notifications()->find($notificationId); - - if ($notification) { - $notification->markAsRead(); - // Clear cache to refresh counts - Cache::forget("superadmin_notifications_counts_{$user->user_id}"); - $this->loadCounts(); + try { + $user = auth()->user(); + $notification = $user->notifications()->find($notificationId); + + if ($notification) { + $notification->markAsRead(); + // Clear cache to refresh counts + Cache::forget("superadmin_notifications_counts_{$user->user_id}"); + $this->loadCounts(); + } + } catch (\Exception $e) { + Log::error('Failed to mark notification as read: ' . $e->getMessage()); + $this->error('Failed to mark notification as read.', position: 'toast-top'); } } public function markAllAsRead() { - $user = auth()->user(); + try { + $user = auth()->user(); - // Only mark system-level notifications as read - $user->notifications() - ->whereRaw('JSON_EXTRACT(data, "$.type") NOT LIKE ?', ['ticket_%']) - ->whereNull('read_at') - ->update(['read_at' => now()]); + // Only mark system-level notifications as read + $user->notifications() + ->whereRaw('JSON_EXTRACT(data, "$.type") NOT LIKE ?', ['ticket_%']) + ->whereNull('read_at') + ->update(['read_at' => now()]); - // Clear cache to refresh counts - Cache::forget("superadmin_notifications_counts_{$user->user_id}"); - $this->loadCounts(); + // Clear cache to refresh counts + Cache::forget("superadmin_notifications_counts_{$user->user_id}"); + $this->loadCounts(); - $this->success('All notifications marked as read.', position: 'toast-top'); + $this->success('All notifications marked as read.', position: 'toast-top'); + } catch (\Exception $e) { + Log::error('Failed to mark all notifications as read: ' . $e->getMessage()); + $this->error('Failed to mark all notifications as read.', position: 'toast-top'); + } } public function deleteNotification($notificationId) { - $user = auth()->user(); - $notification = $user->notifications()->find($notificationId); + try { + $user = auth()->user(); + $notification = $user->notifications()->find($notificationId); - if ($notification) { - $notification->delete(); + if ($notification) { + $notification->delete(); - // Clear cache to refresh counts - Cache::forget("superadmin_notifications_counts_{$user->user_id}"); - $this->loadCounts(); - $this->resetPage(); + // Clear cache to refresh counts + Cache::forget("superadmin_notifications_counts_{$user->user_id}"); + $this->loadCounts(); + $this->resetPage(); - $this->success('Notification deleted.', position: 'toast-top'); + $this->success('Notification deleted.', position: 'toast-top'); + } + } catch (\Exception $e) { + Log::error('Failed to delete notification: ' . $e->getMessage()); + $this->error('Failed to delete notification.', position: 'toast-top'); } } public function clearAllRead() { - $user = auth()->user(); + try { + $user = auth()->user(); - // Only delete read system-level notifications - $deletedCount = $user->notifications() - ->whereRaw('JSON_EXTRACT(data, "$.type") NOT LIKE ?', ['ticket_%']) - ->whereNotNull('read_at') - ->delete(); + // Only delete read system-level notifications + $deletedCount = $user->notifications() + ->whereRaw('JSON_EXTRACT(data, "$.type") NOT LIKE ?', ['ticket_%']) + ->whereNotNull('read_at') + ->delete(); - // Clear cache to refresh counts - Cache::forget("superadmin_notifications_counts_{$user->user_id}"); - $this->loadCounts(); - $this->resetPage(); + // Clear cache to refresh counts + Cache::forget("superadmin_notifications_counts_{$user->user_id}"); + $this->loadCounts(); + $this->resetPage(); - if ($deletedCount > 0) { - $this->success("{$deletedCount} read notification(s) cleared.", position: 'toast-top'); - } else { - $this->info('No read notifications to clear.', position: 'toast-top'); + if ($deletedCount > 0) { + $this->success("{$deletedCount} read notification(s) cleared.", position: 'toast-top'); + } else { + $this->info('No read notifications to clear.', position: 'toast-top'); + } + } catch (\Exception $e) { + Log::error('Failed to clear read notifications: ' . $e->getMessage()); + $this->error('Failed to clear read notifications.', position: 'toast-top'); } } diff --git a/app/Livewire/Superadmin/Profile.php b/app/Livewire/Superadmin/Profile.php index 5871cc0..1f5d12e 100644 --- a/app/Livewire/Superadmin/Profile.php +++ b/app/Livewire/Superadmin/Profile.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Log; use Illuminate\Validation\Rules\Password; use Livewire\Attributes\Layout; use Livewire\Attributes\On; @@ -64,81 +65,95 @@ public function refreshUser(): void public function updateProfile() { - $this->validate([ - 'name' => [ - 'required', - 'string', - 'min:2', - 'max:255', - 'regex:/^[a-zA-Z\s\-\.]+$/', - ], - 'email' => [ - 'required', - 'email:rfc,dns', - 'max:255', - 'unique:users,email,' . Auth::id() . ',user_id', - ], - 'phone' => [ - 'nullable', - 'string', - 'regex:/^(\+63|0)?[9]\d{9}$/', - ], - ], [ - 'name.required' => 'Full name is required.', - 'name.min' => 'Full name must be at least 2 characters.', - 'name.max' => 'Full name must not exceed 255 characters.', - 'name.regex' => 'Full name may only contain letters, spaces, hyphens, and periods.', - 'email.required' => 'Email address is required.', - 'email.email' => 'Please provide a valid email address.', - 'email.unique' => 'This email address is already in use.', - 'phone.regex' => 'Please provide a valid Philippine mobile number (e.g., 09123456789 or +639123456789).', - ]); - - $user = Auth::user(); - $user->update([ - 'name' => $this->name, - 'email' => $this->email, - 'phone' => $this->phone, - ]); - - $this->success('Profile updated successfully!', position: 'toast-top'); - $this->dispatch('avatar-updated'); + try { + $this->validate([ + 'name' => [ + 'required', + 'string', + 'min:2', + 'max:255', + 'regex:/^[a-zA-Z\s\-\.]+$/', + ], + 'email' => [ + 'required', + 'email:rfc,dns', + 'max:255', + 'unique:users,email,' . Auth::id() . ',user_id', + ], + 'phone' => [ + 'nullable', + 'string', + 'regex:/^(\+63|0)?[9]\d{9}$/', + ], + ], [ + 'name.required' => 'Full name is required.', + 'name.min' => 'Full name must be at least 2 characters.', + 'name.max' => 'Full name must not exceed 255 characters.', + 'name.regex' => 'Full name may only contain letters, spaces, hyphens, and periods.', + 'email.required' => 'Email address is required.', + 'email.email' => 'Please provide a valid email address.', + 'email.unique' => 'This email address is already in use.', + 'phone.regex' => 'Please provide a valid Philippine mobile number (e.g., 09123456789 or +639123456789).', + ]); + + $user = Auth::user(); + $user->update([ + 'name' => $this->name, + 'email' => $this->email, + 'phone' => $this->phone, + ]); + + $this->success('Profile updated successfully!', position: 'toast-top'); + $this->dispatch('avatar-updated'); + } catch (\Illuminate\Validation\ValidationException $e) { + throw $e; + } catch (\Exception $e) { + Log::error('Failed to update profile: ' . $e->getMessage()); + $this->error('Failed to update profile. Please try again.', position: 'toast-top'); + } } public function updatePassword() { - $this->validate([ - 'current_password' => [ - 'required', - 'current_password', - ], - 'new_password' => [ - 'required', - 'confirmed', - 'different:current_password', - Password::min(8) - ->mixedCase() - ->numbers() - ->symbols() - ->uncompromised(), - ], - ], [ - 'current_password.required' => 'Current password is required.', - 'current_password.current_password' => 'The current password is incorrect.', - 'new_password.required' => 'New password is required.', - 'new_password.confirmed' => 'Password confirmation does not match.', - 'new_password.different' => 'New password must be different from current password.', - ]); - - $user = Auth::user(); - $user->update([ - 'password' => Hash::make($this->new_password), - ]); - - // Reset password fields - $this->reset(['current_password', 'new_password', 'new_password_confirmation']); - - $this->success('Password updated successfully!', position: 'toast-top'); + try { + $this->validate([ + 'current_password' => [ + 'required', + 'current_password', + ], + 'new_password' => [ + 'required', + 'confirmed', + 'different:current_password', + Password::min(8) + ->mixedCase() + ->numbers() + ->symbols() + ->uncompromised(), + ], + ], [ + 'current_password.required' => 'Current password is required.', + 'current_password.current_password' => 'The current password is incorrect.', + 'new_password.required' => 'New password is required.', + 'new_password.confirmed' => 'Password confirmation does not match.', + 'new_password.different' => 'New password must be different from current password.', + ]); + + $user = Auth::user(); + $user->update([ + 'password' => Hash::make($this->new_password), + ]); + + // Reset password fields + $this->reset(['current_password', 'new_password', 'new_password_confirmation']); + + $this->success('Password updated successfully!', position: 'toast-top'); + } catch (\Illuminate\Validation\ValidationException $e) { + throw $e; + } catch (\Exception $e) { + Log::error('Failed to update password: ' . $e->getMessage()); + $this->error('Failed to update password. Please try again.', position: 'toast-top'); + } } public function updatePreferences() diff --git a/app/Livewire/Superadmin/Reports/Index.php b/app/Livewire/Superadmin/Reports/Index.php index cee0274..a6cc43a 100644 --- a/app/Livewire/Superadmin/Reports/Index.php +++ b/app/Livewire/Superadmin/Reports/Index.php @@ -11,6 +11,7 @@ use Illuminate\Support\Facades\DB; use Livewire\Attributes\Url; use Livewire\Attributes\Layout; +use Livewire\Attributes\Lazy; use Livewire\Attributes\Title; use Livewire\Attributes\Computed; use Livewire\Component; @@ -18,6 +19,7 @@ use Carbon\Carbon; use Barryvdh\DomPDF\Facade\Pdf; +#[Lazy] class Index extends Component { use Toast; @@ -25,6 +27,11 @@ class Index extends Component #[Title('Reports & Analytics - SuperAdmin')] #[Layout('components.layouts.superadmin')] + public function placeholder() + { + return view('livewire.superadmin.placeholders.reports'); + } + // Filters with URL state #[Url(except: '')] public $dateFrom; @@ -35,6 +42,12 @@ class Index extends Component public $selectedOffices = []; public $selectedEventTypes = []; + // Filter drawer visibility + public bool $showFilterDrawer = false; + + #[Url(except: '')] + public $searchTerm = ''; + #[Url(except: 'overview')] public $reportType = 'overview'; // overview, events, users, tickets @@ -52,11 +65,43 @@ public function mount() public function clearFilters() { - $this->reset(['selectedOffices', 'selectedEventTypes']); + $this->reset(['selectedOffices', 'selectedEventTypes', 'searchTerm']); $this->loadChartData(); $this->success('Filters cleared!', position: 'toast-top'); } + public function resetDateRange() + { + $this->dateTo = now()->format('Y-m-d'); + $this->dateFrom = now()->subDays(30)->format('Y-m-d'); + $this->loadChartData(); + } + + public function resetOffices() + { + $this->selectedOffices = []; + $this->loadChartData(); + } + + public function resetEventTypes() + { + $this->selectedEventTypes = []; + $this->loadChartData(); + } + + public function resetSearch() + { + $this->searchTerm = ''; + $this->loadChartData(); + } + + // MaryUI Chart configurations + public array $eventsByMonthChart = []; + public array $eventsByTypeChart = []; + public array $eventsByOfficeChart = []; + public array $ticketStatusChart = []; + public array $usersByRoleChart = []; + public function loadChartData() { $this->chartData = [ @@ -67,6 +112,170 @@ public function loadChartData() 'ticketStatusDistribution' => $this->getTicketStatusDistribution(), 'usersByRole' => $this->getUsersByRole(), ]; + + // Build MaryUI chart configurations + $this->buildChartConfigs(); + } + + protected function buildChartConfigs(): void + { + // Events by Month - Line Chart + $eventsByMonth = $this->chartData['eventsByMonth']; + $this->eventsByMonthChart = [ + 'type' => 'line', + 'data' => [ + 'labels' => $eventsByMonth['labels'] ?? [], + 'datasets' => [ + [ + 'label' => 'Events', + 'data' => $eventsByMonth['data'] ?? [], + 'borderColor' => 'rgb(79, 209, 197)', + 'backgroundColor' => 'rgba(79, 209, 197, 0.1)', + 'tension' => 0.4, + 'fill' => true, + 'pointBackgroundColor' => 'rgb(79, 209, 197)', + 'pointRadius' => 4, + ], + ], + ], + 'options' => [ + 'responsive' => true, + 'maintainAspectRatio' => false, + 'plugins' => [ + 'legend' => ['display' => false], + ], + 'scales' => [ + 'y' => ['beginAtZero' => true, 'grid' => ['display' => false]], + 'x' => ['grid' => ['display' => false]], + ], + ], + ]; + + // Events by Type - Doughnut Chart + $eventsByType = $this->chartData['eventsByType']; + $this->eventsByTypeChart = [ + 'type' => 'doughnut', + 'data' => [ + 'labels' => $eventsByType['labels'] ?? [], + 'datasets' => [ + [ + 'data' => $eventsByType['data'] ?? [], + 'backgroundColor' => [ + 'rgba(79, 209, 197, 0.8)', + 'rgba(99, 102, 241, 0.8)', + 'rgba(251, 191, 36, 0.8)', + 'rgba(248, 113, 113, 0.8)', + 'rgba(168, 85, 247, 0.8)', + 'rgba(34, 197, 94, 0.8)', + 'rgba(236, 72, 153, 0.8)', + 'rgba(14, 165, 233, 0.8)', + ], + 'borderWidth' => 0, + ], + ], + ], + 'options' => [ + 'responsive' => true, + 'maintainAspectRatio' => false, + 'plugins' => [ + 'legend' => [ + 'position' => 'bottom', + 'labels' => ['usePointStyle' => true, 'padding' => 20], + ], + ], + 'cutout' => '70%', + ], + ]; + + // Events by Office - Bar Chart + $eventsByOffice = $this->chartData['eventsByOffice']; + $this->eventsByOfficeChart = [ + 'type' => 'bar', + 'data' => [ + 'labels' => $eventsByOffice['labels'] ?? [], + 'datasets' => [ + [ + 'label' => 'Events', + 'data' => $eventsByOffice['data'] ?? [], + 'backgroundColor' => 'rgba(79, 209, 197, 0.8)', + 'borderRadius' => 6, + ], + ], + ], + 'options' => [ + 'responsive' => true, + 'maintainAspectRatio' => false, + 'plugins' => [ + 'legend' => ['display' => false], + ], + 'scales' => [ + 'y' => ['beginAtZero' => true, 'grid' => ['borderDash' => [2, 4]]], + 'x' => ['grid' => ['display' => false]], + ], + ], + ]; + + // Ticket Status Distribution - Pie Chart + $ticketStatus = $this->chartData['ticketStatusDistribution']; + $this->ticketStatusChart = [ + 'type' => 'pie', + 'data' => [ + 'labels' => $ticketStatus['labels'] ?? [], + 'datasets' => [ + [ + 'data' => $ticketStatus['data'] ?? [], + 'backgroundColor' => [ + 'rgba(34, 197, 94, 0.8)', + 'rgba(59, 130, 246, 0.8)', + 'rgba(251, 191, 36, 0.8)', + 'rgba(239, 68, 68, 0.8)', + 'rgba(168, 85, 247, 0.8)', + 'rgba(236, 72, 153, 0.8)', + ], + 'borderWidth' => 0, + ], + ], + ], + 'options' => [ + 'responsive' => true, + 'maintainAspectRatio' => false, + 'plugins' => [ + 'legend' => [ + 'position' => 'bottom', + 'labels' => ['usePointStyle' => true, 'padding' => 20], + ], + ], + ], + ]; + + // Users by Role - Horizontal Bar Chart + $usersByRole = $this->chartData['usersByRole']; + $this->usersByRoleChart = [ + 'type' => 'bar', + 'data' => [ + 'labels' => $usersByRole['labels'] ?? [], + 'datasets' => [ + [ + 'label' => 'Users', + 'data' => $usersByRole['data'] ?? [], + 'backgroundColor' => 'rgba(99, 102, 241, 0.8)', + 'borderRadius' => 6, + ], + ], + ], + 'options' => [ + 'responsive' => true, + 'maintainAspectRatio' => false, + 'indexAxis' => 'y', + 'plugins' => [ + 'legend' => ['display' => false], + ], + 'scales' => [ + 'x' => ['beginAtZero' => true, 'grid' => ['borderDash' => [2, 4]]], + 'y' => ['grid' => ['display' => false]], + ], + ], + ]; } protected function getOverviewData(): array @@ -176,6 +385,13 @@ protected function getBaseTicketQuery() }); } + if ($this->searchTerm) { + $query->where(function ($q) { + $q->where('tickets.ticket_number', 'like', '%' . $this->searchTerm . '%') + ->orWhere('tickets.title', 'like', '%' . $this->searchTerm . '%'); + }); + } + return $query; } @@ -199,6 +415,11 @@ public function updatedSelectedEventTypes() $this->loadChartData(); } + public function updatedSearchTerm() + { + $this->loadChartData(); + } + public function exportReport($format = 'csv') { try { @@ -225,13 +446,14 @@ protected function getDetailedReportData(): array return $query->with(['eventType', 'user.studentOrganization']) ->get() ->map(function ($ticket) { + $orgDeleted = $ticket->user?->studentOrganization?->trashed(); return [ 'Ticket Number' => $ticket->ticket_number, 'Event Title' => $ticket->title, 'Date From' => Carbon::parse($ticket->date_from)->format('M d, Y'), 'Date To' => Carbon::parse($ticket->date_to)->format('M d, Y'), 'Event Type' => $ticket->eventType->type_name ?? 'N/A', - 'Organization' => $ticket->user->studentOrganization->org_name ?? 'N/A', + 'Organization' => $orgDeleted ? 'Deleted Organization' : ($ticket->user?->studentOrganization?->org_name ?? 'N/A'), 'Status' => ucfirst(str_replace('_', ' ', $ticket->status)), 'Venue' => $ticket->venue_requested ?? 'N/A', 'Created At' => $ticket->created_at->format('M d, Y H:i:s'), diff --git a/app/Livewire/Superadmin/SystemSettings/ContentSectionEditor.php b/app/Livewire/Superadmin/SystemSettings/ContentSectionEditor.php new file mode 100644 index 0000000..787f5ff --- /dev/null +++ b/app/Livewire/Superadmin/SystemSettings/ContentSectionEditor.php @@ -0,0 +1,246 @@ +sectionId = $section->id; + $this->sectionKey = $section->section_key; + $this->title = $section->title; + $this->sectionType = $section->section_type; + $this->content = $section->content?->toHtml() ?? ''; + $this->isActive = $section->is_active; + $this->displayOrder = $section->display_order; + $this->targetRoles = $section->target_roles ?? []; + $this->isEditing = true; + $this->sectionKeyManuallyEdited = true; // Don't auto-update when editing + } + } + + public function render() + { + return view('livewire.superadmin.system-settings.content-section-editor', [ + 'sectionTypes' => ContentSection::getSectionTypes(), + 'roleOptions' => ContentSection::getTargetRoleOptions(), + ]); + } + + // Auto-generate section key from title (only if not manually edited) + public function updatedTitle($value) + { + if (!$this->isEditing && !$this->sectionKeyManuallyEdited) { + $this->sectionKey = Str::slug($value, '_'); + } + } + + // Track when user manually edits the section key + public function updatedSectionKey($value) + { + if (!$this->isEditing) { + // Mark as manually edited if user types something different from auto-generated + $this->sectionKeyManuallyEdited = $value !== Str::slug($this->title, '_'); + } + } + + public function save() + { + $rules = [ + 'sectionType' => 'required|string', + 'title' => 'required|string|max:255', + 'sectionKey' => 'required|string|max:100|regex:/^[a-z0-9_]+$/', + 'content' => 'nullable|string', + 'isActive' => 'boolean', + 'displayOrder' => 'integer|min:0', + 'targetRoles' => 'nullable|array', + 'targetRoles.*' => 'string|in:student-org,osa,gso,superadmin', + ]; + + // Add unique validation for section key + if ($this->isEditing) { + $rules['sectionKey'] .= '|unique:content_sections,section_key,' . $this->sectionId; + } else { + $rules['sectionKey'] .= '|unique:content_sections,section_key'; + } + + $this->validate($rules, [ + 'sectionKey.regex' => 'Section key can only contain lowercase letters, numbers, and underscores.', + 'sectionKey.unique' => 'This section key is already in use.', + ]); + + DB::beginTransaction(); + try { + if ($this->isEditing) { + $section = ContentSection::find($this->sectionId); + $originalTitle = $section->title; + $changes = []; + + if ($section->title !== $this->title) { + $changes[] = "Title: {$section->title} → {$this->title}"; + } + if ($section->section_type !== $this->sectionType) { + $changes[] = "Type changed"; + } + if (($section->content?->toHtml() ?? '') !== ($this->content ?? '')) { + $changes[] = "Content updated"; + } + if ($section->is_active !== $this->isActive) { + $changes[] = "Status: " . ($this->isActive ? 'Activated' : 'Deactivated'); + } + + $section->update([ + 'section_key' => $this->sectionKey, + 'section_type' => $this->sectionType, + 'title' => $this->title, + 'content' => $this->content, + 'is_active' => $this->isActive, + 'display_order' => $this->displayOrder, + 'target_roles' => !empty($this->targetRoles) ? $this->targetRoles : null, + ]); + + if (!empty($changes)) { + TransactionLogService::log( + 'content_section_updated', + "Updated content section: {$originalTitle} (ID: {$section->id}). Changes: " . implode(', ', $changes) + ); + } + + $this->clearCache(); + $this->success('Content section updated successfully!', position: 'toast-top'); + } else { + $section = ContentSection::create([ + 'section_key' => $this->sectionKey, + 'section_type' => $this->sectionType, + 'title' => $this->title, + 'content' => $this->content, + 'is_active' => $this->isActive, + 'display_order' => $this->displayOrder ?: (ContentSection::max('display_order') + 1), + 'target_roles' => !empty($this->targetRoles) ? $this->targetRoles : null, + ]); + + TransactionLogService::log( + 'content_section_created', + "Created content section: {$section->title} (ID: {$section->id})" + ); + + // Send notification to superadmins about content section creation + $superadmins = User::where('role_id', User::getRoleId('superadmin'))->get(); + foreach ($superadmins as $admin) { + $admin->notify(new \App\Notifications\SystemSettingsUpdatedNotification( + 'content_section', + $section->title, + 'created', + auth()->user() + )); + } + + // For announcements, also notify targeted users (in-app only, no email) + if ($this->sectionType === ContentSection::TYPE_ANNOUNCEMENT && $this->isActive) { + $this->notifyAnnouncementRecipients($section); + } + + $this->clearCache(); + $this->success('Content section created successfully!', position: 'toast-top'); + } + + DB::commit(); + + // Redirect back to system settings with content tab active + return $this->redirect(route('superadmin.system-settings', ['activeTab' => 'content']), navigate: true); + } catch (\Exception $e) { + DB::rollBack(); + \Log::error('Content section save failed', ['error' => $e->getMessage()]); + $this->error('Failed to save content section. Please try again.', position: 'toast-top'); + } + } + + public function cancel() + { + return $this->redirect(route('superadmin.system-settings', ['activeTab' => 'content']), navigate: true); + } + + protected function clearCache() + { + Cache::forget('content_sections'); + $sections = ContentSection::all(); + foreach ($sections as $section) { + Cache::forget("content_section_{$section->section_key}"); + Cache::forget("content_sections_type_{$section->section_type}"); + } + } + + /** + * Send in-app notifications to users based on announcement target roles + */ + protected function notifyAnnouncementRecipients(ContentSection $section): void + { + $targetRoles = $section->target_roles; + $currentUser = auth()->user(); + + // Build query for users to notify + $usersQuery = User::query(); + + if (!empty($targetRoles)) { + // Get role IDs for the target roles + $roleIds = []; + foreach ($targetRoles as $roleName) { + $roleId = User::getRoleId($roleName); + if ($roleId) { + $roleIds[] = $roleId; + } + } + + if (empty($roleIds)) { + return; // No valid roles found + } + + $usersQuery->whereIn('role_id', $roleIds); + } + + // Exclude the current user (they created the announcement) + $usersQuery->where('user_id', '!=', $currentUser->user_id); + + // Send notifications in chunks to avoid memory issues + $usersQuery->chunk(100, function ($users) use ($section, $currentUser) { + foreach ($users as $user) { + $user->notify(new AnnouncementNotification($section, $currentUser)); + } + }); + } +} diff --git a/app/Livewire/Superadmin/SystemSettings/ContentSectionManager.php b/app/Livewire/Superadmin/SystemSettings/ContentSectionManager.php new file mode 100644 index 0000000..2c33e72 --- /dev/null +++ b/app/Livewire/Superadmin/SystemSettings/ContentSectionManager.php @@ -0,0 +1,161 @@ + $this->getContentSections(), + 'sectionTypes' => ContentSection::getSectionTypes(), + ]); + } + + protected function getContentSections() + { + $query = ContentSection::query()->orderBy('display_order')->orderBy('created_at', 'desc'); + + if ($this->filterType) { + $query->where('section_type', $this->filterType); + } + + if ($this->search) { + $query->where(function ($q) { + $q->where('title', 'like', "%{$this->search}%") + ->orWhere('section_key', 'like', "%{$this->search}%"); + }); + } + + return $query->get(); + } + + // Navigate to create page + public function createSection() + { + return $this->redirect(route('superadmin.content-section.create'), navigate: true); + } + + // Navigate to edit page + public function editSection($id) + { + return $this->redirect(route('superadmin.content-section.edit', $id), navigate: true); + } + + public function toggleActive($id) + { + $section = ContentSection::find($id); + if ($section) { + $section->update(['is_active' => !$section->is_active]); + + $status = $section->is_active ? 'activated' : 'deactivated'; + TransactionLogService::log( + "content_section_{$status}", + "Content section '{$section->title}' (ID: {$section->id}) was {$status}" + ); + + $this->clearCache(); + $this->success("Content section {$status} successfully!", position: 'toast-top'); + } + } + + public function openDeleteModal($id) + { + $section = ContentSection::find($id); + if ($section) { + $this->deletingId = $id; + $this->deletingTitle = $section->title; + $this->deleteModalOpen = true; + } + } + + public function resetDeleteModal() + { + $this->reset(['deletingId', 'deletingTitle']); + $this->deleteModalOpen = false; + } + + public function confirmDelete() + { + $section = ContentSection::find($this->deletingId); + + if (!$section) { + $this->error('Content section not found!', position: 'toast-top'); + return; + } + + DB::beginTransaction(); + try { + TransactionLogService::log( + 'content_section_deleted', + "Deleted content section: {$section->title} (ID: {$section->id})" + ); + + $section->delete(); + + DB::commit(); + + $this->reset(['deletingId', 'deletingTitle']); + $this->deleteModalOpen = false; + $this->clearCache(); + $this->success('Content section deleted successfully!', position: 'toast-bottom'); + } catch (\Exception $e) { + DB::rollBack(); + \Log::error('Content section deletion failed', ['error' => $e->getMessage()]); + $this->error('Failed to delete content section. Please try again.', position: 'toast-top'); + } + } + + public function openPreview($id) + { + $this->previewSection = ContentSection::find($id); + $this->previewModalOpen = true; + } + + public function closePreview() + { + $this->previewSection = null; + $this->previewModalOpen = false; + } + + protected function clearCache() + { + Cache::forget('content_sections'); + // Clear individual section caches + $sections = ContentSection::all(); + foreach ($sections as $section) { + Cache::forget("content_section_{$section->section_key}"); + Cache::forget("content_sections_type_{$section->section_type}"); + } + $this->dispatch('cache-cleared'); + } + + #[On('refresh-cache')] + public function refreshCache() + { + $this->clearCache(); + } +} diff --git a/app/Livewire/Superadmin/SystemSettings/EventTypeEditor.php b/app/Livewire/Superadmin/SystemSettings/EventTypeEditor.php new file mode 100644 index 0000000..3e945e1 --- /dev/null +++ b/app/Livewire/Superadmin/SystemSettings/EventTypeEditor.php @@ -0,0 +1,140 @@ +eventTypeId = $eventType->event_type_id; + $this->typeName = $eventType->type_name; + $this->description = $eventType->description ?? ''; + $this->documentaryRequirements = $eventType->documentary_requirements?->toHtml() ?? ''; + $this->isEditing = true; + } + } + + public function render() + { + return view('livewire.superadmin.system-settings.event-type-editor'); + } + + public function save() + { + $rules = [ + 'typeName' => 'required|string|max:255', + 'description' => 'nullable|string|max:500', + 'documentaryRequirements' => 'nullable|string', + ]; + + // Add unique validation for type name + if ($this->isEditing) { + $rules['typeName'] .= '|unique:event__types,type_name,' . $this->eventTypeId . ',event_type_id'; + } else { + $rules['typeName'] .= '|unique:event__types,type_name'; + } + + $this->validate($rules, [ + 'typeName.required' => 'Event type name is required.', + 'typeName.unique' => 'This event type name already exists.', + ]); + + DB::beginTransaction(); + try { + if ($this->isEditing) { + $eventType = Event_Type::find($this->eventTypeId); + $originalName = $eventType->type_name; + $originalDescription = $eventType->description; + $originalRequirements = $eventType->documentary_requirements?->toHtml() ?? ''; + $changes = []; + + if ($originalName !== $this->typeName) { + $changes[] = "Name: {$originalName} → {$this->typeName}"; + } + if ($originalDescription !== $this->description) { + $changes[] = "Description updated"; + } + if ($originalRequirements !== $this->documentaryRequirements) { + $changes[] = "Documentary requirements updated"; + } + + $eventType->type_name = $this->typeName; + $eventType->description = $this->description ?: 'Event type description'; + $eventType->documentary_requirements = $this->documentaryRequirements; + $eventType->save(); + + if (!empty($changes)) { + TransactionLogService::logEventTypeOperation('updated', $eventType, $changes); + } + + DB::commit(); + $this->clearCache(); + $this->success('Event type updated successfully!', position: 'toast-bottom'); + } else { + $eventType = new Event_Type(); + $eventType->type_name = $this->typeName; + $eventType->description = $this->description ?: 'Event type description'; + $eventType->documentary_requirements = $this->documentaryRequirements; + $eventType->save(); + + TransactionLogService::logEventTypeOperation('created', $eventType); + + DB::commit(); + + // Send notification to superadmins + $superadmins = User::where('role_id', User::getRoleId('superadmin'))->get(); + foreach ($superadmins as $admin) { + $admin->notify(new \App\Notifications\SystemSettingsUpdatedNotification( + 'event_type', + $eventType->type_name, + 'created', + auth()->user() + )); + } + + $this->clearCache(); + $this->success('Event type created successfully!', position: 'toast-bottom'); + } + + return $this->redirect(route('superadmin.system-settings', ['activeTab' => 'event-types']), navigate: true); + } catch (\Exception $e) { + DB::rollBack(); + \Log::error('Event type operation failed', ['error' => $e->getMessage()]); + $this->error('Failed to save event type: ' . $e->getMessage(), position: 'toast-bottom'); + } + } + + public function cancel() + { + return $this->redirect(route('superadmin.system-settings', ['activeTab' => 'event-types']), navigate: true); + } + + protected function clearCache() + { + Cache::forget('event_types'); + } +} diff --git a/app/Livewire/Superadmin/SystemSettings/EventTypeManager.php b/app/Livewire/Superadmin/SystemSettings/EventTypeManager.php index 74d3700..bef440e 100644 --- a/app/Livewire/Superadmin/SystemSettings/EventTypeManager.php +++ b/app/Livewire/Superadmin/SystemSettings/EventTypeManager.php @@ -3,7 +3,6 @@ namespace App\Livewire\Superadmin\SystemSettings; use App\Models\Event_Type; -use App\Models\User; use App\Services\TransactionLogService; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; @@ -15,29 +14,10 @@ class EventTypeManager extends Component { use Toast; - // Add form data - public $newEventTypeName = ''; - - public $newEventTypeDescription = ''; - - public $addEventTypeModalOpen = false; - - // Edit form data - public $editingEventTypeId = null; - - public $eventTypeName = ''; - - public $eventTypeDescription = ''; - - public $editEventTypeModalOpen = false; - // Delete data public $deletingEventTypeId = null; - public $deletingEventTypeName = ''; - public $hasAssociatedEvents = false; - public $deleteModalOpen = false; // Cache duration @@ -57,114 +37,6 @@ protected function getEventTypes() }); } - public function addEventType() - { - $this->validate([ - 'newEventTypeName' => 'required|string|max:255|unique:event__types,type_name', - 'newEventTypeDescription' => 'nullable|string|max:500', - ]); - - DB::beginTransaction(); - try { - $eventType = Event_Type::create([ - 'type_name' => $this->newEventTypeName, - 'description' => $this->newEventTypeDescription ?? 'Event type description', - ]); - - // Log the event type creation - TransactionLogService::logEventTypeOperation('created', $eventType); - - DB::commit(); - - // Send notification after commit - $superadmins = User::where('role_id', User::getRoleId('superadmin'))->get(); - foreach ($superadmins as $admin) { - $admin->notify(new \App\Notifications\SystemSettingsUpdatedNotification( - 'event_type', - $eventType->type_name, - 'created', - auth()->user() - )); - } - - $this->reset(['newEventTypeName', 'newEventTypeDescription']); - $this->addEventTypeModalOpen = false; - $this->resetErrorBag(); - $this->clearEventTypesCache(); - $this->success('Event type added successfully!', position: 'toast-top'); - } catch (\Exception $e) { - DB::rollBack(); - \Log::error('Event type creation failed', ['error' => $e->getMessage()]); - $this->error('Failed to create event type: ' . $e->getMessage(), position: 'toast-top'); - } - } - - public function resetAddEventTypeForm() - { - $this->reset(['newEventTypeName', 'newEventTypeDescription']); - $this->addEventTypeModalOpen = false; - $this->resetErrorBag(); - } - - public function openEditModal($eventTypeId) - { - $eventType = Event_Type::find($eventTypeId); - if ($eventType) { - $this->editingEventTypeId = $eventTypeId; - $this->eventTypeName = $eventType->type_name; - $this->eventTypeDescription = $eventType->description ?? ''; - $this->editEventTypeModalOpen = true; - } - } - - public function editEventType() - { - $this->validate([ - 'eventTypeName' => 'required|string|max:255|unique:event__types,type_name,' . $this->editingEventTypeId . ',event_type_id', - 'eventTypeDescription' => 'nullable|string|max:500', - ]); - - $eventType = Event_Type::find($this->editingEventTypeId); - if ($eventType) { - $originalName = $eventType->type_name; - $originalDescription = $eventType->description; - $changes = []; - - if ($originalName !== $this->eventTypeName) { - $changes[] = "Name: {$originalName} → {$this->eventTypeName}"; - } - - if ($originalDescription !== $this->eventTypeDescription) { - $changes[] = "Description: {$originalDescription} → {$this->eventTypeDescription}"; - } - - $eventType->update([ - 'type_name' => $this->eventTypeName, - 'description' => $this->eventTypeDescription ?? 'Event type description', - ]); - - // Log the event type update with changes - if (! empty($changes)) { - TransactionLogService::logEventTypeOperation('updated', $eventType, $changes); - $this->success('Event type updated successfully!', position: 'toast-top'); - } else { - $this->info('Nothing Updated!', position: 'toast-top'); - } - - $this->reset(['editingEventTypeId', 'eventTypeName', 'eventTypeDescription']); - $this->editEventTypeModalOpen = false; - $this->resetErrorBag(); - $this->clearEventTypesCache(); - } - } - - public function resetEventTypeForm() - { - $this->reset(['editingEventTypeId', 'eventTypeName', 'eventTypeDescription']); - $this->editEventTypeModalOpen = false; - $this->resetErrorBag(); - } - public function openDeleteModal($eventTypeId) { $eventType = Event_Type::find($eventTypeId); @@ -186,7 +58,7 @@ public function confirmDelete() { $eventType = Event_Type::find($this->deletingEventTypeId); - if (! $eventType) { + if (!$eventType) { $this->error('Event type not found!', position: 'toast-top'); return; } diff --git a/app/Livewire/Superadmin/SystemSettings/Index.php b/app/Livewire/Superadmin/SystemSettings/Index.php index 3b2d5cf..5687045 100644 --- a/app/Livewire/Superadmin/SystemSettings/Index.php +++ b/app/Livewire/Superadmin/SystemSettings/Index.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Cache; use Livewire\Attributes\Layout; use Livewire\Attributes\Title; +use Livewire\Attributes\Url; use Livewire\Component; use Mary\Traits\Toast; @@ -17,6 +18,9 @@ class Index extends Component #[Layout('components.layouts.superadmin')] // Office Settings + #[Url] + public $activeTab = 'organizations'; + public $defaultOfficeId = ''; public $crossOfficeApprovals = false; @@ -24,6 +28,11 @@ class Index extends Component // Cache duration protected $cacheDuration = 10; + public function mount() + { + // #[Url] attribute on $activeTab handles query parameter binding automatically + } + public function render() { return view('livewire.superadmin.system-settings.index')->with([ @@ -75,6 +84,7 @@ public function refreshCache() Cache::forget('organizations'); Cache::forget('courses'); Cache::forget('offices'); + Cache::forget('venues'); Cache::forget('system_settings'); $this->dispatch('refresh-cache'); diff --git a/app/Livewire/Superadmin/SystemSettings/OrganizationManager.php b/app/Livewire/Superadmin/SystemSettings/OrganizationManager.php index 46051e4..7f009ee 100644 --- a/app/Livewire/Superadmin/SystemSettings/OrganizationManager.php +++ b/app/Livewire/Superadmin/SystemSettings/OrganizationManager.php @@ -69,8 +69,11 @@ class OrganizationManager extends Component public function render() { + $organizations = $this->getOrganizations(); + return view('livewire.superadmin.system-settings.organization-manager', [ - 'organizations' => $this->getOrganizations(), + 'academicOrgs' => $organizations->filter(fn($org) => $org->course_id !== null)->values(), + 'nonAcademicOrgs' => $organizations->filter(fn($org) => $org->course_id === null)->values(), 'allCourses' => $this->getCourses(), ]); } @@ -78,7 +81,7 @@ public function render() protected function getOrganizations() { return Cache::remember('organizations', $this->cacheDuration, function () { - return Student_Organization::with('course')->orderBy('org_name', 'desc')->get(); + return Student_Organization::with('course')->orderBy('org_name', 'asc')->get(); }); } diff --git a/app/Livewire/Superadmin/SystemSettings/VenueEditor.php b/app/Livewire/Superadmin/SystemSettings/VenueEditor.php new file mode 100644 index 0000000..6acf1ea --- /dev/null +++ b/app/Livewire/Superadmin/SystemSettings/VenueEditor.php @@ -0,0 +1,146 @@ +venueId = $venue->venue_id; + $this->venueName = $venue->venue_name; + $this->venueLocation = $venue->venue_location ?? ''; + $this->isActive = (bool) $venue->is_active; + $this->isEditing = true; + } + } + + public function render() + { + return view('livewire.superadmin.system-settings.venue-editor'); + } + + public function save() + { + $rules = [ + 'venueName' => 'required|string|max:255', + 'venueLocation' => 'nullable|string|max:500', + 'isActive' => 'boolean', + ]; + + // Add unique validation for venue name + if ($this->isEditing) { + $rules['venueName'] .= '|unique:venues,venue_name,' . $this->venueId . ',venue_id'; + } else { + $rules['venueName'] .= '|unique:venues,venue_name'; + } + + $this->validate($rules, [ + 'venueName.required' => 'Venue name is required.', + 'venueName.unique' => 'This venue name already exists.', + 'venueLocation.max' => 'Location must not exceed 500 characters.', + ]); + + DB::beginTransaction(); + try { + if ($this->isEditing) { + $venue = Venue::find($this->venueId); + $originalName = $venue->venue_name; + $originalLocation = $venue->venue_location; + $originalStatus = $venue->is_active; + $changes = []; + + if ($originalName !== $this->venueName) { + $changes[] = "Name: {$originalName} → {$this->venueName}"; + } + if ($originalLocation !== $this->venueLocation) { + $oldLocation = $originalLocation ?: '(empty)'; + $newLocation = $this->venueLocation ?: '(empty)'; + $changes[] = "Location: {$oldLocation} → {$newLocation}"; + } + if ((bool) $originalStatus !== $this->isActive) { + $oldStatus = $originalStatus ? 'Active' : 'Inactive'; + $newStatus = $this->isActive ? 'Active' : 'Inactive'; + $changes[] = "Status: {$oldStatus} → {$newStatus}"; + } + + $venue->venue_name = $this->venueName; + $venue->venue_location = $this->venueLocation ?: null; + $venue->is_active = $this->isActive; + $venue->save(); + + if (!empty($changes)) { + TransactionLogService::logVenueOperation('updated', $venue, $changes); + } + + DB::commit(); + $this->clearCache(); + $this->success('Venue updated successfully!', position: 'toast-bottom'); + } else { + $venue = new Venue(); + $venue->venue_name = $this->venueName; + $venue->venue_location = $this->venueLocation ?: null; + $venue->is_active = $this->isActive; + $venue->save(); + + TransactionLogService::logVenueOperation('created', $venue); + + DB::commit(); + + // Send notification to superadmins + $superadmins = User::where('role_id', User::getRoleId('superadmin'))->get(); + foreach ($superadmins as $admin) { + $admin->notify(new SystemSettingsUpdatedNotification( + 'venue', + $venue->venue_name, + 'created', + auth()->user() + )); + } + + $this->clearCache(); + $this->success('Venue created successfully!', position: 'toast-bottom'); + } + + return $this->redirect(route('superadmin.system-settings', ['activeTab' => 'venues']), navigate: true); + } catch (\Exception $e) { + DB::rollBack(); + \Log::error('Venue operation failed', ['error' => $e->getMessage()]); + $this->error('Failed to save venue: ' . $e->getMessage(), position: 'toast-bottom'); + } + } + + public function cancel() + { + return $this->redirect(route('superadmin.system-settings', ['activeTab' => 'venues']), navigate: true); + } + + protected function clearCache() + { + Cache::forget('venues'); + } +} diff --git a/app/Livewire/Superadmin/SystemSettings/VenueManager.php b/app/Livewire/Superadmin/SystemSettings/VenueManager.php new file mode 100644 index 0000000..7816a5d --- /dev/null +++ b/app/Livewire/Superadmin/SystemSettings/VenueManager.php @@ -0,0 +1,139 @@ + $this->getVenues(), + ]); + } + + protected function getVenues() + { + return Cache::remember('venues', $this->cacheDuration, function () { + return Venue::orderBy('created_at', 'desc')->get(); + }); + } + + public function openDeleteModal($venueId) + { + $venue = Venue::find($venueId); + if ($venue) { + $this->deletingVenueId = $venueId; + $this->deletingVenueName = $venue->venue_name; + $this->associatedTicketsCount = $venue->tickets()->count(); + $this->hasAssociatedTickets = $this->associatedTicketsCount > 0; + $this->deleteModalOpen = true; + } + } + + public function resetDeleteModal() + { + $this->reset(['deletingVenueId', 'deletingVenueName', 'hasAssociatedTickets', 'associatedTicketsCount']); + $this->deleteModalOpen = false; + } + + public function confirmDelete() + { + $venue = Venue::find($this->deletingVenueId); + + if (!$venue) { + $this->error('Venue not found!', position: 'toast-top'); + return; + } + + // Block deletion if venue has associated tickets + if ($venue->tickets()->count() > 0) { + $this->error('Cannot delete venue that is being used by tickets! Consider deactivating it instead.', position: 'toast-top'); + return; + } + + DB::beginTransaction(); + try { + // Log the venue deletion before deleting + TransactionLogService::logVenueOperation('deleted', $venue); + + $venue->delete(); + + DB::commit(); + + $this->reset(['deletingVenueId', 'deletingVenueName', 'hasAssociatedTickets', 'associatedTicketsCount']); + $this->deleteModalOpen = false; + $this->clearVenuesCache(); + $this->success('Venue deleted successfully!', position: 'toast-top'); + } catch (\Exception $e) { + DB::rollBack(); + \Log::error('Venue deletion failed', ['error' => $e->getMessage()]); + $this->error('Failed to delete venue: ' . $e->getMessage(), position: 'toast-top'); + } + } + + /** + * Toggle venue active status + * Deactivating only prevents new ticket submissions, doesn't affect existing tickets + */ + public function toggleActive($venueId) + { + $venue = Venue::find($venueId); + + if (!$venue) { + $this->error('Venue not found!', position: 'toast-top'); + return; + } + + DB::beginTransaction(); + try { + $previousStatus = $venue->is_active ? 'active' : 'inactive'; + $venue->is_active = !$venue->is_active; + $venue->save(); + + $newStatus = $venue->is_active ? 'active' : 'inactive'; + TransactionLogService::logVenueOperation('updated', $venue, ["Status: {$previousStatus} → {$newStatus}"]); + + DB::commit(); + + $this->clearVenuesCache(); + $statusLabel = $venue->is_active ? 'activated' : 'deactivated'; + $this->success("Venue {$statusLabel} successfully!", position: 'toast-top'); + } catch (\Exception $e) { + DB::rollBack(); + \Log::error('Venue status toggle failed', ['error' => $e->getMessage()]); + $this->error('Failed to update venue status: ' . $e->getMessage(), position: 'toast-top'); + } + } + + protected function clearVenuesCache() + { + Cache::forget('venues'); + $this->dispatch('cache-cleared'); + } + + #[On('refresh-cache')] + public function refreshCache() + { + Cache::forget('venues'); + } +} diff --git a/app/Livewire/Superadmin/Users/Index.php b/app/Livewire/Superadmin/Users/Index.php index 8bcffa7..771dfa8 100644 --- a/app/Livewire/Superadmin/Users/Index.php +++ b/app/Livewire/Superadmin/Users/Index.php @@ -15,12 +15,14 @@ use Illuminate\Support\Facades\Hash; use Livewire\Attributes\Computed; use Livewire\Attributes\Layout; +use Livewire\Attributes\Lazy; use Livewire\Attributes\Title; use Livewire\Attributes\Url; use Livewire\Component; use Livewire\WithPagination; use Mary\Traits\Toast; +#[Lazy] class Index extends Component { use Toast, WithPagination; @@ -28,6 +30,11 @@ class Index extends Component #[Title('Superadmin - User Management')] #[Layout('components.layouts.superadmin')] + public function placeholder() + { + return view('livewire.superadmin.placeholders.users'); + } + // Search and filter properties with URL state #[Url(except: '')] public $search = ''; @@ -268,13 +275,13 @@ public function createUser() // Reset and close $this->createForm->reset(); $this->dispatch('user-drawer-close'); - $this->success('User created successfully!', position: 'toast-top'); + $this->success('User created successfully!', position: 'toast-bottom'); } catch (\Exception $e) { DB::rollBack(); \Log::error('User creation failed', [ 'error' => $e->getMessage(), ]); - $this->error('Failed to create user: ' . $e->getMessage(), position: 'toast-top'); + $this->error('Failed to create user: ' . $e->getMessage(), position: 'toast-bottom'); } } diff --git a/app/Models/ContentSection.php b/app/Models/ContentSection.php new file mode 100644 index 0000000..a856fd0 --- /dev/null +++ b/app/Models/ContentSection.php @@ -0,0 +1,193 @@ + + */ + protected $richTextAttributes = [ + 'content', + ]; + + protected $fillable = [ + 'section_key', + 'section_type', + 'title', + 'content', + 'is_active', + 'display_order', + 'target_roles', + ]; + + protected $casts = [ + 'is_active' => 'boolean', + 'target_roles' => 'array', + ]; + + // Section type constants + const TYPE_ANNOUNCEMENT = 'announcement'; + const TYPE_TERMS_CONDITIONS = 'terms_conditions'; + const TYPE_DOCUMENTARY_REQUIREMENTS = 'documentary_requirements'; + const TYPE_FAQ = 'faq'; + const TYPE_PAGE_CONTENT = 'page_content'; + + /** + * Get all available section types with user-friendly labels + */ + public static function getSectionTypes(): array + { + return [ + ['id' => self::TYPE_ANNOUNCEMENT, 'name' => 'Announcement'], + ['id' => self::TYPE_TERMS_CONDITIONS, 'name' => 'Terms & Conditions'], + ['id' => self::TYPE_DOCUMENTARY_REQUIREMENTS, 'name' => 'Documentary Requirements'], + ['id' => self::TYPE_FAQ, 'name' => 'FAQ'], + ['id' => self::TYPE_PAGE_CONTENT, 'name' => 'Page Content'], + ]; + } + + /** + * Get section type label + */ + public function getTypeLabelAttribute(): string + { + $types = collect(self::getSectionTypes()); + $type = $types->firstWhere('id', $this->section_type); + return $type ? $type['name'] : ucfirst(str_replace('_', ' ', $this->section_type)); + } + + /** + * Get section type icon + */ + public function getTypeIconAttribute(): string + { + return match ($this->section_type) { + self::TYPE_ANNOUNCEMENT => 'o-megaphone', + self::TYPE_TERMS_CONDITIONS => 'o-document-check', + self::TYPE_DOCUMENTARY_REQUIREMENTS => 'o-clipboard-document-list', + self::TYPE_FAQ => 'o-question-mark-circle', + self::TYPE_PAGE_CONTENT => 'o-document-text', + default => 'o-document', + }; + } + + /** + * Get section type color for badges + */ + public function getTypeColorAttribute(): string + { + return match ($this->section_type) { + self::TYPE_ANNOUNCEMENT => 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', + self::TYPE_TERMS_CONDITIONS => 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400', + self::TYPE_DOCUMENTARY_REQUIREMENTS => 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', + self::TYPE_FAQ => 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400', + self::TYPE_PAGE_CONTENT => 'bg-slate-100 text-slate-700 dark:bg-slate-900/30 dark:text-slate-400', + default => 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400', + }; + } + + /** + * Cached retrieval by section key + */ + public static function getByKey(string $key): ?self + { + return Cache::remember("content_section_{$key}", 300, function () use ($key) { + return static::where('section_key', $key) + ->where('is_active', true) + ->first(); + }); + } + + /** + * Get all active sections of a specific type + */ + public static function getActiveByType(string $type): \Illuminate\Database\Eloquent\Collection + { + return Cache::remember("content_sections_type_{$type}", 300, function () use ($type) { + return static::where('section_type', $type) + ->where('is_active', true) + ->orderBy('display_order') + ->get(); + }); + } + + /** + * Get available target role options for announcements + */ + public static function getTargetRoleOptions(): array + { + return [ + ['id' => 'student-org', 'name' => 'Student Organizations'], + ['id' => 'osa', 'name' => 'OSA Staff'], + ['id' => 'gso', 'name' => 'GSO Staff'], + ['id' => 'superadmin', 'name' => 'Superadmins'], + ]; + } + + /** + * Check if this content section should be visible to a specific user + * Returns true if: + * - target_roles is null/empty (show to all users) + * - The user's role is in the target_roles array + * - The user is a guest and target_roles is null/empty + */ + public function isVisibleToUser(?User $user): bool + { + // If no target roles specified, show to everyone + if (empty($this->target_roles)) { + return true; + } + + // If user is not logged in, don't show role-targeted announcements + if (!$user) { + return false; + } + + // Load role if not already loaded + if (!$user->relationLoaded('role')) { + $user->load('role'); + } + + $userRole = $user->role?->role_name; + + // Check if user's role is in target roles + return in_array($userRole, $this->target_roles); + } + + /** + * Get active sections of a specific type, filtered by user role + */ + public static function getActiveByTypeForUser(string $type, ?User $user): \Illuminate\Support\Collection + { + return static::getActiveByType($type)->filter(function ($section) use ($user) { + return $section->isVisibleToUser($user); + }); + } + + /** + * Clear cache when updated + */ + protected static function booted() + { + static::saved(function ($section) { + Cache::forget("content_section_{$section->section_key}"); + Cache::forget("content_sections_type_{$section->section_type}"); + Cache::forget('content_sections'); + }); + + static::deleted(function ($section) { + Cache::forget("content_section_{$section->section_key}"); + Cache::forget("content_sections_type_{$section->section_type}"); + Cache::forget('content_sections'); + }); + } +} diff --git a/app/Models/Event_Type.php b/app/Models/Event_Type.php index c9ddc2d..7cc9eec 100644 --- a/app/Models/Event_Type.php +++ b/app/Models/Event_Type.php @@ -4,11 +4,20 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Tonysm\RichTextLaravel\Models\Traits\HasRichText; class Event_Type extends Model { /** @use HasFactory<\Database\Factories\EventTypeFactory> */ use HasFactory; + use HasRichText; + + /** + * The rich text attributes. + */ + protected $richTextAttributes = [ + 'documentary_requirements', + ]; /** * The table associated with the model. diff --git a/app/Models/Faq.php b/app/Models/Faq.php new file mode 100644 index 0000000..c93eb54 --- /dev/null +++ b/app/Models/Faq.php @@ -0,0 +1,111 @@ + 'boolean', + 'display_order' => 'integer', + ]; + + /** + * Cache duration in seconds + */ + protected static int $cacheDuration = 300; + + /** + * Scope for active FAQs only + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + /** + * Scope for ordering by display_order then created_at + */ + public function scopeOrdered($query) + { + return $query->orderBy('display_order')->orderBy('created_at'); + } + + /** + * Get all active FAQs grouped by category with caching + */ + public static function getActiveGroupedByCategory(): \Illuminate\Support\Collection + { + return Cache::remember('faqs_grouped_by_category', static::$cacheDuration, function () { + return static::active() + ->ordered() + ->get() + ->groupBy(function ($faq) { + return $faq->category ?: 'General'; + }); + }); + } + + /** + * Get all active FAQs with caching + */ + public static function getActive(): \Illuminate\Database\Eloquent\Collection + { + return Cache::remember('faqs_active', static::$cacheDuration, function () { + return static::active()->ordered()->get(); + }); + } + + /** + * Get all unique categories + */ + public static function getCategories(): array + { + return Cache::remember('faqs_categories', static::$cacheDuration, function () { + return static::whereNotNull('category') + ->distinct() + ->pluck('category') + ->filter() + ->values() + ->toArray(); + }); + } + + /** + * Clear all FAQ-related caches + */ + public static function clearCache(): void + { + Cache::forget('faqs_grouped_by_category'); + Cache::forget('faqs_active'); + Cache::forget('faqs_categories'); + Cache::forget('faqs_all'); + } + + /** + * Boot method to clear cache on model events + */ + protected static function booted(): void + { + static::saved(function () { + static::clearCache(); + }); + + static::deleted(function () { + static::clearCache(); + }); + } +} diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php index 6d64c14..93aae0e 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -71,10 +71,12 @@ class Ticket extends Model /** * User who created this ticket + * Note: withTrashed() ensures soft-deleted users are still loaded + * so tickets remain visible even after the user is deleted */ public function user() { - return $this->belongsTo(User::class, 'user_id'); + return $this->belongsTo(User::class, 'user_id')->withTrashed(); } /** diff --git a/app/Models/User.php b/app/Models/User.php index 4c6414e..e2602c1 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -43,6 +43,7 @@ class User extends Authenticatable implements MustVerifyEmail 'phone', 'org_id', 'office_id', + 'position_id', 'avatar', 'avatar_style', 'avatar_seed', @@ -170,10 +171,12 @@ public function getDashboardRoute(): string /** * Relationship to Student Organization + * Note: withTrashed() ensures soft-deleted organizations are still loaded + * so user data remains visible even after the organization is deleted */ public function studentOrganization() { - return $this->belongsTo(Student_Organization::class, 'org_id'); + return $this->belongsTo(Student_Organization::class, 'org_id')->withTrashed(); } public function role() diff --git a/app/Notifications/AnnouncementNotification.php b/app/Notifications/AnnouncementNotification.php new file mode 100644 index 0000000..76b27e6 --- /dev/null +++ b/app/Notifications/AnnouncementNotification.php @@ -0,0 +1,53 @@ +announcement = $announcement; + $this->createdBy = $createdBy; + } + + /** + * Get the notification's delivery channels. + * Only database (in-app) notifications, no email + */ + public function via($notifiable): array + { + return ['database']; + } + + /** + * Get the array representation of the notification for database storage. + */ + public function toDatabase($notifiable): array + { + return [ + 'type' => 'announcement', + 'title' => 'New Announcement', + 'message' => $this->announcement->title, + 'content' => strip_tags($this->announcement->content?->toHtml() ?? ''), + 'color' => 'info', + 'announcement_id' => $this->announcement->id, + 'announcement_key' => $this->announcement->section_key, + 'created_by' => $this->createdBy->name, + 'created_by_id' => $this->createdBy->user_id, + 'timestamp' => now()->toDateTimeString(), + ]; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index de8a94a..8cf9483 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,10 +2,13 @@ namespace App\Providers; +use App\View\Html\Sanitizer; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; +use Symfony\Component\HtmlSanitizer\HtmlSanitizer; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; class AppServiceProvider extends ServiceProvider { @@ -14,7 +17,33 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + // Register the HTML Sanitizer for XSS protection + $this->app->scoped(Sanitizer::class, function () { + return new Sanitizer(new HtmlSanitizer( + (new HtmlSanitizerConfig) + ->allowSafeElements() + // Explicitly allow elements used by Trix editor + ->allowElement('div') + ->allowElement('p') + ->allowElement('br') + ->allowElement('ul') + ->allowElement('ol') + ->allowElement('li') + ->allowElement('strong') + ->allowElement('em') + ->allowElement('del') + ->allowElement('a', ['href', 'target', 'rel']) + ->allowElement('blockquote') + ->allowElement('pre') + ->allowElement('h1') + ->allowElement('h2') + ->allowElement('h3') + ->allowElement('code') + ->allowAttribute('class', '*') + ->allowAttribute('style', '*') + ->allowAttribute('id', '*') + )); + }); } /** diff --git a/app/Services/TransactionLogService.php b/app/Services/TransactionLogService.php index a156c5e..afedffa 100644 --- a/app/Services/TransactionLogService.php +++ b/app/Services/TransactionLogService.php @@ -50,15 +50,15 @@ public static function logUserOperation(string $operation, User $user, array $ch { $details = match ($operation) { 'created' => "Created user account: {$user->name} ({$user->email})", - 'updated' => "Updated user account: {$user->name} ({$user->email})". - (empty($changes) ? '' : ' - Changes: '.implode(', ', $changes)), + 'updated' => "Updated user account: {$user->name} ({$user->email})" . + (empty($changes) ? '' : ' - Changes: ' . implode(', ', $changes)), 'deleted' => "Deleted user account: {$user->name} ({$user->email})", 'password_reset' => "Reset password for user: {$user->name} ({$user->email})", 'role_changed' => "Changed role for user: {$user->name} ({$user->email})", default => "User operation: {$operation} on {$user->name} ({$user->email})" }; - self::log('USER_'.strtoupper($operation), $details); + self::log('USER_' . strtoupper($operation), $details); } /** @@ -70,13 +70,13 @@ public static function logEventTypeOperation(string $operation, $eventType, arra $details = match ($operation) { 'created' => "Created event type: {$eventTypeName}", - 'updated' => "Updated event type: {$eventTypeName}". - (empty($changes) ? '' : ' - Changes: '.implode(', ', $changes)), + 'updated' => "Updated event type: {$eventTypeName}" . + (empty($changes) ? '' : ' - Changes: ' . implode(', ', $changes)), 'deleted' => "Deleted event type: {$eventTypeName}", default => "Event type operation: {$operation} on {$eventTypeName}" }; - self::log('EVENT_TYPE_'.strtoupper($operation), $details); + self::log('EVENT_TYPE_' . strtoupper($operation), $details); } /** @@ -88,13 +88,13 @@ public static function logOrgOperation(string $operation, $org, array $changes = $details = match ($operation) { 'created' => "Created student organization: {$orgName}", - 'updated' => "Updated student organization: {$orgName}". - (empty($changes) ? '' : ' - Changes: '.implode(', ', $changes)), + 'updated' => "Updated student organization: {$orgName}" . + (empty($changes) ? '' : ' - Changes: ' . implode(', ', $changes)), 'deleted' => "Deleted student organization: {$orgName}", default => "Student organization operation: {$operation} on {$orgName}" }; - self::log('STUDENT_ORG_'.strtoupper($operation), $details); + self::log('STUDENT_ORG_' . strtoupper($operation), $details); } /** @@ -107,26 +107,26 @@ public static function logTicketOperation(string $operation, $ticket, array $cha $details = match ($operation) { 'created' => "Created ticket: {$ticketTitle} ({$ticketNumber})", - 'updated' => "Updated ticket: {$ticketTitle} ({$ticketNumber})". - (empty($changes) ? '' : ' - Changes: '.implode(', ', $changes)), + 'updated' => "Updated ticket: {$ticketTitle} ({$ticketNumber})" . + (empty($changes) ? '' : ' - Changes: ' . implode(', ', $changes)), 'amended' => "Amended ticket: {$ticketTitle} ({$ticketNumber}) - Resubmitted after revision", - 'approved' => "Approved ticket: {$ticketTitle} ({$ticketNumber})". - (empty($changes) ? '' : ' - '.implode(', ', $changes)), - 'forwarded' => "Forwarded ticket: {$ticketTitle} ({$ticketNumber}) to GSO". - (empty($changes) ? '' : ' - '.implode(', ', $changes)), - 'for_revision' => "Requested revision for ticket: {$ticketTitle} ({$ticketNumber})". - (empty($changes) ? '' : ' - '.implode(', ', $changes)), - 'final_approved' => "Final approval for ticket: {$ticketTitle} ({$ticketNumber}) after GSO review". - (empty($changes) ? '' : ' - '.implode(', ', $changes)), - 'final_for_revision' => "Final rejection for ticket: {$ticketTitle} ({$ticketNumber}) after GSO review". - (empty($changes) ? '' : ' - '.implode(', ', $changes)), - 'completed' => "Completed ticket: {$ticketTitle} ({$ticketNumber})". - (empty($changes) ? '' : ' - '.implode(', ', array_map(fn ($k, $v) => "{$k}: {$v}", array_keys($changes), $changes))), + 'approved' => "Approved ticket: {$ticketTitle} ({$ticketNumber})" . + (empty($changes) ? '' : ' - ' . implode(', ', $changes)), + 'forwarded' => "Forwarded ticket: {$ticketTitle} ({$ticketNumber}) to GSO" . + (empty($changes) ? '' : ' - ' . implode(', ', $changes)), + 'for_revision' => "Requested revision for ticket: {$ticketTitle} ({$ticketNumber})" . + (empty($changes) ? '' : ' - ' . implode(', ', $changes)), + 'final_approved' => "Final approval for ticket: {$ticketTitle} ({$ticketNumber}) after GSO review" . + (empty($changes) ? '' : ' - ' . implode(', ', $changes)), + 'final_for_revision' => "Final rejection for ticket: {$ticketTitle} ({$ticketNumber}) after GSO review" . + (empty($changes) ? '' : ' - ' . implode(', ', $changes)), + 'completed' => "Completed ticket: {$ticketTitle} ({$ticketNumber})" . + (empty($changes) ? '' : ' - ' . implode(', ', array_map(fn($k, $v) => "{$k}: {$v}", array_keys($changes), $changes))), 'deleted' => "Deleted ticket: {$ticketTitle} ({$ticketNumber})", default => "Ticket operation: {$operation} on {$ticketTitle} ({$ticketNumber})" }; - self::log('TICKET_'.strtoupper($operation), $details); + self::log('TICKET_' . strtoupper($operation), $details); } /** @@ -145,7 +145,7 @@ public static function logAuthEvent(string $event, ?User $user = null, string $a default => "Auth event: {$event} for {$userInfo}" }; - self::log('AUTH_'.strtoupper($event), $details); + self::log('AUTH_' . strtoupper($event), $details); } /** @@ -154,7 +154,7 @@ public static function logAuthEvent(string $event, ?User $user = null, string $a public static function logSystemOperation(string $operation, string $details = ''): void { $fullDetails = empty($details) ? "System operation: {$operation}" : $details; - self::log('SYSTEM_'.strtoupper($operation), $fullDetails); + self::log('SYSTEM_' . strtoupper($operation), $fullDetails); } /** @@ -166,13 +166,13 @@ public static function logOfficeOperation(string $operation, $office, array $cha $details = match ($operation) { 'created' => "Created office: {$officeName}", - 'updated' => "Updated office: {$officeName}". - (empty($changes) ? '' : ' - Changes: '.implode(', ', $changes)), + 'updated' => "Updated office: {$officeName}" . + (empty($changes) ? '' : ' - Changes: ' . implode(', ', $changes)), 'deleted' => "Deleted office: {$officeName}", default => "Office operation: {$operation} on {$officeName}" }; - self::log('OFFICE_'.strtoupper($operation), $details); + self::log('OFFICE_' . strtoupper($operation), $details); } /** @@ -184,14 +184,14 @@ public static function logOfficeApproval(string $officeName, string $action, $ti $ticketNumber = is_object($ticket) ? $ticket->ticket_number : ''; $details = match ($action) { - 'approved' => "{$officeName} approved ticket: {$ticketTitle} ({$ticketNumber})". - (empty($changes) ? '' : ' - '.implode(', ', $changes)), - 'for_revision' => "{$officeName} for revision ticket: {$ticketTitle} ({$ticketNumber})". - (empty($changes) ? '' : ' - '.implode(', ', $changes)), + 'approved' => "{$officeName} approved ticket: {$ticketTitle} ({$ticketNumber})" . + (empty($changes) ? '' : ' - ' . implode(', ', $changes)), + 'for_revision' => "{$officeName} for revision ticket: {$ticketTitle} ({$ticketNumber})" . + (empty($changes) ? '' : ' - ' . implode(', ', $changes)), default => "{$officeName} {$action} on ticket: {$ticketTitle} ({$ticketNumber})" }; - self::log('OFFICE_'.strtoupper($action), $details); + self::log('OFFICE_' . strtoupper($action), $details); } public static function logCourseOperation(string $operation, $course, array $changes = []): void @@ -200,13 +200,31 @@ public static function logCourseOperation(string $operation, $course, array $cha $details = match ($operation) { 'created' => "Created course: {$courseName}", - 'updated' => "Updated course: {$courseName}". - (empty($changes) ? '' : ' - Changes: '.implode(', ', $changes)), + 'updated' => "Updated course: {$courseName}" . + (empty($changes) ? '' : ' - Changes: ' . implode(', ', $changes)), 'deleted' => "Deleted course: {$courseName}", default => "Course operation: {$operation} on {$courseName}" }; - self::log('COURSE_'.strtoupper($operation), $details); + self::log('COURSE_' . strtoupper($operation), $details); + } + + /** + * Log venue operations + */ + public static function logVenueOperation(string $operation, $venue, array $changes = []): void + { + $venueName = is_object($venue) ? $venue->venue_name : $venue; + + $details = match ($operation) { + 'created' => "Created venue: {$venueName}", + 'updated' => "Updated venue: {$venueName}" . + (empty($changes) ? '' : ' - Changes: ' . implode(', ', $changes)), + 'deleted' => "Deleted venue: {$venueName}", + default => "Venue operation: {$operation} on {$venueName}" + }; + + self::log('VENUE_' . strtoupper($operation), $details); } /** diff --git a/app/Traits/WithProfilePhoto.php b/app/Traits/WithProfilePhoto.php index 6e669e5..abe81b9 100644 --- a/app/Traits/WithProfilePhoto.php +++ b/app/Traits/WithProfilePhoto.php @@ -31,7 +31,7 @@ protected function photoValidationRules(): array 'nullable', 'image', 'mimes:jpg,jpeg,png,webp', - 'max:10240', // 10MB max + 'max:5120', // 5MB max ], ]; } @@ -44,7 +44,7 @@ protected function photoValidationMessages(): array return [ 'photo.image' => 'The file must be an image.', 'photo.mimes' => 'The image must be a JPG, PNG, or WebP file.', - 'photo.max' => 'The image must not exceed 10MB.', + 'photo.max' => 'The image must not exceed 5MB.', ]; } diff --git a/app/View/Html/Sanitizer.php b/app/View/Html/Sanitizer.php new file mode 100644 index 0000000..c27f7a7 --- /dev/null +++ b/app/View/Html/Sanitizer.php @@ -0,0 +1,33 @@ +sanitizer->sanitizeFor($element, $input); + } + + /** + * Sanitize HTML content. + * + * @param string $input The HTML input to sanitize + * @return string + */ + public function sanitize(string $input): string + { + return $this->sanitizer->sanitize($input); + } +} diff --git a/app/helpers.php b/app/helpers.php new file mode 100644 index 0000000..e785106 --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,24 @@ +sanitizeFor($element, $html)); + } +} diff --git a/composer.json b/composer.json index 936bb88..723130c 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,9 @@ "livewire/volt": "^1.7", "owenvoke/blade-fontawesome": "^2.9", "resend/resend-laravel": "^1.1", - "robsontenorio/mary": "^2.4" + "robsontenorio/mary": "^2.4", + "symfony/html-sanitizer": "^8.0", + "tonysm/rich-text-laravel": "^3.4" }, "require-dev": { "barryvdh/laravel-debugbar": "^3.16", @@ -40,7 +42,10 @@ "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" - } + }, + "files": [ + "app/helpers.php" + ] }, "autoload-dev": { "psr-4": { diff --git a/composer.lock b/composer.lock index 07ec468..3a2ed31 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4257767c53670b373a6cd59b5e8c269d", + "content-hash": "dc35b362e8dddce2a8e6b271e4fed159", "packages": [ { "name": "barryvdh/laravel-dompdf", @@ -154,16 +154,16 @@ }, { "name": "blade-ui-kit/blade-icons", - "version": "1.8.0", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/driesvints/blade-icons.git", - "reference": "7b743f27476acb2ed04cb518213d78abe096e814" + "reference": "47e7b6f43250e6404e4224db8229219cd42b543c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/driesvints/blade-icons/zipball/7b743f27476acb2ed04cb518213d78abe096e814", - "reference": "7b743f27476acb2ed04cb518213d78abe096e814", + "url": "https://api.github.com/repos/driesvints/blade-icons/zipball/47e7b6f43250e6404e4224db8229219cd42b543c", + "reference": "47e7b6f43250e6404e4224db8229219cd42b543c", "shasum": "" }, "require": { @@ -210,7 +210,7 @@ } ], "description": "A package to easily make use of icons in your Laravel Blade views.", - "homepage": "https://github.com/blade-ui-kit/blade-icons", + "homepage": "https://github.com/driesvints/blade-icons", "keywords": [ "blade", "icons", @@ -218,8 +218,8 @@ "svg" ], "support": { - "issues": "https://github.com/blade-ui-kit/blade-icons/issues", - "source": "https://github.com/blade-ui-kit/blade-icons" + "issues": "https://github.com/driesvints/blade-icons/issues", + "source": "https://github.com/driesvints/blade-icons" }, "funding": [ { @@ -231,7 +231,7 @@ "type": "paypal" } ], - "time": "2025-02-13T20:35:06+00:00" + "time": "2026-01-20T09:46:32+00:00" }, { "name": "brick/math", @@ -670,16 +670,16 @@ }, { "name": "dompdf/php-font-lib", - "version": "1.0.1", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/dompdf/php-font-lib.git", - "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d" + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", - "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a", + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a", "shasum": "" }, "require": { @@ -687,7 +687,7 @@ "php": "^7.1 || ^8.0" }, "require-dev": { - "symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6" + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12" }, "type": "library", "autoload": { @@ -709,9 +709,9 @@ "homepage": "https://github.com/dompdf/php-font-lib", "support": { "issues": "https://github.com/dompdf/php-font-lib/issues", - "source": "https://github.com/dompdf/php-font-lib/tree/1.0.1" + "source": "https://github.com/dompdf/php-font-lib/tree/1.0.2" }, - "time": "2024-12-02T14:37:59+00:00" + "time": "2026-01-20T14:10:26+00:00" }, { "name": "dompdf/php-svg-lib", @@ -1901,16 +1901,16 @@ }, { "name": "laravel/framework", - "version": "v12.46.0", + "version": "v12.49.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae" + "reference": "4bde4530545111d8bdd1de6f545fa8824039fcb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/9dcff48d25a632c1fadb713024c952fec489c4ae", - "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae", + "url": "https://api.github.com/repos/laravel/framework/zipball/4bde4530545111d8bdd1de6f545fa8824039fcb5", + "reference": "4bde4530545111d8bdd1de6f545fa8824039fcb5", "shasum": "" }, "require": { @@ -2023,7 +2023,7 @@ "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", "opis/json-schema": "^2.4.1", - "orchestra/testbench-core": "^10.8.1", + "orchestra/testbench-core": "^10.9.0", "pda/pheanstalk": "^5.0.6|^7.0.0", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", @@ -2119,20 +2119,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-01-07T23:26:53+00:00" + "time": "2026-01-28T03:40:49+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.8", + "version": "v0.3.11", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "096748cdfb81988f60090bbb839ce3205ace0d35" + "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/096748cdfb81988f60090bbb839ce3205ace0d35", - "reference": "096748cdfb81988f60090bbb839ce3205ace0d35", + "url": "https://api.github.com/repos/laravel/prompts/zipball/dd2a2ed95acacbcccd32fd98dee4c946ae7a7217", + "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217", "shasum": "" }, "require": { @@ -2176,22 +2176,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.8" + "source": "https://github.com/laravel/prompts/tree/v0.3.11" }, - "time": "2025-11-21T20:52:52+00:00" + "time": "2026-01-27T02:55:06+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.7", + "version": "v2.0.8", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd" + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd", - "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7581a4407012f5f53365e11bafc520fd7f36bc9b", + "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b", "shasum": "" }, "require": { @@ -2239,7 +2239,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-11-21T20:52:36+00:00" + "time": "2026-01-08T16:22:46+00:00" }, { "name": "laravel/tinker", @@ -2498,16 +2498,16 @@ }, { "name": "league/flysystem", - "version": "3.30.2", + "version": "3.31.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277" + "reference": "1717e0b3642b0df65ecb0cc89cdd99fa840672ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", - "reference": "5966a8ba23e62bdb518dd9e0e665c2dbd4b5b277", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1717e0b3642b0df65ecb0cc89cdd99fa840672ff", + "reference": "1717e0b3642b0df65ecb0cc89cdd99fa840672ff", "shasum": "" }, "require": { @@ -2575,22 +2575,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.30.2" + "source": "https://github.com/thephpleague/flysystem/tree/3.31.0" }, - "time": "2025-11-10T17:13:11+00:00" + "time": "2026-01-23T15:38:47+00:00" }, { "name": "league/flysystem-local", - "version": "3.30.2", + "version": "3.31.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d" + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/ab4f9d0d672f601b102936aa728801dd1a11968d", - "reference": "ab4f9d0d672f601b102936aa728801dd1a11968d", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", "shasum": "" }, "require": { @@ -2624,9 +2624,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.2" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" }, - "time": "2025-11-10T11:23:37+00:00" + "time": "2026-01-23T15:30:45+00:00" }, { "name": "league/mime-type-detection", @@ -2686,20 +2686,20 @@ }, { "name": "league/uri", - "version": "7.7.0", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807" + "reference": "4436c6ec8d458e4244448b069cc572d088230b76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807", - "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.7", + "league/uri-interfaces": "^7.8", "php": "^8.1", "psr/http-factory": "^1" }, @@ -2713,11 +2713,11 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "ext-uri": "to use the PHP native URI class", - "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", - "league/uri-components": "Needed to easily manipulate URI objects components", - "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle WHATWG URL", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2772,7 +2772,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.7.0" + "source": "https://github.com/thephpleague/uri/tree/7.8.0" }, "funding": [ { @@ -2780,20 +2780,20 @@ "type": "github" } ], - "time": "2025-12-07T16:02:06+00:00" + "time": "2026-01-14T17:24:56+00:00" }, { "name": "league/uri-interfaces", - "version": "7.7.0", + "version": "7.8.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c" + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/62ccc1a0435e1c54e10ee6022df28d6c04c2946c", - "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", "shasum": "" }, "require": { @@ -2806,7 +2806,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", - "rowbot/url": "to handle WHATWG URL", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -2856,7 +2856,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.7.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" }, "funding": [ { @@ -2864,20 +2864,20 @@ "type": "github" } ], - "time": "2025-12-07T16:03:21+00:00" + "time": "2026-01-15T06:54:53+00:00" }, { "name": "livewire/livewire", - "version": "v3.7.3", + "version": "v3.7.6", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c" + "reference": "276ac156f6ae414990784854a2673e3d23c68b24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/a5384df9fbd3eaf02e053bc49aabc8ace293fc1c", - "reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c", + "url": "https://api.github.com/repos/livewire/livewire/zipball/276ac156f6ae414990784854a2673e3d23c68b24", + "reference": "276ac156f6ae414990784854a2673e3d23c68b24", "shasum": "" }, "require": { @@ -2932,7 +2932,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.7.3" + "source": "https://github.com/livewire/livewire/tree/v3.7.6" }, "funding": [ { @@ -2940,20 +2940,20 @@ "type": "github" } ], - "time": "2025-12-19T02:00:29+00:00" + "time": "2026-01-23T05:41:38+00:00" }, { "name": "livewire/volt", - "version": "v1.10.1", + "version": "v1.10.2", "source": { "type": "git", "url": "https://github.com/livewire/volt.git", - "reference": "48cff133990c6261c63ee279fc091af6f6c6654e" + "reference": "4aa52b9adbdcb0f58af9cdb1ebabfbcbee32fac9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/volt/zipball/48cff133990c6261c63ee279fc091af6f6c6654e", - "reference": "48cff133990c6261c63ee279fc091af6f6c6654e", + "url": "https://api.github.com/repos/livewire/volt/zipball/4aa52b9adbdcb0f58af9cdb1ebabfbcbee32fac9", + "reference": "4aa52b9adbdcb0f58af9cdb1ebabfbcbee32fac9", "shasum": "" }, "require": { @@ -3011,7 +3011,7 @@ "issues": "https://github.com/livewire/volt/issues", "source": "https://github.com/livewire/volt" }, - "time": "2025-11-25T16:19:15+00:00" + "time": "2026-01-28T03:03:30+00:00" }, { "name": "masterminds/html5", @@ -3185,16 +3185,16 @@ }, { "name": "nesbot/carbon", - "version": "3.11.0", + "version": "3.11.1", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "bdb375400dcd162624531666db4799b36b64e4a1" + "reference": "f438fcc98f92babee98381d399c65336f3a3827f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", - "reference": "bdb375400dcd162624531666db4799b36b64e4a1", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/f438fcc98f92babee98381d399c65336f3a3827f", + "reference": "f438fcc98f92babee98381d399c65336f3a3827f", "shasum": "" }, "require": { @@ -3218,7 +3218,7 @@ "phpstan/extension-installer": "^1.4.3", "phpstan/phpstan": "^2.1.22", "phpunit/phpunit": "^10.5.53", - "squizlabs/php_codesniffer": "^3.13.4" + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" }, "bin": [ "bin/carbon" @@ -3261,14 +3261,14 @@ } ], "description": "An API extension for DateTime that supports 281 different languages.", - "homepage": "https://carbon.nesbot.com", + "homepage": "https://carbonphp.github.io/carbon/", "keywords": [ "date", "datetime", "time" ], "support": { - "docs": "https://carbon.nesbot.com/docs", + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", "issues": "https://github.com/CarbonPHP/carbon/issues", "source": "https://github.com/CarbonPHP/carbon" }, @@ -3286,7 +3286,7 @@ "type": "tidelift" } ], - "time": "2025-12-02T21:04:28+00:00" + "time": "2026-01-29T09:26:29+00:00" }, { "name": "nette/schema", @@ -4539,20 +4539,20 @@ }, { "name": "robsontenorio/mary", - "version": "2.5.0", + "version": "2.6.0", "source": { "type": "git", "url": "https://github.com/robsontenorio/mary.git", - "reference": "3971e466b947fee5b270e8f8a2ec7449b2866fa3" + "reference": "8f91de45487be126ec8a5c7d4068e8a88a117871" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/robsontenorio/mary/zipball/3971e466b947fee5b270e8f8a2ec7449b2866fa3", - "reference": "3971e466b947fee5b270e8f8a2ec7449b2866fa3", + "url": "https://api.github.com/repos/robsontenorio/mary/zipball/8f91de45487be126ec8a5c7d4068e8a88a117871", + "reference": "8f91de45487be126ec8a5c7d4068e8a88a117871", "shasum": "" }, "require": { - "blade-ui-kit/blade-heroicons": "^2.0", + "blade-ui-kit/blade-heroicons": "^2.6", "illuminate/support": "^10.0|^11.0|^12.0", "jfcherng/php-diff": "^6.15", "laravel/prompts": "^0|^1" @@ -4598,7 +4598,6 @@ "components", "laravel", "livewire", - "livewire 3", "livewire components", "livewire packages", "livewire ui", @@ -4614,7 +4613,7 @@ ], "support": { "issues": "https://github.com/robsontenorio/mary/issues", - "source": "https://github.com/robsontenorio/mary/tree/2.5.0" + "source": "https://github.com/robsontenorio/mary/tree/2.6.0" }, "funding": [ { @@ -4622,7 +4621,7 @@ "type": "github" } ], - "time": "2025-12-26T14:19:12+00:00" + "time": "2026-01-14T20:27:28+00:00" }, { "name": "sabberworm/php-css-parser", @@ -4698,6 +4697,67 @@ }, "time": "2025-09-14T07:37:21+00:00" }, + { + "name": "spatie/laravel-package-tools", + "version": "1.92.7", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/f09a799850b1ed765103a4f0b4355006360c49a5", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0", + "pestphp/pest": "^1.23|^2.1|^3.1", + "phpunit/php-code-coverage": "^9.0|^10.0|^11.0", + "phpunit/phpunit": "^9.5.24|^10.5|^11.5", + "spatie/pest-plugin-test-time": "^1.1|^2.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.92.7" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-07-17T15:46:43+00:00" + }, { "name": "symfony/clock", "version": "v8.0.0", @@ -4777,16 +4837,16 @@ }, { "name": "symfony/console", - "version": "v7.4.3", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6" + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/732a9ca6cd9dfd940c639062d5edbde2f6727fb6", - "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6", + "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894", + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894", "shasum": "" }, "require": { @@ -4851,7 +4911,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.3" + "source": "https://github.com/symfony/console/tree/v7.4.4" }, "funding": [ { @@ -4871,7 +4931,7 @@ "type": "tidelift" } ], - "time": "2025-12-23T14:50:43+00:00" + "time": "2026-01-13T11:36:38+00:00" }, { "name": "symfony/css-selector", @@ -5011,16 +5071,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.4.0", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2" + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2", - "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", "shasum": "" }, "require": { @@ -5069,7 +5129,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.4.0" + "source": "https://github.com/symfony/error-handler/tree/v7.4.4" }, "funding": [ { @@ -5089,20 +5149,20 @@ "type": "tidelift" } ], - "time": "2025-11-05T14:29:59+00:00" + "time": "2026-01-20T16:42:42+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v8.0.0", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "573f95783a2ec6e38752979db139f09fec033f03" + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", - "reference": "573f95783a2ec6e38752979db139f09fec033f03", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", "shasum": "" }, "require": { @@ -5154,7 +5214,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" }, "funding": [ { @@ -5174,7 +5234,7 @@ "type": "tidelift" } ], - "time": "2025-10-30T14:17:19+00:00" + "time": "2026-01-05T11:45:55+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -5254,16 +5314,16 @@ }, { "name": "symfony/finder", - "version": "v7.4.3", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "fffe05569336549b20a1be64250b40516d6e8d06" + "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/fffe05569336549b20a1be64250b40516d6e8d06", - "reference": "fffe05569336549b20a1be64250b40516d6e8d06", + "url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", + "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", "shasum": "" }, "require": { @@ -5298,7 +5358,79 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.3" + "source": "https://github.com/symfony/finder/tree/v7.4.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T15:07:59+00:00" + }, + { + "name": "symfony/html-sanitizer", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/html-sanitizer.git", + "reference": "b091fe14296544172b1ec810cbd0af42e8d8ce89" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/b091fe14296544172b1ec810cbd0af42e8d8ce89", + "reference": "b091fe14296544172b1ec810cbd0af42e8d8ce89", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "league/uri": "^6.5|^7.0", + "php": ">=8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HtmlSanitizer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Titouan Galopin", + "email": "galopintitouan@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to sanitize untrusted HTML input for safe insertion into a document's DOM.", + "homepage": "https://symfony.com", + "keywords": [ + "Purifier", + "html", + "sanitizer" + ], + "support": { + "source": "https://github.com/symfony/html-sanitizer/tree/v8.0.0" }, "funding": [ { @@ -5318,20 +5450,20 @@ "type": "tidelift" } ], - "time": "2025-12-23T14:50:43+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.4.3", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52" + "reference": "446d0db2b1f21575f1284b74533e425096abdfb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a70c745d4cea48dbd609f4075e5f5cbce453bd52", - "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/446d0db2b1f21575f1284b74533e425096abdfb6", + "reference": "446d0db2b1f21575f1284b74533e425096abdfb6", "shasum": "" }, "require": { @@ -5380,7 +5512,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.3" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.5" }, "funding": [ { @@ -5400,20 +5532,20 @@ "type": "tidelift" } ], - "time": "2025-12-23T14:23:49+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.3", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "885211d4bed3f857b8c964011923528a55702aa5" + "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/885211d4bed3f857b8c964011923528a55702aa5", - "reference": "885211d4bed3f857b8c964011923528a55702aa5", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/229eda477017f92bd2ce7615d06222ec0c19e82a", + "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a", "shasum": "" }, "require": { @@ -5499,7 +5631,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.3" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.5" }, "funding": [ { @@ -5519,20 +5651,20 @@ "type": "tidelift" } ], - "time": "2025-12-31T08:43:57+00:00" + "time": "2026-01-28T10:33:42+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.3", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "e472d35e230108231ccb7f51eb6b2100cac02ee4" + "reference": "7b750074c40c694ceb34cb926d6dffee231c5cd6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/e472d35e230108231ccb7f51eb6b2100cac02ee4", - "reference": "e472d35e230108231ccb7f51eb6b2100cac02ee4", + "url": "https://api.github.com/repos/symfony/mailer/zipball/7b750074c40c694ceb34cb926d6dffee231c5cd6", + "reference": "7b750074c40c694ceb34cb926d6dffee231c5cd6", "shasum": "" }, "require": { @@ -5583,7 +5715,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.3" + "source": "https://github.com/symfony/mailer/tree/v7.4.4" }, "funding": [ { @@ -5603,20 +5735,20 @@ "type": "tidelift" } ], - "time": "2025-12-16T08:02:06+00:00" + "time": "2026-01-08T08:25:11+00:00" }, { "name": "symfony/mime", - "version": "v7.4.0", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a" + "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a", - "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a", + "url": "https://api.github.com/repos/symfony/mime/zipball/b18c7e6e9eee1e19958138df10412f3c4c316148", + "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148", "shasum": "" }, "require": { @@ -5627,15 +5759,15 @@ }, "conflict": { "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/mailer": "<6.4", "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "phpdocumentor/reflection-docblock": "^5.2", "symfony/dependency-injection": "^6.4|^7.0|^8.0", "symfony/process": "^6.4|^7.0|^8.0", "symfony/property-access": "^6.4|^7.0|^8.0", @@ -5672,7 +5804,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.0" + "source": "https://github.com/symfony/mime/tree/v7.4.5" }, "funding": [ { @@ -5692,7 +5824,7 @@ "type": "tidelift" } ], - "time": "2025-11-16T10:14:42+00:00" + "time": "2026-01-27T08:59:58+00:00" }, { "name": "symfony/polyfill-ctype", @@ -6525,16 +6657,16 @@ }, { "name": "symfony/process", - "version": "v7.4.3", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f" + "reference": "608476f4604102976d687c483ac63a79ba18cc97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/2f8e1a6cdf590ca63715da4d3a7a3327404a523f", - "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", "shasum": "" }, "require": { @@ -6566,7 +6698,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.3" + "source": "https://github.com/symfony/process/tree/v7.4.5" }, "funding": [ { @@ -6586,20 +6718,20 @@ "type": "tidelift" } ], - "time": "2025-12-19T10:00:43+00:00" + "time": "2026-01-26T15:07:59+00:00" }, { "name": "symfony/routing", - "version": "v7.4.3", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090" + "reference": "0798827fe2c79caeed41d70b680c2c3507d10147" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090", - "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090", + "url": "https://api.github.com/repos/symfony/routing/zipball/0798827fe2c79caeed41d70b680c2c3507d10147", + "reference": "0798827fe2c79caeed41d70b680c2c3507d10147", "shasum": "" }, "require": { @@ -6651,7 +6783,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.3" + "source": "https://github.com/symfony/routing/tree/v7.4.4" }, "funding": [ { @@ -6671,7 +6803,7 @@ "type": "tidelift" } ], - "time": "2025-12-19T10:00:43+00:00" + "time": "2026-01-12T12:19:02+00:00" }, { "name": "symfony/service-contracts", @@ -6762,16 +6894,16 @@ }, { "name": "symfony/string", - "version": "v8.0.1", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" + "reference": "758b372d6882506821ed666032e43020c4f57194" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", - "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", + "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194", + "reference": "758b372d6882506821ed666032e43020c4f57194", "shasum": "" }, "require": { @@ -6828,7 +6960,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.1" + "source": "https://github.com/symfony/string/tree/v8.0.4" }, "funding": [ { @@ -6848,20 +6980,20 @@ "type": "tidelift" } ], - "time": "2025-12-01T09:13:36+00:00" + "time": "2026-01-12T12:37:40+00:00" }, { "name": "symfony/translation", - "version": "v8.0.3", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "60a8f11f0e15c48f2cc47c4da53873bb5b62135d" + "reference": "db70c8ce7db74fd2da7b1d268db46b2a8ce32c10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/60a8f11f0e15c48f2cc47c4da53873bb5b62135d", - "reference": "60a8f11f0e15c48f2cc47c4da53873bb5b62135d", + "url": "https://api.github.com/repos/symfony/translation/zipball/db70c8ce7db74fd2da7b1d268db46b2a8ce32c10", + "reference": "db70c8ce7db74fd2da7b1d268db46b2a8ce32c10", "shasum": "" }, "require": { @@ -6921,7 +7053,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v8.0.3" + "source": "https://github.com/symfony/translation/tree/v8.0.4" }, "funding": [ { @@ -6941,7 +7073,7 @@ "type": "tidelift" } ], - "time": "2025-12-21T10:59:45+00:00" + "time": "2026-01-13T13:06:50+00:00" }, { "name": "symfony/translation-contracts", @@ -7027,16 +7159,16 @@ }, { "name": "symfony/uid", - "version": "v7.4.0", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "2498e9f81b7baa206f44de583f2f48350b90142c" + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/2498e9f81b7baa206f44de583f2f48350b90142c", - "reference": "2498e9f81b7baa206f44de583f2f48350b90142c", + "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36", "shasum": "" }, "require": { @@ -7081,7 +7213,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.0" + "source": "https://github.com/symfony/uid/tree/v7.4.4" }, "funding": [ { @@ -7101,20 +7233,20 @@ "type": "tidelift" } ], - "time": "2025-09-25T11:02:55+00:00" + "time": "2026-01-03T23:30:35+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.4.3", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "7e99bebcb3f90d8721890f2963463280848cba92" + "reference": "0e4769b46a0c3c62390d124635ce59f66874b282" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7e99bebcb3f90d8721890f2963463280848cba92", - "reference": "7e99bebcb3f90d8721890f2963463280848cba92", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0e4769b46a0c3c62390d124635ce59f66874b282", + "reference": "0e4769b46a0c3c62390d124635ce59f66874b282", "shasum": "" }, "require": { @@ -7168,7 +7300,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.3" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.4" }, "funding": [ { @@ -7188,7 +7320,7 @@ "type": "tidelift" } ], - "time": "2025-12-18T07:04:31+00:00" + "time": "2026-01-01T22:13:48+00:00" }, { "name": "thecodingmachine/safe", @@ -7384,6 +7516,139 @@ }, "time": "2025-12-02T11:56:42+00:00" }, + { + "name": "tonysm/globalid-laravel", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/tonysm/globalid-laravel.git", + "reference": "bdcc1cce172af4a0f6fd68989be670fe81b9ae63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tonysm/globalid-laravel/zipball/bdcc1cce172af4a0f6fd68989be670fe81b9ae63", + "reference": "bdcc1cce172af4a0f6fd68989be670fe81b9ae63", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^8.47|^9.0|^10.0|^11.0|^12.0", + "php": "^8.2", + "spatie/laravel-package-tools": "^1.9.2" + }, + "require-dev": { + "laravel/pint": "^1.21", + "nunomaduro/collision": "^7.0|^8.1", + "orchestra/testbench": "^8.0|^9.0|^10.0", + "phpunit/phpunit": "^10.5|^11.5.3" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "GlobalId": "Tonysm\\GlobalId\\GlobalIdFacade" + }, + "providers": [ + "Tonysm\\GlobalId\\GlobalIdServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Tonysm\\GlobalId\\": "src", + "Tonysm\\GlobalId\\Database\\Factories\\": "database/factories" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tony Messias", + "email": "tonysm@hey.com", + "role": "Developer" + } + ], + "description": "Identify app models with a URI. Inspired by the globalid gem.", + "homepage": "https://github.com/tonysm/globalid-laravel", + "keywords": [ + "globalid", + "globalid-laravel", + "laravel", + "tonysm" + ], + "support": { + "issues": "https://github.com/tonysm/globalid-laravel/issues", + "source": "https://github.com/tonysm/globalid-laravel/tree/1.3.0" + }, + "time": "2025-03-11T23:05:46+00:00" + }, + { + "name": "tonysm/rich-text-laravel", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/tonysm/rich-text-laravel.git", + "reference": "2209dd90b1f62ddf50c2580047e8415b5df7a316" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tonysm/rich-text-laravel/zipball/2209dd90b1f62ddf50c2580047e8415b5df7a316", + "reference": "2209dd90b1f62ddf50c2580047e8415b5df7a316", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.47|^12.0", + "php": "^8.2", + "spatie/laravel-package-tools": "^1.9.2", + "tonysm/globalid-laravel": "^1.1" + }, + "require-dev": { + "laravel/pint": "^1.10", + "livewire/livewire": "^3.4", + "nunomaduro/collision": "^6.0|^8.0", + "orchestra/testbench": "^8.21|^9.6|^10.0", + "orchestra/workbench": "^8.0|^9.6|^10.0", + "phpunit/phpunit": "^10.5|^11.5.3", + "symfony/html-sanitizer": "^7.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Tonysm\\RichTextLaravel\\RichTextLaravelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Tonysm\\RichTextLaravel\\": "src", + "Tonysm\\RichTextLaravel\\Database\\Factories\\": "database/factories" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tony Messias", + "email": "tonysm@hey.com", + "role": "Developer" + } + ], + "description": "Integrates Trix content with Laravel", + "homepage": "https://github.com/tonysm/rich-text-laravel", + "keywords": [ + "laravel", + "rich-text-laravel" + ], + "support": { + "issues": "https://github.com/tonysm/rich-text-laravel/issues", + "source": "https://github.com/tonysm/rich-text-laravel/tree/3.4.0" + }, + "time": "2026-01-04T22:11:19+00:00" + }, { "name": "vlucas/phpdotenv", "version": "v5.6.3", @@ -7546,16 +7811,16 @@ "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v3.16.3", + "version": "v3.16.5", "source": { "type": "git", "url": "https://github.com/fruitcake/laravel-debugbar.git", - "reference": "c91e57ea113edd6526f5b8cd6b1c6ee02c67b28e" + "reference": "e85c0a8464da67e5b4a53a42796d46a43fc06c9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/c91e57ea113edd6526f5b8cd6b1c6ee02c67b28e", - "reference": "c91e57ea113edd6526f5b8cd6b1c6ee02c67b28e", + "url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/e85c0a8464da67e5b4a53a42796d46a43fc06c9a", + "reference": "e85c0a8464da67e5b4a53a42796d46a43fc06c9a", "shasum": "" }, "require": { @@ -7615,7 +7880,7 @@ ], "support": { "issues": "https://github.com/fruitcake/laravel-debugbar/issues", - "source": "https://github.com/fruitcake/laravel-debugbar/tree/v3.16.3" + "source": "https://github.com/fruitcake/laravel-debugbar/tree/v3.16.5" }, "funding": [ { @@ -7627,7 +7892,7 @@ "type": "github" } ], - "time": "2025-12-23T17:37:00+00:00" + "time": "2026-01-23T15:03:22+00:00" }, { "name": "barryvdh/laravel-ide-helper", @@ -8884,20 +9149,20 @@ }, { "name": "pestphp/pest", - "version": "v4.3.1", + "version": "v4.3.2", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "bc57a84e77afd4544ff9643a6858f68d05aeab96" + "reference": "3a4329ddc7a2b67c19fca8342a668b39be3ae398" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/bc57a84e77afd4544ff9643a6858f68d05aeab96", - "reference": "bc57a84e77afd4544ff9643a6858f68d05aeab96", + "url": "https://api.github.com/repos/pestphp/pest/zipball/3a4329ddc7a2b67c19fca8342a668b39be3ae398", + "reference": "3a4329ddc7a2b67c19fca8342a668b39be3ae398", "shasum": "" }, "require": { - "brianium/paratest": "^7.16.0", + "brianium/paratest": "^7.16.1", "nunomaduro/collision": "^8.8.3", "nunomaduro/termwind": "^2.3.3", "pestphp/pest-plugin": "^4.0.0", @@ -8905,18 +9170,18 @@ "pestphp/pest-plugin-mutate": "^4.0.1", "pestphp/pest-plugin-profanity": "^4.2.1", "php": "^8.3.0", - "phpunit/phpunit": "^12.5.4", - "symfony/process": "^7.4.3|^8.0.0" + "phpunit/phpunit": "^12.5.8", + "symfony/process": "^7.4.4|^8.0.0" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.4", + "phpunit/phpunit": ">12.5.8", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { "pestphp/pest-dev-tools": "^4.0.0", - "pestphp/pest-plugin-browser": "^4.1.1", + "pestphp/pest-plugin-browser": "^4.2.1", "pestphp/pest-plugin-type-coverage": "^4.0.3", "psy/psysh": "^0.12.18" }, @@ -8984,7 +9249,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.3.1" + "source": "https://github.com/pestphp/pest/tree/v4.3.2" }, "funding": [ { @@ -8996,7 +9261,7 @@ "type": "github" } ], - "time": "2026-01-04T16:29:59+00:00" + "time": "2026-01-28T01:01:19+00:00" }, { "name": "pestphp/pest-plugin", @@ -9713,16 +9978,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.1", + "version": "2.3.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374" + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/16dbf9937da8d4528ceb2145c9c7c0bd29e26374", - "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", "shasum": "" }, "require": { @@ -9754,9 +10019,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" }, - "time": "2026-01-12T11:33:04+00:00" + "time": "2026-01-25T14:56:51+00:00" }, { "name": "phpunit/php-code-coverage", @@ -10094,16 +10359,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.4", + "version": "12.5.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a" + "reference": "37ddb96c14bfee10304825edbb7e66d341ec6889" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4ba0e923f9d3fc655de22f9547c01d15a41fc93a", - "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/37ddb96c14bfee10304825edbb7e66d341ec6889", + "reference": "37ddb96c14bfee10304825edbb7e66d341ec6889", "shasum": "" }, "require": { @@ -10117,13 +10382,13 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.5.1", + "phpunit/php-code-coverage": "^12.5.2", "phpunit/php-file-iterator": "^6.0.0", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", "phpunit/php-timer": "^8.0.0", "sebastian/cli-parser": "^4.2.0", - "sebastian/comparator": "^7.1.3", + "sebastian/comparator": "^7.1.4", "sebastian/diff": "^7.0.0", "sebastian/environment": "^8.0.3", "sebastian/exporter": "^7.0.2", @@ -10171,7 +10436,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.4" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.8" }, "funding": [ { @@ -10195,7 +10460,7 @@ "type": "tidelift" } ], - "time": "2025-12-15T06:05:34+00:00" + "time": "2026-01-27T06:12:29+00:00" }, { "name": "sebastian/cli-parser", @@ -10268,16 +10533,16 @@ }, { "name": "sebastian/comparator", - "version": "7.1.3", + "version": "7.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148" + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dc904b4bb3ab070865fa4068cd84f3da8b945148", - "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", "shasum": "" }, "require": { @@ -10336,7 +10601,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.3" + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" }, "funding": [ { @@ -10356,7 +10621,7 @@ "type": "tidelift" } ], - "time": "2025-08-20T11:27:00+00:00" + "time": "2026-01-24T09:28:48+00:00" }, { "name": "sebastian/complexity", @@ -11224,24 +11489,24 @@ }, { "name": "ta-tikoma/phpunit-architecture-test", - "version": "0.8.5", + "version": "0.8.6", "source": { "type": "git", "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", - "reference": "cf6fb197b676ba716837c886baca842e4db29005" + "reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/cf6fb197b676ba716837c886baca842e4db29005", - "reference": "cf6fb197b676ba716837c886baca842e4db29005", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/ad48430b92901fd7d003fdaf2d7b139f96c0906e", + "reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e", "shasum": "" }, "require": { "nikic/php-parser": "^4.18.0 || ^5.0.0", "php": "^8.1.0", - "phpdocumentor/reflection-docblock": "^5.3.0", - "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0", - "symfony/finder": "^6.4.0 || ^7.0.0" + "phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "require-dev": { "laravel/pint": "^1.13.7", @@ -11277,9 +11542,9 @@ ], "support": { "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", - "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.5" + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.6" }, - "time": "2025-04-20T20:23:40+00:00" + "time": "2026-01-30T07:16:00+00:00" }, { "name": "theseer/tokenizer", diff --git a/config/livewire.php b/config/livewire.php index 7f272ff..3ab230f 100644 --- a/config/livewire.php +++ b/config/livewire.php @@ -65,7 +65,7 @@ 'temporary_file_upload' => [ 'disk' => null, // Example: 'local', 's3' | Default: 'default' - 'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB) + 'rules' => ['file', 'max:12288'], // Allow up to 12MB for temporary uploads 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp' 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1' 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs... diff --git a/database/migrations/2026_01_30_000001_create_content_sections_table.php b/database/migrations/2026_01_30_000001_create_content_sections_table.php new file mode 100644 index 0000000..d6958fc --- /dev/null +++ b/database/migrations/2026_01_30_000001_create_content_sections_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('section_key')->unique(); // e.g., 'announcements', 'terms_conditions' + $table->string('section_type'); // 'announcement', 'terms_conditions', etc. + $table->string('title'); // Display title + $table->longText('content')->nullable(); // Rich text HTML content + $table->boolean('is_active')->default(true); // Toggle visibility + $table->integer('display_order')->default(0); // For ordering multiple sections + $table->json('target_roles')->nullable(); + $table->timestamps(); + + $table->index(['section_type', 'is_active']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('content_sections'); + } +}; diff --git a/database/migrations/2026_01_30_221339_create_rich_texts_table.php b/database/migrations/2026_01_30_221339_create_rich_texts_table.php new file mode 100644 index 0000000..31eff54 --- /dev/null +++ b/database/migrations/2026_01_30_221339_create_rich_texts_table.php @@ -0,0 +1,25 @@ +id(); + $table->morphs('record'); + $table->string('field'); + $table->longText('body')->nullable(); + $table->timestamps(); + + $table->unique(['field', 'record_type', 'record_id']); + }); + } + + public function down() + { + Schema::dropIfExists('rich_texts'); + } +}; diff --git a/database/migrations/2026_02_02_000000_create_faqs_table.php b/database/migrations/2026_02_02_000000_create_faqs_table.php new file mode 100644 index 0000000..e304c9d --- /dev/null +++ b/database/migrations/2026_02_02_000000_create_faqs_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('question'); + $table->text('answer'); + $table->string('category')->nullable(); + $table->integer('display_order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + // Indexes for performance + $table->index(['is_active', 'display_order']); + $table->index('category'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('faqs'); + } +}; diff --git a/database/seeders/ContentSectionSeeder.php b/database/seeders/ContentSectionSeeder.php new file mode 100644 index 0000000..ee0592c --- /dev/null +++ b/database/seeders/ContentSectionSeeder.php @@ -0,0 +1,122 @@ + 'announcements', + 'section_type' => ContentSection::TYPE_ANNOUNCEMENT, + 'title' => 'Announcements', + 'content' => '
+

Welcome to the PLV Event Scheduling System!

+

This platform streamlines the event request and approval process for all student organizations.

+
    +
  • Submit event requests with all required documentation
  • +
  • Track your request status in real-time
  • +
  • Receive notifications on approval updates
  • +
+

For questions or assistance, please contact the OSA office.

+
', + 'is_active' => true, + 'display_order' => 1, + ], + [ + 'section_key' => 'terms_conditions', + 'section_type' => ContentSection::TYPE_TERMS_CONDITIONS, + 'title' => 'Terms & Conditions', + 'content' => '
+

By submitting an event request through this system, you agree to the following terms:

+
    +
  1. Accuracy of Information: All information provided must be accurate and complete.
  2. +
  3. Compliance: Events must comply with university policies and guidelines.
  4. +
  5. Responsibility: The requesting organization is responsible for the conduct of the event.
  6. +
  7. Cancellation: Any changes or cancellations must be communicated at least 48 hours in advance.
  8. +
  9. Documentation: All required documentary requirements must be submitted for approval.
  10. +
+
', + 'is_active' => true, + 'display_order' => 1, + ], + [ + 'section_key' => 'documentary_requirements', + 'section_type' => ContentSection::TYPE_DOCUMENTARY_REQUIREMENTS, + 'title' => 'Documentary Requirements', + 'content' => '
+

Please prepare the following documents before submitting your event request:

+
    +
  • Letter of Request - Addressed to the OSA Director
  • +
  • Event Proposal - Detailed description of the event
  • +
  • Budget Proposal - Itemized budget breakdown
  • +
  • Program Flow - Timeline of activities
  • +
  • List of Participants - Expected attendees
  • +
  • Venue Layout - If applicable
  • +
+

Note: Additional documents may be requested depending on the event type.

+
', + 'is_active' => true, + 'display_order' => 1, + ], + [ + 'section_key' => 'event_guidelines', + 'section_type' => ContentSection::TYPE_PAGE_CONTENT, + 'title' => 'Event Guidelines', + 'content' => '
+

All events must adhere to the following guidelines:

+
    +
  • Events must align with the university\'s mission and values
  • +
  • Safety protocols must be in place and followed
  • +
  • Proper venue setup and cleanup is required
  • +
  • All promotional materials must be approved
  • +
  • Post-event reports must be submitted within 7 days
  • +
+
', + 'is_active' => true, + 'display_order' => 2, + ], + [ + 'section_key' => 'faq', + 'section_type' => ContentSection::TYPE_FAQ, + 'title' => 'Frequently Asked Questions', + 'content' => '
+
+

Q: How long does the approval process take?

+

A: The standard approval process takes 5-7 working days, depending on the complexity of the event.

+
+
+

Q: Can I edit my request after submission?

+

A: Yes, you can edit requests that are pending review. Once approved or rejected, changes cannot be made.

+
+
+

Q: Who can submit event requests?

+

A: Only registered officers of recognized student organizations can submit event requests.

+
+
+

Q: What happens if my event is rejected?

+

A: You will receive feedback on why the event was rejected and can resubmit with the necessary changes.

+
+
', + 'is_active' => true, + 'display_order' => 1, + ], + ]; + + foreach ($sections as $section) { + ContentSection::updateOrCreate( + ['section_key' => $section['section_key']], + $section + ); + } + + $this->command->info('Content sections seeded successfully!'); + } +} diff --git a/database/seeders/FaqSeeder.php b/database/seeders/FaqSeeder.php new file mode 100644 index 0000000..9c07121 --- /dev/null +++ b/database/seeders/FaqSeeder.php @@ -0,0 +1,111 @@ + 'How long does the approval process take?', + 'answer' => 'The standard approval process takes 5-7 working days, depending on the complexity of the event. Events requiring venue approval from GSO may take additional time.', + 'category' => 'Event Submission', + 'display_order' => 1, + 'is_active' => true, + ], + [ + 'question' => 'Can I edit my request after submission?', + 'answer' => 'Yes, you can edit requests that are pending review or when revision is requested. Once your event is approved or rejected, changes cannot be made directly. You would need to submit a new request.', + 'category' => 'Event Submission', + 'display_order' => 2, + 'is_active' => true, + ], + [ + 'question' => 'Who can submit event requests?', + 'answer' => 'Only registered officers of recognized student organizations can submit event requests. You must be logged in with an authorized account associated with your organization.', + 'category' => 'Event Submission', + 'display_order' => 3, + 'is_active' => true, + ], + [ + 'question' => 'What documents are required for event submission?', + 'answer' => 'Required documents typically include: Activity Proposal, Letter of Request addressed to the OSA Director, and any venue-specific requirements. Documentary requirements may vary depending on the event type.', + 'category' => 'Event Submission', + 'display_order' => 4, + 'is_active' => true, + ], + + // Approval Process Category + [ + 'question' => 'What happens if my event is rejected?', + 'answer' => 'You will receive feedback on why the event was rejected. Depending on the reason, you may be able to address the concerns and resubmit with the necessary changes. Check the comments section for specific feedback from OSA.', + 'category' => 'Approval Process', + 'display_order' => 5, + 'is_active' => true, + ], + [ + 'question' => 'What does "Needs Revision" status mean?', + 'answer' => 'This status indicates that OSA has reviewed your request but requires some changes or additional information before approval. Check the comments section for specific instructions on what needs to be revised.', + 'category' => 'Approval Process', + 'display_order' => 6, + 'is_active' => true, + ], + [ + 'question' => 'Why was my event forwarded to GSO?', + 'answer' => 'Events that require specific venues managed by the General Services Office (GSO) are forwarded for venue approval. This ensures the venue is available and suitable for your event.', + 'category' => 'Approval Process', + 'display_order' => 7, + 'is_active' => true, + ], + + // Calendar & Scheduling Category + [ + 'question' => 'How do I check venue availability?', + 'answer' => 'You can view the Event Calendar to see all approved events and their scheduled venues. This helps you choose available dates and times when submitting your event request.', + 'category' => 'Calendar & Scheduling', + 'display_order' => 8, + 'is_active' => true, + ], + [ + 'question' => 'Can I reschedule an approved event?', + 'answer' => 'Yes, approved events can be rescheduled by submitting a reschedule request. The request will go through the approval process again to ensure the new date and venue are available.', + 'category' => 'Calendar & Scheduling', + 'display_order' => 9, + 'is_active' => true, + ], + + // General Category + [ + 'question' => 'How do I contact OSA for support?', + 'answer' => 'You can reach the Office of Student Affairs via email at plv.osa.official@gmail.com or visit the OSA office during office hours. For urgent matters, please call the OSA hotline.', + 'category' => 'General', + 'display_order' => 10, + 'is_active' => true, + ], + [ + 'question' => 'What happens to my event data after the event date passes?', + 'answer' => 'Completed events are archived for record-keeping purposes. You can still view your event history in the History section of your dashboard. Post-event reports should be submitted within 7 days after the event.', + 'category' => 'General', + 'display_order' => 11, + 'is_active' => true, + ], + ]; + + foreach ($faqs as $faq) { + Faq::updateOrCreate( + ['question' => $faq['question']], + $faq + ); + } + + $this->command->info('FAQs seeded successfully!'); + } +} diff --git a/docker/nginx.conf b/docker/nginx.conf index 8fd4724..415ae88 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -3,6 +3,9 @@ server { listen 80 default_server; server_name _; + # Allow file uploads up to 12MB (slightly above 10MB app limit for overhead) + client_max_body_size 12M; + root /var/www/html/public; index index.php; diff --git a/docker/start.sh b/docker/start.sh index c5a6011..e5429c3 100644 --- a/docker/start.sh +++ b/docker/start.sh @@ -47,9 +47,14 @@ if [ ! -L /var/www/html/public/storage ]; then php artisan storage:link || true fi -# Run migrations on startup -echo "Running migrations..." -php artisan migrate --force || true +# Run migration:fresh if enabled +if [ "${MIGRATE_FRESH:-false}" = "true" ]; then + echo "Running migration:fresh..." + php artisan migrate:fresh --force +else + echo "Running migrations..." + php artisan migrate --force || echo "Migration failed or already up-to-date" +fi # Run seeding if enabled if [ "${SEED_ON_DEPLOY:-false}" = "true" ]; then diff --git a/package-lock.json b/package-lock.json index d0a6499..935bf26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@fullcalendar/interaction": "^6.1.19", "@fullcalendar/list": "^6.1.19", "@fullcalendar/timegrid": "^6.1.19", - "chart.js": "^4.5.1" + "chart.js": "^4.5.1", + "sortablejs": "^1.15.6" }, "devDependencies": { "@tailwindcss/forms": "^0.5.2", @@ -25,6 +26,7 @@ "laravel-vite-plugin": "^2.0.0", "postcss": "^8.4.31", "tailwindcss": "^4.1.13", + "trix": "^2.0.10", "vite": "^7.0.4" } }, @@ -42,10 +44,10 @@ } }, "node_modules/@dicebear/adventurer": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-9.2.4.tgz", - "integrity": "sha512-Xvboay3VH1qe7lH17T+bA3qPawf5EjccssDiyhCX/VT0P21c65JyjTIUJV36Nsv08HKeyDscyP0kgt9nPTRKvA==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/adventurer/-/adventurer-9.3.1.tgz", + "integrity": "sha512-MBCA8QtRC4mWbYncFDxI67LxxXMccsORqJS8osD4F/MgOPMJsdoN9QrRfsY/MjO+4NbTSxsVzOhn2nf1WzoLbA==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -54,10 +56,10 @@ } }, "node_modules/@dicebear/adventurer-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/adventurer-neutral/-/adventurer-neutral-9.2.4.tgz", - "integrity": "sha512-I9IrB4ZYbUHSOUpWoUbfX3vG8FrjcW8htoQ4bEOR7TYOKKE11Mo1nrGMuHZ7GPfwN0CQeK1YVJhWqLTmtYn7Pg==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/adventurer-neutral/-/adventurer-neutral-9.3.1.tgz", + "integrity": "sha512-MKrzLkAGx0cdBVD+XJu6ERhdJjWsjoFS+0nF9MZT17h/m/Q12FSoj+ACoKTEXBS/LBQfQqjA9HstBlSxMzmBdw==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -66,10 +68,10 @@ } }, "node_modules/@dicebear/avataaars": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/avataaars/-/avataaars-9.2.4.tgz", - "integrity": "sha512-QKNBtA/1QGEzR+JjS4XQyrFHYGbzdOp0oa6gjhGhUDrMegDFS8uyjdRfDQsFTebVkyLWjgBQKZEiDqKqHptB6A==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/avataaars/-/avataaars-9.3.1.tgz", + "integrity": "sha512-gQwtaTfPVwNAvVktdTjyhGnQtt5ifeE/6XyMX/fUJTTo/uI2NLy4LedzjsibA/DW8xi+TbgUyXlyTaJs0H6MGA==", + "license": "See LICENSE file", "engines": { "node": ">=18.0.0" }, @@ -78,10 +80,10 @@ } }, "node_modules/@dicebear/avataaars-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/avataaars-neutral/-/avataaars-neutral-9.2.4.tgz", - "integrity": "sha512-HtBvA7elRv50QTOOsBdtYB1GVimCpGEDlDgWsu1snL5Z3d1+3dIESoXQd3mXVvKTVT8Z9ciA4TEaF09WfxDjAA==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/avataaars-neutral/-/avataaars-neutral-9.3.1.tgz", + "integrity": "sha512-d9enbUJcRfMui0ZESJ9ofJXKJPdqrzKgqefT9fcC8EfOvP0WqVtsUzcPj9l6FYhG1fMDdTsx+A8e//1lCynbQQ==", + "license": "See LICENSE file", "engines": { "node": ">=18.0.0" }, @@ -90,10 +92,10 @@ } }, "node_modules/@dicebear/big-ears": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/big-ears/-/big-ears-9.2.4.tgz", - "integrity": "sha512-U33tbh7Io6wG6ViUMN5fkWPER7hPKMaPPaYgafaYQlCT4E7QPKF2u8X1XGag3jCKm0uf4SLXfuZ8v+YONcHmNQ==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/big-ears/-/big-ears-9.3.1.tgz", + "integrity": "sha512-JN2ZlrSvpKJNyRAFzyeg+Y5wBG0EZQc8Ds5bZIHkf2/uaLUQIeDT1At2Sr7hSJDKSYZ8z83H6ckbzpDl5b9MzQ==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -102,10 +104,10 @@ } }, "node_modules/@dicebear/big-ears-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/big-ears-neutral/-/big-ears-neutral-9.2.4.tgz", - "integrity": "sha512-pPjYu80zMFl43A9sa5+tAKPkhp4n9nd7eN878IOrA1HAowh/XePh5JN8PTkNFS9eM+rnN9m8WX08XYFe30kLYw==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/big-ears-neutral/-/big-ears-neutral-9.3.1.tgz", + "integrity": "sha512-Fvw/GoT+3q77zwUbHOujGujQ4oVgtoOXE7ByfxcPeVcaUUTRARpWXlNwUBg0zt+o/Dfv875awpt3sIgKuecGsw==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -114,10 +116,10 @@ } }, "node_modules/@dicebear/big-smile": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/big-smile/-/big-smile-9.2.4.tgz", - "integrity": "sha512-zeEfXOOXy7j9tfkPLzfQdLBPyQsctBetTdEfKRArc1k3RUliNPxfJG9j88+cXQC6GXrVW2pcT2X50NSPtugCFQ==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/big-smile/-/big-smile-9.3.1.tgz", + "integrity": "sha512-c5USb4n3Zw32WIJUZqc2+mCe3vbN6XJtZjKtFbisFujMAX6I3avRf6S1JBbm5oT86ynGH6P1/EZ2K7WkThfEBg==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -126,10 +128,10 @@ } }, "node_modules/@dicebear/bottts": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/bottts/-/bottts-9.2.4.tgz", - "integrity": "sha512-4CTqrnVg+NQm6lZ4UuCJish8gGWe8EqSJrzvHQRO5TEyAKjYxbTdVqejpkycG1xkawha4FfxsYgtlSx7UwoVMw==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/bottts/-/bottts-9.3.1.tgz", + "integrity": "sha512-qIPokserYLIwpScbsvFADwspBfa1Mg8JFEtYcXYcbPLnNek8bZiAhpQSc1bHSqHjm10bFEjvTr0opSNr72CBzw==", + "license": "See LICENSE file", "engines": { "node": ">=18.0.0" }, @@ -138,10 +140,10 @@ } }, "node_modules/@dicebear/bottts-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/bottts-neutral/-/bottts-neutral-9.2.4.tgz", - "integrity": "sha512-eMVdofdD/udHsKIaeWEXShDRtiwk7vp4FjY7l0f79vIzfhkIsXKEhPcnvHKOl/yoArlDVS3Uhgjj0crWTO9RJA==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/bottts-neutral/-/bottts-neutral-9.3.1.tgz", + "integrity": "sha512-Ea3dZ7/absDmedpFIZp+yoeS6Dq0sZ8W87xw39SS45Mr1s3i4lVd/0XWc9U5QBl0XrP4CQKB7b2QpcSY4tIYtg==", + "license": "See LICENSE file", "engines": { "node": ">=18.0.0" }, @@ -150,41 +152,42 @@ } }, "node_modules/@dicebear/collection": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/collection/-/collection-9.2.4.tgz", - "integrity": "sha512-I1wCUp0yu5qSIeMQHmDYXQIXKkKjcja/SYBxppPkYFXpR2alxb0k9/swFDdMbkY6a1c9AT1kI1y+Pg6ywQ2rTA==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/collection/-/collection-9.3.1.tgz", + "integrity": "sha512-3hDYu9K4quu9jiXQTno2e0AyBzmrqm1PE6Mw7u2gYOZZ5GsSqrDdNHQODShyzqDF1LuyypZY4XN4YjFJ6fWqig==", "license": "MIT", "dependencies": { - "@dicebear/adventurer": "9.2.4", - "@dicebear/adventurer-neutral": "9.2.4", - "@dicebear/avataaars": "9.2.4", - "@dicebear/avataaars-neutral": "9.2.4", - "@dicebear/big-ears": "9.2.4", - "@dicebear/big-ears-neutral": "9.2.4", - "@dicebear/big-smile": "9.2.4", - "@dicebear/bottts": "9.2.4", - "@dicebear/bottts-neutral": "9.2.4", - "@dicebear/croodles": "9.2.4", - "@dicebear/croodles-neutral": "9.2.4", - "@dicebear/dylan": "9.2.4", - "@dicebear/fun-emoji": "9.2.4", - "@dicebear/glass": "9.2.4", - "@dicebear/icons": "9.2.4", - "@dicebear/identicon": "9.2.4", - "@dicebear/initials": "9.2.4", - "@dicebear/lorelei": "9.2.4", - "@dicebear/lorelei-neutral": "9.2.4", - "@dicebear/micah": "9.2.4", - "@dicebear/miniavs": "9.2.4", - "@dicebear/notionists": "9.2.4", - "@dicebear/notionists-neutral": "9.2.4", - "@dicebear/open-peeps": "9.2.4", - "@dicebear/personas": "9.2.4", - "@dicebear/pixel-art": "9.2.4", - "@dicebear/pixel-art-neutral": "9.2.4", - "@dicebear/rings": "9.2.4", - "@dicebear/shapes": "9.2.4", - "@dicebear/thumbs": "9.2.4" + "@dicebear/adventurer": "9.3.1", + "@dicebear/adventurer-neutral": "9.3.1", + "@dicebear/avataaars": "9.3.1", + "@dicebear/avataaars-neutral": "9.3.1", + "@dicebear/big-ears": "9.3.1", + "@dicebear/big-ears-neutral": "9.3.1", + "@dicebear/big-smile": "9.3.1", + "@dicebear/bottts": "9.3.1", + "@dicebear/bottts-neutral": "9.3.1", + "@dicebear/croodles": "9.3.1", + "@dicebear/croodles-neutral": "9.3.1", + "@dicebear/dylan": "9.3.1", + "@dicebear/fun-emoji": "9.3.1", + "@dicebear/glass": "9.3.1", + "@dicebear/icons": "9.3.1", + "@dicebear/identicon": "9.3.1", + "@dicebear/initials": "9.3.1", + "@dicebear/lorelei": "9.3.1", + "@dicebear/lorelei-neutral": "9.3.1", + "@dicebear/micah": "9.3.1", + "@dicebear/miniavs": "9.3.1", + "@dicebear/notionists": "9.3.1", + "@dicebear/notionists-neutral": "9.3.1", + "@dicebear/open-peeps": "9.3.1", + "@dicebear/personas": "9.3.1", + "@dicebear/pixel-art": "9.3.1", + "@dicebear/pixel-art-neutral": "9.3.1", + "@dicebear/rings": "9.3.1", + "@dicebear/shapes": "9.3.1", + "@dicebear/thumbs": "9.3.1", + "@dicebear/toon-head": "9.3.1" }, "engines": { "node": ">=18.0.0" @@ -194,9 +197,9 @@ } }, "node_modules/@dicebear/core": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.2.4.tgz", - "integrity": "sha512-hz6zArEcUwkZzGOSJkWICrvqnEZY7BKeiq9rqKzVJIc1tRVv0MkR0FGvIxSvXiK9TTIgKwu656xCWAGAl6oh+w==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/core/-/core-9.3.1.tgz", + "integrity": "sha512-N6Gl9z3SxYp1OVtOzQegtURFqr0D62l3QcXgvshDAVXDNjkziZ5gWj//JxYJRWldNZfVp9/pm97V3ExKI5AXPg==", "license": "MIT", "peer": true, "dependencies": { @@ -207,10 +210,10 @@ } }, "node_modules/@dicebear/croodles": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/croodles/-/croodles-9.2.4.tgz", - "integrity": "sha512-CqT0NgVfm+5kd+VnjGY4WECNFeOrj5p7GCPTSEA7tCuN72dMQOX47P9KioD3wbExXYrIlJgOcxNrQeb/FMGc3A==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/croodles/-/croodles-9.3.1.tgz", + "integrity": "sha512-p40OXll38AYpWh7vOVuapv6ClQuzaMh77e++QJjJGNr6n1OE6YmmQbp6XzE7iELzz2yGoCPIm/FjI+zcH0aAwA==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -219,10 +222,10 @@ } }, "node_modules/@dicebear/croodles-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/croodles-neutral/-/croodles-neutral-9.2.4.tgz", - "integrity": "sha512-8vAS9lIEKffSUVx256GSRAlisB8oMX38UcPWw72venO/nitLVsyZ6hZ3V7eBdII0Onrjqw1RDndslQODbVcpTw==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/croodles-neutral/-/croodles-neutral-9.3.1.tgz", + "integrity": "sha512-NEOV/j+pqxhFmxSC4EFjPgjbTsnOXkX3WgLLVz0PZBpVpS2kPOwBQXZT2fUGZIq7zHucWWatnMaKBvd2LYmFhA==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -231,10 +234,10 @@ } }, "node_modules/@dicebear/dylan": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/dylan/-/dylan-9.2.4.tgz", - "integrity": "sha512-tiih1358djAq0jDDzmW3N3S4C3ynC2yn4hhlTAq/MaUAQtAi47QxdHdFGdxH0HBMZKqA4ThLdVk3yVgN4xsukg==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/dylan/-/dylan-9.3.1.tgz", + "integrity": "sha512-CWP8S9heivya/KSSF72IJ6QKE5bUsoxKSlnLD21uO+NAm5Mzkw00PM0cgA4RvnPNf0Et7HmoJXrrvOBavWE65A==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -243,10 +246,10 @@ } }, "node_modules/@dicebear/fun-emoji": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/fun-emoji/-/fun-emoji-9.2.4.tgz", - "integrity": "sha512-Od729skczse1HvHekgEFv+mSuJKMC4sl5hENGi/izYNe6DZDqJrrD0trkGT/IVh/SLXUFbq1ZFY9I2LoUGzFZg==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/fun-emoji/-/fun-emoji-9.3.1.tgz", + "integrity": "sha512-oSJPxHvAnORxa3FJYwYGQcUuP5LIFRKzMJJ9RP4D5GTYmpjsdG0K895eo4vKkXrY/BVNVROBlfK0KcTX1xOU8g==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -255,9 +258,9 @@ } }, "node_modules/@dicebear/glass": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/glass/-/glass-9.2.4.tgz", - "integrity": "sha512-5lxbJode1t99eoIIgW0iwZMoZU4jNMJv/6vbsgYUhAslYFX5zP0jVRscksFuo89TTtS7YKqRqZAL3eNhz4bTDw==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/glass/-/glass-9.3.1.tgz", + "integrity": "sha512-yNxDgIE9A+/n5VgOMH8P0qb1EGsMhMSSrl0s6ZnTBpHLGwRv1iGXezJaZrkx/ZSPsp8KlOOfTodeqi2vkVdFHg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -267,9 +270,9 @@ } }, "node_modules/@dicebear/icons": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/icons/-/icons-9.2.4.tgz", - "integrity": "sha512-bRsK1qj8u9Z76xs8XhXlgVr/oHh68tsHTJ/1xtkX9DeTQTSamo2tS26+r231IHu+oW3mePtFnwzdG9LqEPRd4A==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/icons/-/icons-9.3.1.tgz", + "integrity": "sha512-p8BrJ/6C2smLKU8vFFu+B54zD/GFbdjumVubzcURjvbOh1YOWU2CD4TruSBZ4d2zbAwgJIAjE+5oANB0a1gfdg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -279,9 +282,9 @@ } }, "node_modules/@dicebear/identicon": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/identicon/-/identicon-9.2.4.tgz", - "integrity": "sha512-R9nw/E8fbu9HltHOqI9iL/o9i7zM+2QauXWMreQyERc39oGR9qXiwgBxsfYGcIS4C85xPyuL5B3I2RXrLBlJPg==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/identicon/-/identicon-9.3.1.tgz", + "integrity": "sha512-P3TmN7pRqlS8S9/1E+lGEMrBbQvjjXGNgXnw+Okviq+41172iLVg6Wv0nbNsOyF9QjRTjrJMq4VT3XgOuU4JAg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -291,9 +294,9 @@ } }, "node_modules/@dicebear/initials": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/initials/-/initials-9.2.4.tgz", - "integrity": "sha512-4SzHG5WoQZl1TGcpEZR4bdsSkUVqwNQCOwWSPAoBJa3BNxbVsvL08LF7I97BMgrCoknWZjQHUYt05amwTPTKtg==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/initials/-/initials-9.3.1.tgz", + "integrity": "sha512-1O61oYxKVeeGL6QcNCcxH7zsqbp37NmHbR/Y5CVqr6AVv0bBswvCzVzUv/Zmmsp70DoYQB+lbX+oNIdcqUWAaw==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -303,9 +306,9 @@ } }, "node_modules/@dicebear/lorelei": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/lorelei/-/lorelei-9.2.4.tgz", - "integrity": "sha512-eS4mPYUgDpo89HvyFAx/kgqSSKh8W4zlUA8QJeIUCWTB0WpQmeqkSgIyUJjGDYSrIujWi+zEhhckksM5EwW0Dg==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/lorelei/-/lorelei-9.3.1.tgz", + "integrity": "sha512-4btARyv+ITuL3GWKA68/h6hAPL52lN1034JHx+dJCjy7zXrsXvFKkQj62LbCkQKHQOihTkAW1dfccVQ7mlGn4w==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -315,9 +318,9 @@ } }, "node_modules/@dicebear/lorelei-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/lorelei-neutral/-/lorelei-neutral-9.2.4.tgz", - "integrity": "sha512-bWq2/GonbcJULtT+B/MGcM2UnA7kBQoH+INw8/oW83WI3GNTZ6qEwe3/W4QnCgtSOhUsuwuiSULguAFyvtkOZQ==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/lorelei-neutral/-/lorelei-neutral-9.3.1.tgz", + "integrity": "sha512-GP2EX3w8Di4b3XN0uM7lARbg1iZ9r0zaZHlUbCE2CzFy3xxrKSrRDYf2BvVt7x76doijXR5SLm4DMbEA9ARJWA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -327,10 +330,10 @@ } }, "node_modules/@dicebear/micah": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/micah/-/micah-9.2.4.tgz", - "integrity": "sha512-XNWJ8Mx+pncIV8Ye0XYc/VkMiax8kTxcP3hLTC5vmELQyMSLXzg/9SdpI+W/tCQghtPZRYTT3JdY9oU9IUlP2g==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/micah/-/micah-9.3.1.tgz", + "integrity": "sha512-LAPY6Zlw/nh0Xts4aIY5d0hlaJEfSah+M5GoBRzKFKlieYdee7hvE8gsCE+OZ4pZUc98Dh7h0XXqVt/ojYW3jw==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -339,10 +342,10 @@ } }, "node_modules/@dicebear/miniavs": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/miniavs/-/miniavs-9.2.4.tgz", - "integrity": "sha512-k7IYTAHE/4jSO6boMBRrNlqPT3bh7PLFM1atfe0nOeCDwmz/qJUBP3HdONajbf3fmo8f2IZYhELrNWTOE7Ox3Q==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/miniavs/-/miniavs-9.3.1.tgz", + "integrity": "sha512-LRLKxDAIk8fW/88YB0vbYiJ850FaO2EcdznOfyW0izDp0ghTGZXsO5B5RUiLTunH8ZCnDdA+DtaugaFTdvOx/Q==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -351,9 +354,9 @@ } }, "node_modules/@dicebear/notionists": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/notionists/-/notionists-9.2.4.tgz", - "integrity": "sha512-zcvpAJ93EfC0xQffaPZQuJPShwPhnu9aTcoPsaYGmw0oEDLcv2XYmDhUUdX84QYCn6LtCZH053rHLVazRW+OGw==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/notionists/-/notionists-9.3.1.tgz", + "integrity": "sha512-xSukD2J+iKaKq/kEOZ6svwon9sQYRpgIeNC7Gfskb7uyC+iUHQmCy6hSxLFGIOFVqEbw6Ow8uNpn9NqaFpQA4w==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -363,9 +366,9 @@ } }, "node_modules/@dicebear/notionists-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/notionists-neutral/-/notionists-neutral-9.2.4.tgz", - "integrity": "sha512-fskWzBVxQzJhCKqY24DGZbYHSBaauoRa1DgXM7+7xBuksH7mfbTmZTvnUAsAqJYBkla8IPb4ERKduDWtlWYYjQ==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/notionists-neutral/-/notionists-neutral-9.3.1.tgz", + "integrity": "sha512-Z9dikJjibAc94EtFnHQb1+ADMISLedgLls5+ARiKwKjPcYZcuRm4U8kR9tMLmqggro10uJlT7YrLSCC/5abUXA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -375,9 +378,9 @@ } }, "node_modules/@dicebear/open-peeps": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/open-peeps/-/open-peeps-9.2.4.tgz", - "integrity": "sha512-s6nwdjXFsplqEI7imlsel4Gt6kFVJm6YIgtZSpry0UdwDoxUUudei5bn957j9lXwVpVUcRjJW+TuEKztYjXkKQ==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/open-peeps/-/open-peeps-9.3.1.tgz", + "integrity": "sha512-g4A3XcLrKPy44ajlhWfmGXYUDzXfogzB/H7Z46k+mxvrhVSF0jsmReYjX80jqHNeEZ9ikIpR5g4Hbo6vmOmjGQ==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -387,10 +390,10 @@ } }, "node_modules/@dicebear/personas": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/personas/-/personas-9.2.4.tgz", - "integrity": "sha512-JNim8RfZYwb0MfxW6DLVfvreCFIevQg+V225Xe5tDfbFgbcYEp4OU/KaiqqO2476OBjCw7i7/8USbv2acBhjwA==", - "license": "MIT", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/personas/-/personas-9.3.1.tgz", + "integrity": "sha512-2xqaiY0/uHKFNhC+ZEBINJZM9/fC8gUMFCqP4N6QuXkFbqNZn4RjgbTITkGtRE5Z4m2q9hEfPey4Dc9jep5lzA==", + "license": "(MIT AND CC-BY-4.0)", "engines": { "node": ">=18.0.0" }, @@ -399,9 +402,9 @@ } }, "node_modules/@dicebear/pixel-art": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/pixel-art/-/pixel-art-9.2.4.tgz", - "integrity": "sha512-4Ao45asieswUdlCTBZqcoF/0zHR3OWUWB0Mvhlu9b1Fbc6IlPBiOfx2vsp6bnVGVnMag58tJLecx2omeXdECBQ==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/pixel-art/-/pixel-art-9.3.1.tgz", + "integrity": "sha512-yUufylvVqkb9wpG/sYRzNTeSk1YbzVgSq/ZSMyxy1kx/R4BhOkiZBSs6Ra3VjeKWVNDBzUWERaVdylLbFvAQaw==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -411,9 +414,9 @@ } }, "node_modules/@dicebear/pixel-art-neutral": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/pixel-art-neutral/-/pixel-art-neutral-9.2.4.tgz", - "integrity": "sha512-ZITPLD1cPN4GjKkhWi80s7e5dcbXy34ijWlvmxbc4eb/V7fZSsyRa9EDUW3QStpo+xrCJLcLR+3RBE5iz0PC/A==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/pixel-art-neutral/-/pixel-art-neutral-9.3.1.tgz", + "integrity": "sha512-N3mcC4CFTAMk3TqRvZVsZAGY2NONnQwoGpP+MD4E2GF+kVWoQYpvzOybVgFoOz2G0Oe4HAwSO5Qt7KTbAiD7KA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -423,9 +426,9 @@ } }, "node_modules/@dicebear/rings": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/rings/-/rings-9.2.4.tgz", - "integrity": "sha512-teZxELYyV2ogzgb5Mvtn/rHptT0HXo9SjUGS4A52mOwhIdHSGGU71MqA1YUzfae9yJThsw6K7Z9kzuY2LlZZHA==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/rings/-/rings-9.3.1.tgz", + "integrity": "sha512-1bQTKJbVzpBPbhSyHS5bzlRjYIRQKO1hR0JGmC/ZWFiE9+ySk/NNwkNghvcDxvDUaz02NLSJXlSp2T8nrsdNHg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -435,9 +438,9 @@ } }, "node_modules/@dicebear/shapes": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/shapes/-/shapes-9.2.4.tgz", - "integrity": "sha512-MhK9ZdFm1wUnH4zWeKPRMZ98UyApolf5OLzhCywfu38tRN6RVbwtBRHc/42ZwoN1JU1JgXr7hzjYucMqISHtbA==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/shapes/-/shapes-9.3.1.tgz", + "integrity": "sha512-xzw/BWSQCznRDFBp8DKQtg1Jxawq+R3upOM2pURwbCPC+9bi8f8CAz1SExA3tlAbbrVx0HQdRKIYS3GW6/GBBA==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -447,9 +450,9 @@ } }, "node_modules/@dicebear/thumbs": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/@dicebear/thumbs/-/thumbs-9.2.4.tgz", - "integrity": "sha512-EL4sMqv9p2+1Xy3d8e8UxyeKZV2+cgt3X2x2RTRzEOIIhobtkL8u6lJxmJbiGbpVtVALmrt5e7gjmwqpryYDpg==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/thumbs/-/thumbs-9.3.1.tgz", + "integrity": "sha512-HS14oyT9HXLT8OPqEz8n0Bdob3oRWoNZ5PSZrxT4nyYXxh0rDSxCCOFwPKanXznk1qCAngtAvuzzID3vo7UG3A==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -458,6 +461,18 @@ "@dicebear/core": "^9.0.0" } }, + "node_modules/@dicebear/toon-head": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@dicebear/toon-head/-/toon-head-9.3.1.tgz", + "integrity": "sha512-9a9ydhbrVG57NuscH92yzIMQ0yxEPgJtzOMG1QR6jWctgbeEuzQvJDPvJQTxtfFjx71VlQNsSL40/5rnMtCaTw==", + "license": "(MIT AND CC-BY-4.0)", + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@dicebear/core": "^9.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -1006,9 +1021,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", - "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", "cpu": [ "arm" ], @@ -1020,9 +1035,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", - "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", "cpu": [ "arm64" ], @@ -1034,9 +1049,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", - "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", "cpu": [ "arm64" ], @@ -1048,9 +1063,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", - "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", "cpu": [ "x64" ], @@ -1062,9 +1077,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", - "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", "cpu": [ "arm64" ], @@ -1076,9 +1091,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", - "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", "cpu": [ "x64" ], @@ -1090,9 +1105,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", - "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", "cpu": [ "arm" ], @@ -1104,9 +1119,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", - "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", "cpu": [ "arm" ], @@ -1118,9 +1133,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", - "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", "cpu": [ "arm64" ], @@ -1132,9 +1147,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", - "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", "cpu": [ "arm64" ], @@ -1146,9 +1161,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", - "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", "cpu": [ "loong64" ], @@ -1160,9 +1175,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", - "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", "cpu": [ "loong64" ], @@ -1174,9 +1189,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", - "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", "cpu": [ "ppc64" ], @@ -1188,9 +1203,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", - "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", "cpu": [ "ppc64" ], @@ -1202,9 +1217,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", - "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", "cpu": [ "riscv64" ], @@ -1216,9 +1231,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", - "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", "cpu": [ "riscv64" ], @@ -1230,9 +1245,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", - "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", "cpu": [ "s390x" ], @@ -1244,9 +1259,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", - "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", "cpu": [ "x64" ], @@ -1258,9 +1273,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", - "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", "cpu": [ "x64" ], @@ -1272,9 +1287,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", - "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", "cpu": [ "x64" ], @@ -1286,9 +1301,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", - "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", "cpu": [ "arm64" ], @@ -1300,9 +1315,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", - "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", "cpu": [ "arm64" ], @@ -1314,9 +1329,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", - "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", "cpu": [ "ia32" ], @@ -1328,9 +1343,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", - "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", "cpu": [ "x64" ], @@ -1342,9 +1357,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", - "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", "cpu": [ "x64" ], @@ -1667,6 +1682,14 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1738,9 +1761,9 @@ } }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", "dev": true, "license": "MIT", "dependencies": { @@ -1750,9 +1773,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.14", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", - "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1809,9 +1832,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001764", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", - "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", "dev": true, "funding": [ { @@ -1974,6 +1997,16 @@ "node": ">=8" } }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1990,9 +2023,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.283", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", + "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", "dev": true, "license": "ISC" }, @@ -2355,9 +2388,9 @@ } }, "node_modules/laravel-vite-plugin": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz", - "integrity": "sha512-zQuvzWfUKQu9oNVi1o0RZAJCwhGsdhx4NEOyrVQwJHaWDseGP9tl7XUPLY2T8Cj6+IrZ6lmyxlR1KC8unf3RLA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.1.0.tgz", + "integrity": "sha512-z+ck2BSV6KWtYcoIzk9Y5+p4NEjqM+Y4i8/H+VZRLq0OgNjW2DqyADquwYu5j8qRvaXwzNmfCWl1KrMlV1zpsg==", "dev": true, "license": "MIT", "dependencies": { @@ -2800,9 +2833,9 @@ } }, "node_modules/rollup": { - "version": "4.55.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", - "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", "dependencies": { @@ -2816,31 +2849,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.55.1", - "@rollup/rollup-android-arm64": "4.55.1", - "@rollup/rollup-darwin-arm64": "4.55.1", - "@rollup/rollup-darwin-x64": "4.55.1", - "@rollup/rollup-freebsd-arm64": "4.55.1", - "@rollup/rollup-freebsd-x64": "4.55.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", - "@rollup/rollup-linux-arm-musleabihf": "4.55.1", - "@rollup/rollup-linux-arm64-gnu": "4.55.1", - "@rollup/rollup-linux-arm64-musl": "4.55.1", - "@rollup/rollup-linux-loong64-gnu": "4.55.1", - "@rollup/rollup-linux-loong64-musl": "4.55.1", - "@rollup/rollup-linux-ppc64-gnu": "4.55.1", - "@rollup/rollup-linux-ppc64-musl": "4.55.1", - "@rollup/rollup-linux-riscv64-gnu": "4.55.1", - "@rollup/rollup-linux-riscv64-musl": "4.55.1", - "@rollup/rollup-linux-s390x-gnu": "4.55.1", - "@rollup/rollup-linux-x64-gnu": "4.55.1", - "@rollup/rollup-linux-x64-musl": "4.55.1", - "@rollup/rollup-openbsd-x64": "4.55.1", - "@rollup/rollup-openharmony-arm64": "4.55.1", - "@rollup/rollup-win32-arm64-msvc": "4.55.1", - "@rollup/rollup-win32-ia32-msvc": "4.55.1", - "@rollup/rollup-win32-x64-gnu": "4.55.1", - "@rollup/rollup-win32-x64-msvc": "4.55.1", + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" } }, @@ -2867,6 +2900,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sortablejs": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.6.tgz", + "integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2970,6 +3009,19 @@ "tree-kill": "cli.js" } }, + "node_modules/trix": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/trix/-/trix-2.1.16.tgz", + "integrity": "sha512-XtZgWI+oBvLzX7CWnkIf+ZWC+chL+YG/TkY43iMTV0Zl+CJjn18B1GJUCEWJ8qgfpcyMBuysnNAfPWiv2sV14A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dompurify": "^3.2.5" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/package.json b/package.json index f651fca..baefde6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "laravel-vite-plugin": "^2.0.0", "postcss": "^8.4.31", "tailwindcss": "^4.1.13", + "trix": "^2.0.10", "vite": "^7.0.4" }, "dependencies": { @@ -27,6 +28,7 @@ "@fullcalendar/interaction": "^6.1.19", "@fullcalendar/list": "^6.1.19", "@fullcalendar/timegrid": "^6.1.19", - "chart.js": "^4.5.1" + "chart.js": "^4.5.1", + "sortablejs": "^1.15.6" } } diff --git a/resources/css/app.css b/resources/css/app.css index 329d12e..469d0e4 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1432,3 +1432,159 @@ input[type="number"] { .dark .fc-view-harness:empty::after { color: oklch(90% 0.01 262.988 / 0.5); } + +/* ===== Minimalistic Trix Editor Styles ===== */ +trix-toolbar { + border: none !important; + background: transparent !important; + padding: 0.5rem !important; + border-bottom: 1px solid oklch(86% 0 0) !important; + border-radius: 0.5rem 0.5rem 0 0 !important; +} + +.dark trix-toolbar { + border-bottom-color: oklch(30% 0.015 262.988) !important; +} + +trix-toolbar .trix-button-row { + flex-wrap: wrap; + gap: 0.25rem; +} + +trix-toolbar .trix-button-group { + border: none !important; + margin: 0 !important; + margin-right: 0.5rem !important; + background: transparent !important; +} + +trix-toolbar .trix-button-group:last-child { + margin-right: 0 !important; +} + +trix-toolbar .trix-button { + border: none !important; + border-radius: 0.375rem !important; + background: transparent !important; + width: 2rem !important; + height: 2rem !important; + padding: 0 !important; + margin: 0 !important; + opacity: 0.7; + transition: all 0.15s ease; +} + +trix-toolbar .trix-button:hover { + background: oklch(93% 0 0) !important; + opacity: 1; +} + +trix-toolbar .trix-button.trix-active { + background: oklch(86% 0 0) !important; + opacity: 1; +} + +.dark trix-toolbar .trix-button:hover { + background: oklch(30% 0.015 262.988) !important; +} + +.dark trix-toolbar .trix-button.trix-active { + background: oklch(35% 0.015 262.988) !important; +} + +/* Dark mode icon colors */ +.dark trix-toolbar .trix-button::before { + filter: invert(1) opacity(0.8); +} + +/* Hide file attachment button */ +trix-toolbar .trix-button-group--file-tools { + display: none !important; +} + +/* Editor container */ +trix-editor { + border-top: none !important; + border-radius: 0 0 0.5rem 0.5rem !important; + min-height: 150px; +} + +trix-editor:empty::before { + color: oklch(60% 0 0); +} + +.dark trix-editor:empty::before { + color: oklch(50% 0 0); +} + +/* Trix content styling */ +trix-editor.trix-content { + padding: 1rem !important; +} + +/* ===== Prose List Styles (Fix DaisyUI Reset) ===== */ +.prose ul { + list-style-type: disc !important; + padding-left: 1.5rem !important; + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; +} + +.prose ol { + list-style-type: decimal !important; + padding-left: 1.5rem !important; + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; +} + +.prose li { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; +} + +.prose ul ul, +.prose ol ul { + list-style-type: circle !important; + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; +} + +.prose ul ul ul, +.prose ol ol ul, +.prose ul ol ul { + list-style-type: square !important; +} + +/* ================================ + SortableJS Drag & Drop Animations + ================================ */ +.sortable-ghost { + opacity: 0.4; + background: oklch(var(--color-primary) / 0.1) !important; + border: 2px dashed oklch(var(--color-primary)) !important; + border-radius: 0.75rem; +} + +.sortable-chosen { + transform: scale(1.02); + box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + border-color: oklch(var(--color-primary)) !important; + z-index: 9999; +} + +.sortable-drag { + opacity: 1 !important; + transform: rotate(2deg) scale(1.05); + box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); + background: white !important; + border-color: oklch(var(--color-primary)) !important; +} + +[data-theme="dark"] .sortable-drag, +.dark .sortable-drag { + background: oklch(var(--color-base-200)) !important; +} + +.faq-item { + transition: transform 150ms ease, box-shadow 150ms ease, border-color 150ms ease; +} diff --git a/resources/js/app.js b/resources/js/app.js index a1c42b0..a3061c9 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,7 +1,9 @@ +import "./libs/trix"; import "./bootstrap"; import "./avatar"; import "./calendar"; import { Chart, registerables } from "chart.js"; +import Sortable from "sortablejs"; // Register all Chart.js components Chart.register(...registerables); @@ -9,6 +11,9 @@ Chart.register(...registerables); // Make Chart available globally window.Chart = Chart; +// Make Sortable available globally for drag and drop +window.Sortable = Sortable; + // Register Alpine.js password strength component globally // This must be in app.js (not inline @push) to work with wire:navigate SPA navigation document.addEventListener("alpine:init", () => { diff --git a/resources/js/libs/trix.js b/resources/js/libs/trix.js new file mode 100644 index 0000000..6acd443 --- /dev/null +++ b/resources/js/libs/trix.js @@ -0,0 +1,5 @@ +import Trix from 'trix'; + +window.Trix = Trix; + +export default Trix; diff --git a/resources/views/components/announcement-banner.blade.php b/resources/views/components/announcement-banner.blade.php new file mode 100644 index 0000000..fe28f07 --- /dev/null +++ b/resources/views/components/announcement-banner.blade.php @@ -0,0 +1,43 @@ +{{-- + Announcement Banner Component + Displays active CMS announcements in a sticky banner at the top of the page + Filters announcements based on the current user's role + + Usage: +--}} + +@php + $announcements = \App\Models\ContentSection::getActiveByTypeForUser('announcement', auth()->user()); +@endphp + +@if ($announcements->isNotEmpty()) +
+ @foreach ($announcements as $announcement) +
+
+
+ +
+ @if ($announcement->title) +

{{ $announcement->title }}

+ @endif +
+ {{ h($announcement->content) }} +
+
+
+ +
+
+ @endforeach +
+@endif diff --git a/resources/views/components/content-section.blade.php b/resources/views/components/content-section.blade.php new file mode 100644 index 0000000..c448ef6 --- /dev/null +++ b/resources/views/components/content-section.blade.php @@ -0,0 +1,28 @@ +{{-- + Content Section Display Component + + Usage: + + + +--}} + +@props(['key', 'showTitle' => true, 'class' => '']) + +@php + $section = \App\Models\ContentSection::getByKey($key); +@endphp + +@if ($section && $section->is_active) +
merge(['class' => 'content-section ' . $class]) }}> + @if ($showTitle && $section->title) +

+ {{ $section->title }} +

+ @endif + +
+ {{ h($section->content) }} +
+
+@endif diff --git a/resources/views/components/documentary-requirements.blade.php b/resources/views/components/documentary-requirements.blade.php new file mode 100644 index 0000000..a2cef02 --- /dev/null +++ b/resources/views/components/documentary-requirements.blade.php @@ -0,0 +1,75 @@ +{{-- + Documentary Requirements Component + Displays documentary requirements from Event_Type rich text field + + Usage: + + or + +--}} + +@props(['eventType' => null, 'eventTypeId' => null]) + +@php + // Resolve Event_Type model + $resolvedEventType = $eventType; + if (!$resolvedEventType && $eventTypeId) { + $resolvedEventType = \App\Models\Event_Type::find($eventTypeId); + } + + $requirements = null; + if ($resolvedEventType && $resolvedEventType->documentary_requirements) { + $requirements = $resolvedEventType->documentary_requirements; + } + + $isRichText = $requirements instanceof \Tonysm\RichTextLaravel\Models\RichText; +@endphp + +
+
+ +
+

Required Documents:

+ @if ($isRichText) + {{-- Rich text from Event_Type --}} +
+
    + {{--
  • Document containing the Rationale
  • --}} +
+ {{ h((string) $requirements) }} +
+ @elseif ($resolvedEventType) + {{-- Fallback to config if no rich text set --}} + @php + $configRequirements = config( + "event_requirements.documents.{$resolvedEventType->event_type_id}", + [], + ); + @endphp + @if (!empty($configRequirements)) +
    +
  • Document containing the Rationale
  • + @foreach ($configRequirements as $document) + @if (is_array($document) && isset($document['nested'])) +
  • {{ $document[0] }}
  • +
      + @foreach ($document['nested'] as $nestedDoc) +
    • {{ $nestedDoc }}
    • + @endforeach +
    + @else +
  • {{ is_array($document) ? $document[0] : $document }}
  • + @endif + @endforeach +
+ @else +

No specific requirements defined for this event type.

+ @endif + @else + {{-- No event type provided --}} +

Select an event type to see required documents.

+ @endif + {{ $slot ?? '' }} +
+
+
diff --git a/resources/views/components/footer.blade.php b/resources/views/components/footer.blade.php index 39e91fe..f2db1e5 100644 --- a/resources/views/components/footer.blade.php +++ b/resources/views/components/footer.blade.php @@ -47,8 +47,8 @@ class="text-base-content/70 hover:text-primary transition-colors">Submit Ticket<
  • Event Calendar
  • -
  • My Tickets
  • +
  • FAQ
  • @elseif($variant === 'osa')
  • Dashboard
  • @@ -57,8 +57,8 @@ class="text-base-content/70 hover:text-primary transition-colors">Ticket Review<
  • Event Calendar
  • -
  • Reports
  • +
  • FAQ
  • @elseif($variant === 'gso')
  • Dashboard
  • @@ -67,8 +67,8 @@ class="text-base-content/70 hover:text-primary transition-colors">Ticket Review<
  • Event Calendar
  • -
  • Reports
  • +
  • FAQ
  • @elseif($variant === 'superadmin')
  • Dashboard
  • @@ -78,11 +78,15 @@ class="text-base-content/70 hover:text-primary transition-colors">User Managemen
  • Event Calendar
  • -
  • Reports
  • +
  • FAQ
  • @else
  • Home
  • +
  • FAQ
  • +
  • About Us
  • Login
  • @endif diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php index 8e63f6e..417edd6 100644 --- a/resources/views/components/layouts/app.blade.php +++ b/resources/views/components/layouts/app.blade.php @@ -88,6 +88,9 @@ @persist('osa-navigation') @endpersist + + {{-- Announcements Banner --}} + {{-- Page Content --}} diff --git a/resources/views/components/layouts/gso-layout.blade.php b/resources/views/components/layouts/gso-layout.blade.php index 384f6a5..1f9a42b 100644 --- a/resources/views/components/layouts/gso-layout.blade.php +++ b/resources/views/components/layouts/gso-layout.blade.php @@ -92,6 +92,9 @@ @persist('gso-navigation') @endpersist + + {{-- Announcements Banner --}} + {{-- Page Content --}} diff --git a/resources/views/components/layouts/guest.blade.php b/resources/views/components/layouts/guest.blade.php index 5f346cd..993becd 100644 --- a/resources/views/components/layouts/guest.blade.php +++ b/resources/views/components/layouts/guest.blade.php @@ -68,7 +68,7 @@ class="relative w-16 h-16 sm:w-20 sm:h-20 rounded-2xl shadow-2xl border-2 border
    + class="bg-white dark:bg-gray-900 rounded-3xl shadow-xl shadow-gray-200/50 dark:shadow-none max-sm:p-0 p-6 sm:p-8 border border-gray-100 dark:border-gray-800"> {{ $slot }}
    diff --git a/resources/views/components/layouts/student-org-layout.blade.php b/resources/views/components/layouts/student-org-layout.blade.php index 0307938..82651ab 100644 --- a/resources/views/components/layouts/student-org-layout.blade.php +++ b/resources/views/components/layouts/student-org-layout.blade.php @@ -35,6 +35,8 @@
    + {{-- Announcements Banner --}} + {{-- MAIN --}} {{-- SIDEBAR --}} @@ -46,7 +48,7 @@
    PLV Logo -

    Student Org

    +

    Event Scheduling

    @@ -73,6 +75,9 @@ @persist('student-navigation') @endpersist + + {{-- Announcements Banner --}} + {{-- --}} {{-- Page Content --}} diff --git a/resources/views/components/layouts/superadmin.blade.php b/resources/views/components/layouts/superadmin.blade.php index bc6ddf3..55bfc15 100644 --- a/resources/views/components/layouts/superadmin.blade.php +++ b/resources/views/components/layouts/superadmin.blade.php @@ -38,8 +38,7 @@ @persist('superadmin-sidebar') {{-- SIDEBAR --}} - + {{-- BRAND --}}
    @@ -86,10 +85,28 @@ class="tooltip tooltip-right" data-tip="User Management" wire:navigate.hover /> + {{-- Content Management --}} + + + {{-- + + + + + --}} + - {{-- --}} @@ -110,10 +127,14 @@ class="tooltip tooltip-right" data-tip="User Management" wire:navigate.hover /> @persist('superadmin-navigation') @endpersist + + {{-- Announcements Banner --}} +
    {{-- Page Content --}} {{ $slot }} +
    diff --git a/resources/views/components/terms_and_conditions.blade.php b/resources/views/components/terms_and_conditions.blade.php index 316da54..d03a5dc 100644 --- a/resources/views/components/terms_and_conditions.blade.php +++ b/resources/views/components/terms_and_conditions.blade.php @@ -1,21 +1,74 @@ -
    - {{-- Alert Banner --}} -
    -
    - -
    -

    Please read carefully before proceeding -

    -

    By checking the agreement box, you acknowledge - and accept all terms below

    +{{-- + Terms and Conditions Component + CMS-powered with fallback to hardcoded content +--}} + +@php + $termsSection = \App\Models\ContentSection::getByKey('terms_conditions'); +@endphp + +@if ($termsSection && $termsSection->is_active) + {{-- CMS-powered Terms & Conditions --}} +
    + {{-- Alert Banner --}} +
    +
    + +
    +

    Please read carefully before + proceeding

    +

    By checking the agreement box, you + acknowledge and accept all terms below

    +
    + + {{-- CMS Content --}} +
    + +
    - {{-- Terms List with Better Spacing --}} -
    -
    + +@else + {{-- Fallback: Hardcoded Terms & Conditions --}} +
    + {{-- Alert Banner --}} +
    +
    + +
    +

    Please read carefully before + proceeding +

    +

    By checking the agreement box, you + acknowledge + and accept all terms below

    +
    +
    +
    + + {{-- Terms List with Better Spacing --}} +
    {{-- Term 1 --}}
    true and accurate to the best of
    -
    - - +
    + + +@endif diff --git a/resources/views/components/tickets/review.blade.php b/resources/views/components/tickets/review.blade.php index 0b6c519..73dc66f 100644 --- a/resources/views/components/tickets/review.blade.php +++ b/resources/views/components/tickets/review.blade.php @@ -10,35 +10,31 @@ forwardRemarks: '', finalApprovalRemarks: '' }" x-on:ticket-approved.window="showApproval = false; approvalRemarks = ''" - x-on:ticket-forwarded.window="showForward = false; forwardRemarks = ''" - x-on:ticket-for-revision.window="showRevision = false; revisionRemarks = ''" - x-on:ticket-final-approved.window="showFinalApproval = false; finalApprovalRemarks = ''" x-cloak> + x-on:ticket-forwarded.window="showForward = false; forwardRemarks = ''" + x-on:ticket-for-revision.window="showRevision = false; revisionRemarks = ''" + x-on:ticket-final-approved.window="showFinalApproval = false; finalApprovalRemarks = ''" x-cloak> {{-- Flash Messages --}} @if (session()->has('success')) -
    +
    {{ session('success') }}
    @endif @if (session()->has('error')) -
    +
    {{ session('error') }}
    @endif @if (session()->has('info')) -
    +
    {{ session('info') }}
    @endif @if (session()->has('warning')) -
    +
    {{ session('warning') }}
    @endif @@ -50,7 +46,7 @@
    @@ -113,25 +109,25 @@ class="badge {{ $resolvedOfficeBadge }} {{ $resolvedOfficeTextClass }} text-whit {{-- Ticket Details - Now using modular components --}}
    {{-- Organization Information --}} - + {{-- Event Details --}} - + {{-- Schedule & Venue --}} - + {{-- Budget Information --}} - + {{-- Additional Information --}} - + {{-- Attachments --}} - + {{-- Comments --}} - +
    {{-- Sidebar --}} @@ -139,6 +135,7 @@ class="badge {{ $resolvedOfficeBadge }} {{ $resolvedOfficeTextClass }} text-whit {{-- Ticket Info --}}

    Ticket Details

    + @php $userDeleted = $ticket->user?->trashed(); @endphp
    @@ -147,12 +144,16 @@ class="badge {{ $resolvedOfficeBadge }} {{ $resolvedOfficeTextClass }} text-whit
    -

    {{ $ticket->user->name }}

    +

    + {{ $userDeleted ? 'Deleted User' : $ticket->user?->name }} +

    -

    {{ $ticket->user->email }}

    +

    + {{ $userDeleted ? 'N/A' : $ticket->user?->email }} +

    @@ -309,7 +310,7 @@ class="badge {{ $schedule->status === 'approved' ? 'badge-success' : 'badge-info {{-- Timeline Dot --}}
    - +
    {{-- Content Card --}} @@ -373,7 +374,7 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between">
    @else
    - +

    No approval actions yet

    This ticket is awaiting initial review by OSA

    @@ -472,8 +473,9 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between">
    @if (in_array('approve', $allowedActions)) @can('approve', $ticket) - @endcan @@ -482,7 +484,7 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between"> @else @if ($officeDecisionDetails)
    - +

    {{ $officeDecisionDetails['message'] }}

    @endif @@ -490,8 +492,9 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between">
    @if (in_array('final_approve', $allowedActions)) @can('finalApprove', $ticket) - @endcan @@ -499,8 +502,9 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between"> @if (in_array('for_revision', $allowedActions)) @can('requestRevision', $ticket) - @endcan @@ -513,7 +517,7 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between"> @if (in_array('approve', $allowedActions)) @can('approve', $ticket) @endcan @@ -522,20 +526,19 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between"> @if (in_array('for_revision', $allowedActions)) @can('requestRevision', $ticket) @endcan @endif
    - @elseif (in_array($ticket->status, ['received', 'amended'])) {{-- Initial Review Actions --}}
    @if (in_array('approve', $allowedActions)) @can('approve', $ticket) @endcan @@ -544,7 +547,7 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between"> @if (in_array('for_revision', $allowedActions)) @can('requestRevision', $ticket) @endcan @@ -553,7 +556,7 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between"> @if (in_array('forward', $allowedActions)) @can('forwardToGso', $ticket) @endcan @@ -562,7 +565,7 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between"> @else @if ($officeDecisionDetails)
    - +

    {{ $officeDecisionDetails['message'] }}

    @else @@ -576,9 +579,10 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between"> 'label' => 'This ticket has been approved.', ], 'for_revision' => [ - 'wrapper' => 'flex items-start gap-3 rounded-2xl bg-warning/10 border border-warning/30 px-4 py-3 text-warning', - 'icon' => 'o-arrow-path', - 'label' => 'This ticket has been put for revision.', + 'wrapper' => + 'flex items-start gap-3 rounded-2xl bg-warning/10 border border-warning/30 px-4 py-3 text-warning', + 'icon' => 'o-arrow-path', + 'label' => 'This ticket has been put for revision.', ], 'gso_review' => [ 'wrapper' => @@ -593,12 +597,12 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between"> @if ($statusDisplay)
    - + {{ $statusDisplay['label'] }}
    @else
    - + No actions available for current ticket status.
    @endif @@ -615,7 +619,7 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between">

    Confirm Ticket Approval

    - +

    You are about to approve this ticket

    This action will create an event and schedule it on the calendar.

    @@ -623,18 +627,18 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between">
    + placeholder="Enter your remarks for approving this ticket...">

    Provide a brief explanation for this approval

    @error('approvalRemarks') - {{ $message }} + {{ $message }} @enderror
    - + + wire:click="approveTicket" spinner="approveTicket" + x-bind:disabled="approvalRemarks.trim().length < 3" + @click="$wire.set('approvalRemarks', approvalRemarks)" />
    @@ -646,7 +650,7 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between">

    Request Ticket Revision

    - +

    Request changes to this ticket

    The student organization will need to revise and resubmit.

    @@ -654,19 +658,19 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between">
    + placeholder="Clearly explain what needs to be changed or added...">

    Be specific about what needs to be revised (minimum 10 characters)

    @error('revisionRemarks') - {{ $message }} + {{ $message }} @enderror
    - + + wire:click="forRevision" spinner="forRevision" + x-bind:disabled="revisionRemarks.trim().length < 10" + @click="$wire.set('revisionRemarks', revisionRemarks)" />
    @@ -679,7 +683,7 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between">

    Forward to GSO

    - +

    Forward this ticket to GSO

    GSO will review and provide their decision. You'll make the final approval. @@ -688,18 +692,18 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between">

    + placeholder="Enter remarks for GSO...">

    Explain why this needs GSO review or what specific approval is needed

    @error('forwardRemarks') - {{ $message }} + {{ $message }} @enderror
    - + + spinner="forwardToGso" x-bind:disabled="forwardRemarks.trim().length < 3" + @click="$wire.set('forwardRemarks', forwardRemarks)" />
    @@ -711,7 +715,7 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between">

    Final Approval

    - +

    Final approval after GSO review

    This will create the event and schedule it on the calendar.

    @@ -719,18 +723,18 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between">
    + placeholder="Enter your final approval remarks...">

    Document your final decision after considering GSO's input

    @error('finalApprovalRemarks') - {{ $message }} + {{ $message }} @enderror
    - + + wire:click="finalApproval" spinner="finalApproval" + x-bind:disabled="finalApprovalRemarks.trim().length < 3" + @click="$wire.set('finalApprovalRemarks', finalApprovalRemarks)" />
    @@ -763,8 +767,8 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between"> }); Livewire.on('open-attachment-preview', ({ - url - }) => { + url + }) => { if (url) { window.open(url, '_blank'); } @@ -772,8 +776,8 @@ class="mt-3 pt-2 border-t border-base-300 flex items-center justify-between"> // Add this download listener Livewire.on('download-attachment', ({ - url - }) => { + url + }) => { if (url) { const link = document.createElement('a'); link.href = url; diff --git a/resources/views/components/tickets/sections/organization-info.blade.php b/resources/views/components/tickets/sections/organization-info.blade.php index eb21cf8..1d6c5ad 100644 --- a/resources/views/components/tickets/sections/organization-info.blade.php +++ b/resources/views/components/tickets/sections/organization-info.blade.php @@ -1,42 +1,84 @@ @props(['ticket']) +@php + $userDeleted = $ticket->user?->trashed(); + $orgDeleted = $ticket->user?->studentOrganization?->trashed(); +@endphp +

    Organization Information + @if ($userDeleted || $orgDeleted) + Archived + @endif

    - @if ($ticket->user->studentOrganization) + @if ($ticket->user?->studentOrganization && !$orgDeleted) {{ $ticket->user->studentOrganization->org_name }} logo + alt="{{ $ticket->user->studentOrganization->org_name }} logo" + class="w-16 h-16 md:w-20 md:h-20 object-cover rounded-lg flex-shrink-0"> + @elseif ($orgDeleted) +
    + +
    @endif

    - {{ $ticket->user?->studentOrganization?->org_name ?? 'No Organization' }}

    + @if ($orgDeleted) + Deleted Organization + @else + {{ $ticket->user?->studentOrganization?->org_name ?? 'No Organization' }} + @endif +

    - {{ $ticket->user?->studentOrganization?->course?->course_name ?? 'N/A' }}

    + @if ($orgDeleted) + N/A + @else + {{ $ticket->user?->studentOrganization?->course?->course_name ?? 'N/A' }} + @endif +

    -

    {{ $ticket->user?->name }}

    +

    + @if ($userDeleted) + Deleted User + @else + {{ $ticket->user?->name }} + @endif +

    -

    {{ $ticket->user?->position?->position_name ?? 'N/A' }}

    +

    + @if ($userDeleted) + N/A + @else + {{ $ticket->user?->position?->position_name ?? 'N/A' }} + @endif +

    -

    {{ $ticket->user?->email }}

    +

    + @if ($userDeleted) + N/A + @else + {{ $ticket->user?->email }} + @endif +

    @@ -47,7 +89,12 @@

    - {{ $ticket->user?->studentOrganization?->adviser_name ?? 'N/A' }}

    + @if ($orgDeleted) + N/A + @else + {{ $ticket->user?->studentOrganization?->adviser_name ?? 'N/A' }} + @endif +

    diff --git a/resources/views/components/tickets/ticket-card.blade.php b/resources/views/components/tickets/ticket-card.blade.php index d9c3bee..aa74e51 100644 --- a/resources/views/components/tickets/ticket-card.blade.php +++ b/resources/views/components/tickets/ticket-card.blade.php @@ -1,5 +1,9 @@ @props(['ticket', 'role' => 'osa']) +@php + $orgDeleted = $ticket->user?->studentOrganization?->trashed(); +@endphp +
    @@ -9,7 +13,12 @@

    {{ $ticket->title }}

    - {{ $ticket->user->studentOrganization->org_name ?? 'No Organization' }}

    + @if ($orgDeleted) + Deleted Organization + @else + {{ $ticket->user?->studentOrganization?->org_name ?? 'No Organization' }} + @endif +

    @php $statusClasses = [ diff --git a/resources/views/components/trix-input.blade.php b/resources/views/components/trix-input.blade.php new file mode 100644 index 0000000..226a496 --- /dev/null +++ b/resources/views/components/trix-input.blade.php @@ -0,0 +1,11 @@ +@props(['id', 'name', 'value' => '']) + + + + + +merge(['class' => 'trix-content border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:ring-1 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm dark:[&_pre]:!bg-gray-700 dark:[&_pre]:rounded dark:[&_pre]:!text-white']) }}> diff --git a/resources/views/livewire/components/event-calendar.blade.php b/resources/views/livewire/components/event-calendar.blade.php index cb50a7b..a617b08 100644 --- a/resources/views/livewire/components/event-calendar.blade.php +++ b/resources/views/livewire/components/event-calendar.blade.php @@ -33,10 +33,21 @@ class="bg-base-100 rounded-box shadow-lg p-4 sm:p-6 mb-6"> {{-- Calendar Navigation --}}
    -

    - Loading... -

    +
    + {{-- Clickable title that triggers date picker --}} +

    + Loading... + +

    + {{-- Hidden date picker input --}} + +
    @@ -50,8 +61,25 @@ class="bg-base-100 rounded-box shadow-lg p-4 sm:p-6 mb-6"> {{-- View Mode & Filters Section --}}
    + {{-- Quick Search --}} +
    +
    + + + +
    +
    + {{-- View Mode Buttons --}} -
    +
    @@ -75,16 +103,23 @@ class="btn-sm view-mode-btn flex-1"
    {{-- Toggle Past Events & Filter Button --}} -
    +
    {{-- Filter Button with Notification Badge --}} -
    +
    {{-- Notification Badge - includes past events indicator --}} -
    +
    - +
    @@ -107,9 +142,19 @@ class="badge {{ $showPastEvents ? 'badge-warning' : 'badge-primary' }} badge-sm initialType: '{{ $eventTypeFilter }}' })" x-init="init()" x-on:open-filters.window="$nextTick(() => open = true)" x-cloak x-on:clear-filters.window="clearAll()"> -
    -
    -
    + {{-- Backdrop --}} +
    +
    + + {{-- Drawer Panel --}} +

    Filter Events

    @@ -159,8 +204,8 @@ class="absolute right-0 top-0 h-full w-11/12 lg:w-1/3 bg-base-100 shadow-xl bord {{-- Past Events Toggle --}}
    diff --git a/resources/views/livewire/faq.blade.php b/resources/views/livewire/faq.blade.php new file mode 100644 index 0000000..3c3414a --- /dev/null +++ b/resources/views/livewire/faq.blade.php @@ -0,0 +1,152 @@ +
    +
    + {{-- Header Section --}} +
    +
    + +
    +

    + Frequently Asked Questions +

    +

    + Find answers to common questions about the PLV Event Scheduling System. Can't find what you're looking + for? Contact us for more help. +

    + + {{-- Decorative element --}} +
    +
    +
    +
    + + @if ($this->hasFaqs) + {{-- FAQ Accordion Sections --}} +
    + @foreach ($this->faqs as $category => $faqGroup) +
    + {{-- Category Title --}} + @if ($this->faqs->count() > 1 || $category !== 'General') +

    + + {{ $category }} +

    + @endif + + {{-- DaisyUI Accordion --}} +
    + @foreach ($faqGroup as $index => $faq) +
    + +
    + + + {{ $faq->question }} + +
    +
    +
    + {!! nl2br(e($faq->answer)) !!} +
    +
    +
    + @endforeach +
    +
    + @endforeach +
    + + {{-- Contact CTA --}} +
    +
    +

    Still have questions?

    +

    Our team is here to help you with any additional inquiries.

    + +
    +
    + @else + {{-- Empty State: No FAQs Available --}} +
    +
    + +
    +

    FAQs Coming Soon

    +

    + We're currently preparing helpful answers to common questions. Check back soon or contact us + directly for assistance. +

    + + + Contact Us + +
    + @endif + + {{-- Back to Home Link --}} + +
    + + {{-- Accordion Transition Styles --}} + +
    diff --git a/resources/views/livewire/gso/dashboard.blade.php b/resources/views/livewire/gso/dashboard.blade.php index c64b350..2e37512 100644 --- a/resources/views/livewire/gso/dashboard.blade.php +++ b/resources/views/livewire/gso/dashboard.blade.php @@ -68,8 +68,9 @@
    -

    GSO Dashboard

    -

    General Services Office - Event Management System

    +

    GSO Dashboard

    +

    General Services Office - Event Management + System

    +
    - - - - - - - + wire:loading.class="opacity-50" wire:target="refreshData"> + + +
    +
    +
    +

    Pending Approvals

    +

    + {{ number_format(data_get($stats, 'pending', 0)) }} +

    +

    Awaiting review

    +
    +
    + +
    +
    +
    + + +
    +
    +
    +

    Approved Today

    +

    + {{ number_format(data_get($stats, 'approvedToday', 0)) }}

    +

    Processed today

    +
    +
    + +
    +
    +
    + + +
    +
    +
    +

    For Revision

    +

    + {{ number_format(data_get($stats, 'for_revisionToday', 0)) }}

    +

    Needs correction

    +
    +
    + +
    +
    +
    + + +
    +
    +
    +

    Upcoming Events

    +

    + {{ number_format(data_get($stats, 'upcomingEvents', 0)) }}

    +

    Scheduled approved

    +
    +
    + +
    +
    +
    +
    - + wire:target="refreshData"> + + +
    -

    High Priority Items

    -

    +

    High Priority

    +

    {{ number_format($pendingApprovalRows->where('priority_key', 'high')->count()) }}

    +

    Urgent requests

    +
    +
    +
    -
    - +
    - + +
    -

    Tickets In Queue

    -

    +

    Tickets In Queue

    +

    {{ number_format($ticketsInQueue ?? 0) }}

    +

    Total in pipeline

    +
    +
    +
    -
    - +
    + wire:target="refreshData"> + subtitle="Requests requiring your attention" class="col-span-1 lg:col-span-2 shadow-md"> + label="View All" wire:navigate /> @if ($pendingApprovalRows->count() > 0)
    @foreach ($pendingApprovalRows as $approval) -
    +
    + class="{{ $approval['request_type_badge_class'] }} badge-sm text-white whitespace-normal h-auto" />
    - + {{ $approval['event_title'] }}
    - + {{ $approval['organization'] }}
    @@ -182,16 +249,16 @@ class="{{ $approval['request_type_badge_class'] }} badge-sm text-white whitespac
    + class="{{ $approval['request_type_badge_class'] }} badge-sm" />
    - + {{ $approval['event_title'] }}
    - + {{ $approval['organization'] }} {{ $approval['event_date'] }} @@ -204,15 +271,15 @@ class="{{ $approval['request_type_badge_class'] }} badge-sm"/>
    @else
    - +

    No pending approvals

    All caught up! Great work.

    @endif - + @if ($recentActivityItems->count() > 0)
    @@ -234,14 +301,16 @@ class="shadow-md"> @endphp
    + wire:key="activity-{{ $activity['id'] ?? \Illuminate\Support\Str::uuid()->toString() }}">
    - +
    -

    {{ $activity['action'] ?? 'Activity' }}

    -

    +

    + {{ $activity['action'] ?? 'Activity' }}

    +

    {{ $activity['details'] ?? 'Details unavailable.' }}

    -

    {{ $activity['time_ago'] ?? 'Just now' }} +

    + {{ $activity['time_ago'] ?? 'Just now' }}

    @@ -250,52 +319,55 @@ class="shadow-md">
    @else
    - -

    Activity logs will appear here once actions are taken.

    + +

    Activity logs will appear here once actions + are taken.

    @endif
    + class="shadow-md" wire:loading.class="opacity-50" wire:target="refreshData"> @if ($approvalSnapshotRows->count() > 0)
    @foreach ($approvalSnapshotRows as $row) -
    +
    - Ticket -
    - + Ticket +
    +
    - Event -

    {{ $row['event_title'] }}

    + Event +

    {{ $row['event_title'] }}

    - Organization -

    {{ $row['organization'] }}

    + Organization +

    {{ $row['organization'] }}

    - Event Date -

    {{ $row['event_date'] }}

    + Event Date +

    {{ $row['event_date'] }}

    - Priority + Priority
    @php $priorityClass = match ($row['priority_key'] ?? 'low') { @@ -305,7 +377,7 @@ class="shadow-md" default => 'badge-ghost', }; @endphp - +
    @@ -316,69 +388,89 @@ class="shadow-md"
    +
    +
    + class="btn-primary" icon-right="o-arrow-right" wire:navigate />
    @else -
    - -

    No approval data to display

    -

    New tickets will appear here once submitted.

    +
    + +

    No approval data to display

    +

    New tickets will appear here once + submitted.

    @endif +
    -

    Quick Actions

    -
    +
    +

    Quick Actions

    +

    Frequently used features

    +
    +
    + -
    - -

    Ticket Review

    -

    Process office approvals

    + class="sm:p-5 group max-md:pt-1 md:p-6 bg-gradient-to-br from-indigo/5 to-indigo/10 dark:from-indigo/10 dark:to-indigo/20 hover:from-indigo/10 hover:to-indigo/20 rounded-xl border-2 border-transparent hover:border-blue-800 transition-all duration-200 cursor-pointer"> +
    +
    + +
    +

    Ticket Review

    +

    Process office approvals

    + -
    - -

    Event Calendar

    -

    View schedule

    + class="sm:p-5 group max-md:pt-1 md:p-6 bg-gradient-to-br from-indigo/5 to-indigo/10 dark:from-indigo/10 dark:to-indigo/20 hover:from-indigo/10 hover:to-indigo/20 rounded-xl border-2 border-transparent hover:border-violet-800 transition-all duration-200 cursor-pointer"> +
    +
    + +
    +

    Event Calendar

    +

    View schedule

    + - diff --git a/resources/views/livewire/gso/notifications.blade.php b/resources/views/livewire/gso/notifications.blade.php index c86dc1c..3bedac7 100644 --- a/resources/views/livewire/gso/notifications.blade.php +++ b/resources/views/livewire/gso/notifications.blade.php @@ -138,6 +138,7 @@ class="loading loading-spinner loading-sm">
    + @@ -128,7 +129,8 @@ class="loading loading-spinner loading-sm"> -
    + @endif + @if (isset($data['ticket_number']))
    diff --git a/resources/views/livewire/osa/reports.blade.php b/resources/views/livewire/osa/reports.blade.php index d73a27b..90e33ec 100644 --- a/resources/views/livewire/osa/reports.blade.php +++ b/resources/views/livewire/osa/reports.blade.php @@ -366,9 +366,12 @@ @foreach ($reportData->take(10) as $ticket) + @php $orgDeleted = $ticket->user?->studentOrganization?->trashed(); @endphp {{ $ticket->title }} - {{ $ticket->user->studentOrganization->org_name ?? 'No Organization' }} + + {{ $orgDeleted ? 'Deleted Organization' : $ticket->user?->studentOrganization?->org_name ?? 'No Organization' }} + {{ $ticket->events->first()?->created_at?->format('M d, Y') ?? 'TBD' }} {{ $ticket->updated_at?->format('M d, Y') ?? 'TBD' }} @@ -399,9 +402,11 @@ @foreach ($reportData->take(10) as $ticket) + @php $orgDeleted = $ticket->user?->studentOrganization?->trashed(); @endphp {{ $ticket->title }} - {{ $ticket->user->studentOrganization->org_name ?? 'No Organization' }} + + {{ $orgDeleted ? 'Deleted Organization' : $ticket->user?->studentOrganization?->org_name ?? 'No Organization' }} {{ $ticket->created_at->format('M d, Y') }} {{ $ticket->updated_at?->format('M d, Y') ?? 'TBD' }} diff --git a/resources/views/livewire/osa/ticket-management.blade.php b/resources/views/livewire/osa/ticket-management.blade.php index 2caab52..9d57374 100644 --- a/resources/views/livewire/osa/ticket-management.blade.php +++ b/resources/views/livewire/osa/ticket-management.blade.php @@ -77,9 +77,8 @@
    @if ($search || $statusFilter || $organizationFilter || $dateFilter) - + Clear @@ -154,9 +153,10 @@ @forelse($tickets as $ticket) + @php $orgDeleted = $ticket->user?->studentOrganization?->trashed(); @endphp #{{ $ticket->ticket_number }} @@ -171,14 +171,22 @@
    - {{ $ticket->user->studentOrganization->org_name }} logo + @if (!$orgDeleted && $ticket->user?->studentOrganization) + {{ $ticket->user->studentOrganization->org_name }} logo + @else +
    + +
    + @endif
    -
    - {{ $ticket->user->studentOrganization->org_name ?? 'No Organization' }} +
    + {{ $orgDeleted ? 'Deleted Organization' : $ticket->user?->studentOrganization?->org_name ?? 'No Organization' }}
    @@ -246,13 +254,21 @@ class="{{ $statusClasses[$ticket->status] ?? 'badge-neutral' }} text-white badge
    - {{ $ticket->user->studentOrganization->org_name }} logo + @php $mobileOrgDeleted = $ticket->user?->studentOrganization?->trashed(); @endphp + @if (!$mobileOrgDeleted && $ticket->user?->studentOrganization) + {{ $ticket->user->studentOrganization->org_name }} logo + @else +
    + +
    + @endif
    {{ $ticket->user->studentOrganization->org_name ?? 'No Organization' }} + class="font-medium truncate {{ $mobileOrgDeleted ? 'italic text-base-content/50' : '' }}">{{ $mobileOrgDeleted ? 'Deleted Organization' : $ticket->user?->studentOrganization?->org_name ?? 'No Organization' }}
    diff --git a/resources/views/livewire/osa/ticket-review/index.blade.php b/resources/views/livewire/osa/ticket-review/index.blade.php index 99dea92..6360dfe 100644 --- a/resources/views/livewire/osa/ticket-review/index.blade.php +++ b/resources/views/livewire/osa/ticket-review/index.blade.php @@ -1,7 +1,7 @@
    {{-- Skeleton Loading State (First Load Only) --}} @@ -23,9 +23,10 @@
    - {{ $tickets->total() }} + {{ $tickets->total() }} Tickets -
    @@ -35,7 +36,7 @@ class="loading loading-spinner loading-xs"> {{-- Search Filter --}}
    -
    +
    @@ -67,10 +68,26 @@ class="input input-bordered w-full pr-10" />
    + {{-- Organization Filter --}} +
    + +
    + +
    +
    +
    + @endif +
    + + {{-- Dropdown --}} +
    + + {{-- Filtered Categories List --}} + @if (count($this->filteredCategories) > 0) + @foreach ($this->filteredCategories as $cat) +
    + @if ($categoryMode === 'editing' && $editingCategory === $cat) + {{-- Edit Mode --}} +
    + + + +
    + @else + {{-- View Mode --}} + +
    + + +
    + @endif +
    + @endforeach + @endif + + {{-- Create New Option (shown when search doesn't match exactly) --}} + @if ($categorySearch && !$this->isExactMatch) +
    + +
    + @endif + + {{-- Empty State --}} + @if (count($this->filteredCategories) === 0 && !$categorySearch) +
    + +

    No categories yet

    +
    + @endif +
    + +

    + Type to search or create new. Hover items to edit/delete. +

    +
    + + {{-- Display Order --}} +
    + {{-- --}} + + +

    + Lower numbers appear first. Use to control FAQ ordering. +

    +
    +
    +
    + + {{-- Section 3: Status --}} +
    + + +
    + + + @if ($isEditing && !$isActive) +
    +
    + +
    +

    Hidden from Public

    +

    + This FAQ will not be visible on the public FAQ page until activated. +

    +
    +
    +
    + @endif +
    +
    +
    + + {{-- Form Actions --}} + +
    + + +
    +
    + + + + {{-- Preview Card (for editing) --}} + @if ($question || $answer) +
    +

    + + Preview +

    + +
    + +
    + + + {{ $question ?: 'Your question will appear here...' }} + +
    +
    +
    + {!! $answer ?: 'Your answer will appear here...' !!} +
    +
    +
    +
    +
    + @endif +
    diff --git a/resources/views/livewire/superadmin/faq-manager.blade.php b/resources/views/livewire/superadmin/faq-manager.blade.php new file mode 100644 index 0000000..a1abc9e --- /dev/null +++ b/resources/views/livewire/superadmin/faq-manager.blade.php @@ -0,0 +1,201 @@ +
    + {{-- Page Header --}} +
    +
    +
    +

    + FAQ Management +

    +

    + Manage frequently asked questions displayed on the public FAQ page +

    +
    + +
    +
    + + {{-- Filters Card --}} + +
    + {{-- Search --}} +
    + +
    + + {{-- Category Filter --}} +
    + +
    + + {{-- Status Filter --}} +
    + +
    + + {{-- Clear Filters --}} + @if ($search || $filterCategory || $filterStatus) + + @endif +
    +
    + + {{-- FAQ List --}} + + @if ($faqs->count() > 0) +
    + @foreach ($faqs as $faq) +
    + {{-- FAQ Content --}} +
    +
    + {{-- Drag Handle --}} +
    + +
    + +
    + {{-- Question --}} +
    +

    + {{ $faq->question }} +

    + @if (!$faq->is_active) + + Inactive + + @endif +
    + + {{-- Answer Preview --}} +

    + {{ Str::limit(strip_tags($faq->answer), 150) }} +

    + + {{-- Category Badge --}} + @if ($faq->category) +
    + + + {{ $faq->category }} + +
    + @endif +
    +
    +
    + + {{-- Actions --}} +
    + {{-- Toggle Active Status --}} + + + {{-- Edit --}} + + + {{-- Delete --}} + +
    +
    + @endforeach +
    + + {{-- Pagination --}} + @if ($faqs->hasPages()) + + @endif + @else + {{-- Empty State --}} +
    + +

    No FAQs found

    +

    + @if ($search || $filterCategory || $filterStatus) + Try adjusting your filters or search terms. + @else + Add your first FAQ to help users find answers quickly. + @endif +

    + @if (!$search && !$filterCategory && !$filterStatus) + + @endif +
    + @endif +
    + + {{-- Delete Confirmation Modal --}} + @if ($deletingFaqId) + +
    +
    + + + Are you sure you want to delete this FAQ? + +
    + +
    +

    + {{ $deletingFaqQuestion }} +

    +
    + +

    + This action cannot be undone. The FAQ will be permanently removed from the system. +

    +
    + + + + + +
    + @endif +
    diff --git a/resources/views/livewire/superadmin/logs.blade.php b/resources/views/livewire/superadmin/logs.blade.php index 27c45db..f71e340 100644 --- a/resources/views/livewire/superadmin/logs.blade.php +++ b/resources/views/livewire/superadmin/logs.blade.php @@ -22,42 +22,60 @@
    - - @scope('cell_when', $log) -
    -
    {{ $log->created_at->setTimezone('Asia/Manila')->format('M d, Y') }}
    -
    {{ $log->created_at->setTimezone('Asia/Manila')->format('g:i A') }}
    -
    - @endscope +
    +
    + + @scope('cell_when', $log) +
    +
    + {{ $log->created_at->setTimezone('Asia/Manila')->format('M d, Y') }}
    +
    + {{ $log->created_at->setTimezone('Asia/Manila')->format('g:i A') }}
    +
    + @endscope - @scope('cell_who', $log) - @if ($log->user) -
    -
    {{ $log->user->name }}
    -
    {{ $log->user->email }}
    -
    - @else - System - @endif - @endscope + @scope('cell_who', $log) + @if ($log->user) +
    +
    {{ $log->user->name }}
    +
    {{ $log->user->email }}
    +
    + @else + System + @endif + @endscope - @scope('cell_action', $log) - - @endscope + @scope('cell_action', $log) + + @endscope - @scope('cell_details', $log) -
    - {{ $log->details ?: 'No details' }} -
    - @endscope -
    + @scope('cell_details', $log) +
    + {{ $log->details ?: 'No details' }} +
    + @endscope + + +
    +
    + +
    +

    No logs found

    +

    Try adjusting your search or filters.

    +
    +
    + +
    +
    @if ($logs->hasPages()) diff --git a/resources/views/livewire/superadmin/notifications.blade.php b/resources/views/livewire/superadmin/notifications.blade.php index 9826be3..fd7bbe7 100644 --- a/resources/views/livewire/superadmin/notifications.blade.php +++ b/resources/views/livewire/superadmin/notifications.blade.php @@ -1,302 +1,290 @@ -
    +
    +
    - {{-- Skeleton Loading State (First Load Only) --}} -
    - @include('livewire.superadmin.placeholders.notifications') -
    - - {{-- Actual Content --}} -
    + {{-- Header Section --}} +
    +
    +
    +

    System Notifications

    +

    Track system-level events, user management, and + configuration changes

    +
    +
    + + +
    +
    +
    -
    -
    + {{-- Notification Stats --}} +
    +
    +
    +
    + +
    +
    +

    {{ $unreadCount }}

    +

    Unread

    +
    +
    +
    - {{-- Header Section --}} -
    -
    -
    -

    System Notifications

    -

    Track system-level events, user management, and - configuration changes

    -
    -
    - - -
    +
    +
    +
    + +
    +
    +

    {{ $todayCount }}

    +

    Today

    +
    - {{-- Notification Stats --}} -
    -
    -
    -
    - -
    -
    -

    {{ $unreadCount }}

    -

    Unread

    -
    -
    +
    +
    +
    + +
    +
    +

    {{ $weekCount }}

    +

    This Week

    +
    +
    -
    -
    -
    - -
    -
    -

    {{ $todayCount }}

    -

    Today

    -
    -
    +
    +
    +
    + +
    +
    +

    {{ $totalCount }}

    +

    Total

    +
    +
    +
    -
    -
    -
    - -
    -
    -

    {{ $weekCount }}

    -

    This Week

    -
    -
    + {{-- Filters --}} +
    +
    +
    + +
    + + + + + +
    +
    -
    -
    -
    - -
    -
    -

    {{ $totalCount }}

    -

    Total

    -
    -
    +
    + +
    +
    - {{-- Filters --}} -
    -
    -
    - -
    - - - - - - -
    -
    +
    + + +
    +
    +
    -
    - -
    - -
    -
    + {{-- Notifications List --}} +
    + {{-- Skeleton Loader (Filtering/Searching) --}} +
    + @include('livewire.placeholders.notification-list') +
    -
    - - -
    -
    -
    + {{-- Notifications Grid --}} +
    + @forelse($notifications as $notification) + @php + $data = $notification->data; + $isUnread = is_null($notification->read_at); + $createdAt = Illuminate\Support\Carbon::parse($notification->created_at); + $timeAgo = $createdAt->diffForHumans(); - {{-- Notifications List --}} -
    - {{-- Skeleton Loader (Filtering/Searching) --}} -
    - @include('livewire.placeholders.notification-list') -
    + // Map notification colors + $colorMap = [ + 'primary' => 'primary', + 'success' => 'success', + 'error' => 'error', + 'warning' => 'warning', + 'info' => 'info', + 'secondary' => 'secondary', + ]; + $color = $colorMap[$data['color'] ?? 'primary'] ?? 'primary'; - {{-- Notifications Grid --}} -
    - @forelse($notifications as $notification) - @php - $data = $notification->data; - $isUnread = is_null($notification->read_at); - $createdAt = Illuminate\Support\Carbon::parse($notification->created_at); - $timeAgo = $createdAt->diffForHumans(); + // Determine notification URL + $url = $data['action_url'] ?? route('superadmin.notifications'); - // Map notification colors - $colorMap = [ - 'primary' => 'primary', - 'success' => 'success', - 'error' => 'error', - 'warning' => 'warning', - 'info' => 'info', - 'secondary' => 'secondary', - ]; - $color = $colorMap[$data['color'] ?? 'primary'] ?? 'primary'; + // Map colors to Tailwind classes + $bgClass = match ($color) { + 'success' => 'bg-success/5 border-success hover:bg-success/10', + 'error' => 'bg-error/5 border-error hover:bg-error/10', + 'warning' => 'bg-warning/5 border-warning hover:bg-warning/10', + 'info' => 'bg-info/5 border-info hover:bg-info/10', + 'secondary' => 'bg-secondary/5 border-secondary hover:bg-secondary/10', + default => 'bg-primary/5 border-primary hover:bg-primary/10', + }; - // Determine notification URL - $url = $data['action_url'] ?? route('superadmin.notifications'); + $iconBgClass = match ($color) { + 'success' => 'bg-success/20', + 'error' => 'bg-error/20', + 'warning' => 'bg-warning/20', + 'info' => 'bg-info/20', + 'secondary' => 'bg-secondary/20', + default => 'bg-primary/20', + }; - // Map colors to Tailwind classes - $bgClass = match ($color) { - 'success' => 'bg-success/5 border-success hover:bg-success/10', - 'error' => 'bg-error/5 border-error hover:bg-error/10', - 'warning' => 'bg-warning/5 border-warning hover:bg-warning/10', - 'info' => 'bg-info/5 border-info hover:bg-info/10', - 'secondary' => 'bg-secondary/5 border-secondary hover:bg-secondary/10', - default => 'bg-primary/5 border-primary hover:bg-primary/10', - }; + $iconTextClass = match ($color) { + 'success' => 'text-success', + 'error' => 'text-error', + 'warning' => 'text-warning', + 'info' => 'text-info', + 'secondary' => 'text-secondary', + default => 'text-primary', + }; - $iconBgClass = match ($color) { - 'success' => 'bg-success/20', - 'error' => 'bg-error/20', - 'warning' => 'bg-warning/20', - 'info' => 'bg-info/20', - 'secondary' => 'bg-secondary/20', - default => 'bg-primary/20', - }; + $dotClass = match ($color) { + 'success' => 'bg-success', + 'error' => 'bg-error', + 'warning' => 'bg-warning', + 'info' => 'bg-info', + 'secondary' => 'bg-secondary', + default => 'bg-primary', + }; - $iconTextClass = match ($color) { - 'success' => 'text-success', - 'error' => 'text-error', - 'warning' => 'text-warning', - 'info' => 'text-info', - 'secondary' => 'text-secondary', - default => 'text-primary', - }; + // Icon based on notification type + $iconMap = [ + 'user_created' => 's-user-plus', + 'organization_created' => 's-building-office-2', + 'system_settings_updated' => 's-cog-6-tooth', + 'announcement' => 's-megaphone', + ]; + $icon = $iconMap[$data['type'] ?? ''] ?? 's-bell'; + @endphp - $dotClass = match ($color) { - 'success' => 'bg-success', - 'error' => 'bg-error', - 'warning' => 'bg-warning', - 'info' => 'bg-info', - 'secondary' => 'bg-secondary', - default => 'bg-primary', - }; +
    +
    +
    + +
    +
    + +
    + + + {{-- Pagination --}} + @if ($notifications->hasPages()) + + @endif +
    +
    +
    diff --git a/resources/views/livewire/superadmin/placeholders/dashboard.blade.php b/resources/views/livewire/superadmin/placeholders/dashboard.blade.php new file mode 100644 index 0000000..69e2852 --- /dev/null +++ b/resources/views/livewire/superadmin/placeholders/dashboard.blade.php @@ -0,0 +1,65 @@ +{{-- Dashboard Skeleton --}} +
    + {{-- Header Skeleton --}} +
    +
    +
    +
    + + {{-- Stats Grid Skeleton --}} +
    + @for ($i = 0; $i < 4; $i++) +
    +
    +
    +
    +
    +
    +
    +
    +
    + @endfor +
    + + {{-- Main Content Grid Skeleton --}} +
    + {{-- Pending Approvals Table --}} +
    +
    +
    +
    +
    +
    + @for ($i = 0; $i < 5; $i++) +
    +
    +
    +
    +
    +
    + @endfor +
    +
    + + {{-- Recent Activity List --}} +
    +
    +
    + @for ($i = 0; $i < 5; $i++) +
    +
    +
    + @if ($i < 4) +
    + @endif +
    +
    +
    +
    +
    +
    + @endfor +
    +
    +
    +
    diff --git a/resources/views/livewire/superadmin/placeholders/logs.blade.php b/resources/views/livewire/superadmin/placeholders/logs.blade.php new file mode 100644 index 0000000..0094ff8 --- /dev/null +++ b/resources/views/livewire/superadmin/placeholders/logs.blade.php @@ -0,0 +1,41 @@ +{{-- Logs Skeleton --}} +
    + {{-- Header --}} +
    +
    +
    +
    +
    +
    +
    + +
    +
    + {{-- Unified Filter Bar --}} +
    +
    +
    +
    +
    + + {{-- Table --}} +
    +
    + @for ($i = 0; $i < 10; $i++) +
    +
    +
    +
    +
    +
    + @endfor +
    + + {{-- Pagination --}} +
    +
    +
    +
    +
    +
    +
    diff --git a/resources/views/livewire/superadmin/placeholders/reports.blade.php b/resources/views/livewire/superadmin/placeholders/reports.blade.php new file mode 100644 index 0000000..238bf66 --- /dev/null +++ b/resources/views/livewire/superadmin/placeholders/reports.blade.php @@ -0,0 +1,53 @@ +{{-- Reports Skeleton --}} +
    + {{-- Header --}} +
    +
    +
    +
    +
    +
    +
    + + {{-- Filters --}} +
    +
    +
    +
    +
    +
    +
    +
    +
    + + {{-- Stats Grid --}} +
    + @for ($i = 0; $i < 6; $i++) +
    +
    +
    +
    +
    +
    +
    +
    +
    + @endfor +
    + + {{-- Charts Grid --}} +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    diff --git a/resources/views/livewire/superadmin/placeholders/users.blade.php b/resources/views/livewire/superadmin/placeholders/users.blade.php new file mode 100644 index 0000000..f521e74 --- /dev/null +++ b/resources/views/livewire/superadmin/placeholders/users.blade.php @@ -0,0 +1,47 @@ +{{-- Users Skeleton --}} +
    + {{-- Header --}} +
    +
    +
    +
    + + {{-- Filter Card --}} +
    +
    +
    +
    +
    +
    +
    +
    +
    + + {{-- Users Table Card --}} +
    +
    +
    + {{-- Table Header --}} +
    + + {{-- Table Rows --}} + @for ($i = 0; $i < 8; $i++) +
    +
    {{-- Name --}} +
    {{-- Email --}} +
    {{-- Role --}} +
    {{-- Status --}} +
    {{-- Org --}} +
    {{-- Actions --}} +
    + @endfor +
    + + {{-- Pagination --}} +
    +
    +
    +
    +
    +
    +
    diff --git a/resources/views/livewire/superadmin/reports/index.blade.php b/resources/views/livewire/superadmin/reports/index.blade.php index 1c02aaa..f932984 100644 --- a/resources/views/livewire/superadmin/reports/index.blade.php +++ b/resources/views/livewire/superadmin/reports/index.blade.php @@ -1,81 +1,177 @@
    -
    +
    + {{-- Header --}}
    -

    Reports & Analytics

    +

    Reports & Analytics

    Generate and export system reports

    - - Refresh - - + + + {{-- Filter Toggle Button --}} + + +
    - {{-- Filters --}} - - {{-- Active Filters Display --}} - @if (count($selectedOffices) > 0 || count($selectedEventTypes) > 0) -
    - @foreach ($selectedOffices as $orgId) - @php - $org = $offices->firstWhere('org_id', $orgId); - @endphp - @if ($org) -
    - {{ $org->org_name }} - -
    - @endif - @endforeach + {{-- Filter Drawer (Right Side) --}} + +
    + {{-- Date Range Section --}} +
    +
    + Date range + +
    +
    +
    + + +
    +
    + + +
    +
    +
    - @foreach ($selectedEventTypes as $typeId) - @php - $type = $eventTypes->firstWhere('event_type_id', $typeId); - @endphp - @if ($type) -
    - {{ $type->type_name }} - -
    - @endif - @endforeach +
    - + {{-- Organizations Section --}} +
    +
    + Organizations + +
    +
    + @foreach ($offices as $office) + + @endforeach +
    + @if (count($selectedOffices) > 0) +
    {{ count($selectedOffices) }} selected
    + @endif
    - @endif -
    - +
    - + {{-- Event Types Section --}} +
    +
    + Event types + +
    +
    + @foreach ($eventTypes as $type) + + @endforeach +
    + @if (count($selectedEventTypes) > 0) +
    {{ count($selectedEventTypes) }} selected
    + @endif +
    +
    + + + + + + + + {{-- Active Filters Display Bar --}} + @if (count($selectedOffices) > 0 || count($selectedEventTypes) > 0 || $searchTerm) +
    + + + Active Filters: + + + @if ($searchTerm) +
    + Search: "{{ $searchTerm }}" + +
    + @endif + + @foreach ($selectedOffices as $orgId) + @php $org = $offices->firstWhere('org_id', $orgId); @endphp + @if ($org) +
    + + {{ $org->org_name }} + +
    + @endif + @endforeach - {{-- Organizations Dropdown with Checkboxes --}} - + @foreach ($selectedEventTypes as $typeId) + @php $type = $eventTypes->firstWhere('event_type_id', $typeId); @endphp + @if ($type) +
    + + {{ $type->type_name }} + +
    + @endif + @endforeach - {{-- Event Types Dropdown with Checkboxes --}} - +
    - + @endif {{-- Overview Stats --}}
    @@ -84,10 +180,12 @@ class="btn btn-ghost btn-xs btn-circle">
    -
    Total Events
    -
    {{ $chartData['overview']['total_events'] ?? 0 }} +
    Total Events
    +
    + {{ $chartData['overview']['total_events'] ?? 0 }} +
    +
    In selected period
    -
    In selected period
    @@ -96,10 +194,13 @@ class="btn btn-ghost btn-xs btn-circle">
    -
    Approved Events
    -
    {{ $chartData['overview']['approved_events'] ?? 0 }} +
    Approved Events +
    +
    + {{ $chartData['overview']['approved_events'] ?? 0 }} +
    +
    Successfully approved
    -
    Successfully approved
    @@ -108,9 +209,10 @@ class="btn btn-ghost btn-xs btn-circle">
    -
    Total Tickets
    -
    {{ $chartData['overview']['total_tickets'] ?? 0 }}
    -
    All submissions
    +
    Total Tickets
    +
    + {{ $chartData['overview']['total_tickets'] ?? 0 }}
    +
    All submissions
    @@ -119,9 +221,11 @@ class="btn btn-ghost btn-xs btn-circle">
    -
    Active Organizations
    -
    {{ $chartData['overview']['active_orgs'] ?? 0 }}
    -
    With events
    +
    Active Organizations +
    +
    + {{ $chartData['overview']['active_orgs'] ?? 0 }}
    +
    With events
    @@ -130,197 +234,92 @@ class="btn btn-ghost btn-xs btn-circle">
    {{-- Events by Month Chart --}} -
    - -
    + @if (!empty($eventsByMonthChart)) + + @else +
    + No data available +
    + @endif
    {{-- Events by Type Chart --}} -
    - -
    + @if (!empty($eventsByTypeChart)) + + @else +
    + No data available +
    + @endif
    {{-- Events by Office Chart --}} -
    - -
    + @if (!empty($eventsByOfficeChart)) + + @else +
    + No data available +
    + @endif
    {{-- Ticket Status Distribution --}} -
    - -
    + @if (!empty($ticketStatusChart)) + + @else +
    + No data available +
    + @endif
    {{-- Users by Role Chart --}} -
    - -
    + @if (!empty($usersByRoleChart)) + + @else +
    + No data available +
    + @endif
    {{-- Export Modal --}} - -
    -

    +

    +
    - -@push('scripts') - -@endpush diff --git a/resources/views/livewire/superadmin/system-settings/content-section-editor.blade.php b/resources/views/livewire/superadmin/system-settings/content-section-editor.blade.php new file mode 100644 index 0000000..57c0089 --- /dev/null +++ b/resources/views/livewire/superadmin/system-settings/content-section-editor.blade.php @@ -0,0 +1,282 @@ +
    + {{-- Header with Breadcrumb --}} +
    + {{-- Breadcrumb --}} + + + {{-- Page Title --}} +
    +
    + +
    +
    +

    + {{ $isEditing ? 'Edit Content Section' : 'Add New Content Section' }} +

    +

    + {{ $isEditing ? 'Update the content section details below.' : 'Create a new content section that will be displayed in the system.' }} +

    +
    +
    +
    + + {{-- Main Form Card --}} +
    + +
    + {{-- Section 1: Content Type --}} +
    + +

    + Select the type of content you want to create. This helps organize and display content + appropriately. +

    + +
    + @foreach ($sectionTypes as $type) + + @endforeach +
    + @error('sectionType') +

    {{ $message }}

    + @enderror +
    + + {{-- Section 2: Basic Information --}} +
    + + +
    + {{-- Title --}} +
    + +

    + This will be displayed as the heading of the content section. +

    +
    + + {{-- Section Key --}} +
    + +

    + {{ $isEditing ? 'Section key cannot be changed after creation.' : 'Unique identifier. Auto-generated from title.' }} +

    +
    +
    + +
    + {{-- Display Order --}} +
    + +

    + Lower numbers appear first when multiple sections of the same type are displayed. +

    +
    + + {{-- Active Status --}} +
    + +
    +
    +
    + + {{-- Section 2.5: Target Audience (only for announcements) --}} + @if ($sectionType === 'announcement') +
    + +

    + Choose which user roles should see this announcement. Leave unchecked to show to all users. +

    + +
    + {{-- All Users Toggle --}} +
    + + +
    + + {{-- Divider --}} +
    +
    + or select specific roles +
    +
    + + {{-- Role Checkboxes --}} +
    + @foreach ($roleOptions as $role) + + @endforeach +
    + + @error('targetRoles') +

    {{ $message }}

    + @enderror +
    +
    + @endif + + {{-- Section 3: Content Editor --}} +
    + +

    + Use the rich text editor below to format your content. You can add headings, lists, links, and + more. +

    + + {{-- Trix Rich Text Editor --}} +
    + + {{-- Trix Editor Component --}} + +
    + + {{-- Formatting Help --}} +
    + Shortcuts: + Ctrl+B Bold + Ctrl+I Italic + Ctrl+K Link +
    + + {{-- Live Preview --}} + @if ($content) +
    + +
    +
    + {{ h($content) }} +
    +
    +
    + @endif +
    +
    + + {{-- Form Actions --}} + +
    + + +
    +
    +
    +
    + + {{-- Help Card --}} + +
    +
    + +
    +
    +

    Tips for Great Content

    +
      +
    • • Keep titles clear and descriptive
    • +
    • • Use short paragraphs for easy reading
    • +
    • • Use the toolbar buttons to format text (bold, italic, lists)
    • +
    • • Add links by selecting text and clicking the link button
    • +
    • • Preview your content before saving to ensure it looks right
    • +
    +
    +
    +
    +
    diff --git a/resources/views/livewire/superadmin/system-settings/content-section-manager.blade.php b/resources/views/livewire/superadmin/system-settings/content-section-manager.blade.php new file mode 100644 index 0000000..18c78e5 --- /dev/null +++ b/resources/views/livewire/superadmin/system-settings/content-section-manager.blade.php @@ -0,0 +1,227 @@ +
    + {{-- Main Card --}} + + {{-- Header Section - User-friendly with clear labels --}} +
    + {{-- Title and Description --}} +
    +
    +

    + + Content Sections +

    +

    + Manage announcements, terms & conditions, requirements, and other editable content displayed + throughout the system. +

    +
    + + + Add New Content + +
    + + {{-- Search and Filter Bar --}} +
    +
    + +
    +
    + +
    +
    +
    + + {{-- Content Sections List --}} + @if (count($contentSections) > 0) +
    + @foreach ($contentSections as $section) +
    + {{-- Left Section - Icon, Title, and Info --}} +
    + {{-- Type Icon with Color --}} +
    + +
    + + {{-- Content Info --}} +
    +
    +

    + {{ $section->title }} +

    + {{-- Status Badge --}} + @if ($section->is_active) + + + Active + + @else + + + Inactive + + @endif +
    + + {{-- Type Label and Key --}} +
    + + {{ $section->type_label }} + + + {{ $section->section_key }} + +
    + + {{-- Content Preview --}} + @if ($section->content) +

    + {{ Str::limit(strip_tags($section->content), 80) }} +

    + @endif +
    +
    + + {{-- Right Section - Actions --}} +
    + {{-- Preview Button --}} + + + {{-- Toggle Active Button --}} + + + {{-- Edit Button - Navigate to edit page --}} + + + + + {{-- Delete Button --}} + +
    +
    + @endforeach +
    + @else + {{-- Empty State --}} +
    +
    + +
    +

    No Content Sections Yet

    +

    + Create content sections like announcements, terms & conditions, and documentary requirements that + can be displayed throughout your system. +

    + + + Create Your First Content Section + +
    + @endif +
    + + {{-- Preview Modal (Keep this as a modal since it's just viewing) --}} + @if ($previewSection) + +
    + {{-- Section Info --}} +
    +
    + +
    +
    +

    + {{ $previewSection->type_label }}

    +

    {{ $previewSection->section_key }}

    +
    + @if ($previewSection->is_active) + Active + @else + Inactive + @endif +
    + + {{-- Content Display --}} +
    + {{ h($previewSection->content ?: '

    No content yet.

    ') }} +
    +
    + + + + + + Edit This Content + + +
    + @endif + + {{-- Delete Confirmation Modal --}} + @if ($deletingId) + +
    +
    +
    + +
    +
    +

    + Are you sure you want to delete this content section? +

    +

    + "{{ $deletingTitle }}" will be permanently removed and this action cannot + be undone. +

    +
    +
    + +
    +
    + +

    + If this content is currently displayed somewhere in the system, it will no longer appear + after deletion. +

    +
    +
    +
    + + + + + +
    + @endif +
    diff --git a/resources/views/livewire/superadmin/system-settings/course-manager.blade.php b/resources/views/livewire/superadmin/system-settings/course-manager.blade.php index f5299d3..16e2631 100644 --- a/resources/views/livewire/superadmin/system-settings/course-manager.blade.php +++ b/resources/views/livewire/superadmin/system-settings/course-manager.blade.php @@ -1,40 +1,64 @@
    - -
    -

    Courses

    - Add + +
    +
    +

    Academic Courses

    +

    Manage institutional degree + programs

    +
    +
    @if (count($courses) > 0) -
      +
      @foreach ($courses as $course) -
    • -
      -

      {{ $course->course_name }}

      -

      {{ $course->course_code }} - @if ($course->department) - • {{ $course->department }} - @endif -

      +
      +
      +
      + +
      +
      +

      + {{ $course->course_name }}

      +
      + + {{ $course->course_code }} + + @if ($course->department) + + {{ $course->department }} + @endif +
      +
      -
      - +
      + -
      -
    • +
      @endforeach -
    +
    @else -
    - -

    No courses found

    +
    + +

    No courses registered yet

    +

    Get started by adding your first course.

    @endif @@ -43,8 +67,7 @@
    - + - + - + @@ -83,28 +104,29 @@
    -
    +
    - -

    Warning: This action is permanent

    +

    Warning: This action is permanent

    -

    +

    Are you sure you want to delete the course - {{ $deletingCourseName }}? + {{ $deletingCourseName }}?

    This will permanently remove the course from the system.

    @if ($hasAssociatedOrganizations) -
    -

    +

    +

    Cannot delete: This course has associated student organizations.

    @@ -120,4 +142,3 @@ @endif
    - diff --git a/resources/views/livewire/superadmin/system-settings/event-type-editor.blade.php b/resources/views/livewire/superadmin/system-settings/event-type-editor.blade.php new file mode 100644 index 0000000..f65145e --- /dev/null +++ b/resources/views/livewire/superadmin/system-settings/event-type-editor.blade.php @@ -0,0 +1,138 @@ +
    + {{-- Header with Breadcrumb --}} +
    + {{-- Breadcrumb --}} + + + {{-- Page Title --}} +
    +
    + +
    +
    +

    + {{ $isEditing ? 'Edit Event Type' : 'Add New Event Type' }} +

    +

    + {{ $isEditing ? 'Update the event type details below.' : 'Create a new event type for event categorization.' }} +

    +
    +
    +
    + + {{-- Main Form Card --}} + + +
    + {{-- Section 1: Basic Information --}} +
    + + +
    + {{-- Type Name --}} +
    + +

    + This will be used to categorize events in the system. +

    +
    + + {{-- Description --}} +
    + +
    +
    +
    + + {{-- Section 2: Documentary Requirements --}} +
    + +

    + Define the documents required for this event type. These will be shown to student organizations + when submitting event tickets. +

    + + {{-- Help Text --}} +
    +
    + +
    +

    Formatting Tips

    +

    + Use the toolbar to create bulleted lists for requirements. Example: Event Proposal, + Budget Breakdown, Venue Request Form, etc. +

    +
    +
    +
    + + {{-- Trix Rich Text Editor --}} +
    + + +
    + + {{-- Live Preview --}} + @if ($documentaryRequirements) +
    + +
    +
    + {{ h($documentaryRequirements) }} +
    +
    +
    + @endif +
    +
    + + {{-- Form Actions --}} + +
    + + +
    +
    +
    + +
    diff --git a/resources/views/livewire/superadmin/system-settings/event-type-manager.blade.php b/resources/views/livewire/superadmin/system-settings/event-type-manager.blade.php index b06334a..33164ae 100644 --- a/resources/views/livewire/superadmin/system-settings/event-type-manager.blade.php +++ b/resources/views/livewire/superadmin/system-settings/event-type-manager.blade.php @@ -1,78 +1,68 @@
    - -
    -

    Event Types

    - Add + +
    +
    +

    Event Classifications

    +

    Categorize and label system + events

    +
    +
    @if (count($eventTypes) > 0) -
      +
      @foreach ($eventTypes as $eventType) -
    • -

      {{ $eventType->type_name }}

      -
      - +
      +
      +
      + +
      +
      +

      + {{ $eventType->type_name }}

      + @if ($eventType->description) +

      + {{ $eventType->description }} +

      + @endif +
      +
      +
      + - + wire:loading.attr="disabled" title="Delete Event Type">
      -
    • +
      @endforeach -
    +
    @else -
    - -

    No event types found

    +
    + +

    No event types defined

    +

    Define event categories to help structure + your scheduling.

    @endif - {{-- Add Event Type Modal --}} - -
    - - - - - - - - -
    - - {{-- Edit Event Type Modal --}} - @if ($editingEventTypeId) - -
    - - - - - - - - -
    - @endif - {{-- Delete Event Type Modal --}} @if ($deletingEventTypeId) - +
    @@ -93,10 +83,9 @@ - + @endif
    - diff --git a/resources/views/livewire/superadmin/system-settings/index.blade.php b/resources/views/livewire/superadmin/system-settings/index.blade.php index 81d8772..66b05a6 100644 --- a/resources/views/livewire/superadmin/system-settings/index.blade.php +++ b/resources/views/livewire/superadmin/system-settings/index.blade.php @@ -1,60 +1,118 @@ -
    -
    -

    System Settings

    - - Refresh Cache - +
    + {{-- Header --}} +
    +
    +

    + System + Settings

    +

    Manage core system configurations, organizations, + and + workflows.

    +
    +
    + +
    -
    - {{-- Event Types Manager Component --}} - - - {{-- Student Organizations Manager Component --}} - - - {{-- Courses Manager Component --}} - - - -
    -
    - - - - - - Update Settings - -
    -
    -
    - - -
    - - - - -
    -
    - -

    - Note: This workflow can be customized based on your - organization's - requirements. - Contact your system administrator for workflow modifications. -

    + {{-- Tabs Interface --}} + + +
    + {{-- Student Organizations Manager Component --}} + +
    +
    + + +
    + {{-- Courses Manager Component --}} + +
    +
    + + +
    + {{-- Event Types Manager Component --}} + +
    +
    + + +
    + {{-- Venues Manager Component --}} + +
    +
    + + +
    + {{-- Content Sections Manager Component --}} + +
    +
    + + +
    + +

    Set default office and institutional + approval behavior.

    +
    +
    + + + + +
    + +
    +
    +
    +
    + + +

    Current sequential approval process for + event requests.

    +
    + + + + +
    +
    + +
    +

    Workflow Note

    +

    + This sequence is hardcoded based on institutional policy. Changes require + development-level updates to the approval logic. +

    +
    +
    +
    -
    +
    - -
    + +
    diff --git a/resources/views/livewire/superadmin/system-settings/organization-manager.blade.php b/resources/views/livewire/superadmin/system-settings/organization-manager.blade.php index 9e9219e..eb084ec 100644 --- a/resources/views/livewire/superadmin/system-settings/organization-manager.blade.php +++ b/resources/views/livewire/superadmin/system-settings/organization-manager.blade.php @@ -1,38 +1,81 @@
    - -
    -

    Student Organizations

    - Add + +
    +
    +

    Student Organizations

    +

    Authorized campus groups and + clubs

    +
    +
    - @if (count($organizations) > 0) -
      - @foreach ($organizations as $organization) -
    • -
      -

      {{ $organization->org_name }}

      -

      {{ $organization->org_code }} • - {{ $organization->course->course_name ?? 'N/A' }} -

      -
      -
      - - - - -
      -
    • - @endforeach -
    - @else -
    - -

    No student organizations found

    +
    + {{-- Academic Organizations Section --}} +
    +
    + +

    Academic + Organizations

    + {{-- {{ count($academicOrgs) }} --}} + {{ count($academicOrgs) }} +
    + + @if (count($academicOrgs) > 0) +
    + @foreach ($academicOrgs as $organization) + @include('livewire.superadmin.system-settings.partials.organization-card', [ + 'organization' => $organization, + 'showCourse' => true, + ]) + @endforeach +
    + @else +
    + +

    No academic organizations

    +

    Organizations linked to specific + courses will appear here.

    +
    + @endif
    - @endif + + {{-- Non-Academic Organizations Section --}} +
    +
    + +

    Non-Academic + Organizations

    + {{ count($nonAcademicOrgs) }} +
    + + @if (count($nonAcademicOrgs) > 0) +
    + @foreach ($nonAcademicOrgs as $organization) + @include('livewire.superadmin.system-settings.partials.organization-card', [ + 'organization' => $organization, + 'showCourse' => false, + ]) + @endforeach +
    + @else +
    + +

    No non-academic organizations +

    +

    Institutional organizations (e.g., + Red Cross Youth) will appear here.

    +
    + @endif +
    +
    {{-- Add Student Organization Modal --}} @@ -60,7 +103,7 @@ {{-- Logo Upload with Preview --}}
    + hint="Max 10MB. Images auto-compressed to WebP format." accept=".jpg,.jpeg,.png,.gif,.svg,.webp" /> {{-- Loading indicator --}}
    @@ -135,7 +178,8 @@ class="w-16 h-16 object-cover rounded-lg border border-base-300" />
    + hint="Max 10MB. Images auto-compressed to WebP format." + accept=".jpg,.jpeg,.png,.gif,.svg,.webp" /> {{-- Loading indicator --}}
    -
    +
    - -

    Warning: This action is permanent

    +

    Warning: This action is permanent

    -

    - You are about to delete {{ $deletingOrgName }}. This will permanently remove all +

    + You are about to delete {{ $deletingOrgName }}. This will + permanently remove all data related to this organization.

    diff --git a/resources/views/livewire/superadmin/system-settings/partials/organization-card.blade.php b/resources/views/livewire/superadmin/system-settings/partials/organization-card.blade.php new file mode 100644 index 0000000..87c7bc9 --- /dev/null +++ b/resources/views/livewire/superadmin/system-settings/partials/organization-card.blade.php @@ -0,0 +1,52 @@ +
    +
    +
    + @if ($organization->logo_url) + {{ $organization->org_name }} + @else + + @endif + + {{-- Status indicator dot --}} +
    $organization->status === 'active', + 'bg-slate-400' => $organization->status === 'inactive', + 'bg-red-500' => $organization->status === 'suspended', + ])>
    +
    +
    +
    +

    + {{ $organization->org_name }} +

    + @if ($organization->status === 'suspended') + Suspended + @endif +
    +
    + {{ $organization->org_code }} + @if ($showCourse && $organization->course) + +

    + {{ $organization->course->course_name }}

    + @endif +
    +
    +
    +
    + + + + +
    +
    diff --git a/resources/views/livewire/superadmin/system-settings/venue-editor.blade.php b/resources/views/livewire/superadmin/system-settings/venue-editor.blade.php new file mode 100644 index 0000000..1f33a70 --- /dev/null +++ b/resources/views/livewire/superadmin/system-settings/venue-editor.blade.php @@ -0,0 +1,114 @@ +
    + {{-- Header with Breadcrumb --}} +
    + {{-- Breadcrumb --}} + + + {{-- Page Title --}} +
    +
    + +
    +
    +

    + {{ $isEditing ? 'Edit Venue' : 'Add New Venue' }} +

    +

    + {{ $isEditing ? 'Update the venue details below.' : 'Create a new venue for event scheduling.' }} +

    +
    +
    +
    + + {{-- Main Form Card --}} +
    + +
    + {{-- Section 1: Basic Information --}} +
    + + +
    + {{-- Venue Name --}} +
    + +

    + This will be displayed when users select a venue for their events. +

    +
    + + {{-- Venue Location --}} +
    + +

    + Provide additional location details to help event organizers find the venue. +

    +
    +
    +
    + + {{-- Section 2: Status --}} +
    + + +
    + + + @if ($isEditing && !$isActive) +
    +
    + +
    +

    Deactivation Notice

    +

    + Deactivating this venue will only prevent it from being selected for new + event submissions. Existing tickets that reference this venue will not be + affected. +

    +
    +
    +
    + @endif +
    +
    +
    + + {{-- Form Actions --}} + + + + +
    +
    +
    diff --git a/resources/views/livewire/superadmin/system-settings/venue-manager.blade.php b/resources/views/livewire/superadmin/system-settings/venue-manager.blade.php new file mode 100644 index 0000000..a9f265c --- /dev/null +++ b/resources/views/livewire/superadmin/system-settings/venue-manager.blade.php @@ -0,0 +1,116 @@ +
    + +
    +
    +

    Venue Management

    +

    Manage available venues for + event scheduling

    +
    + +
    + + @if (count($venues) > 0) +
    + @foreach ($venues as $venue) +
    +
    +
    + +
    +
    +
    +

    + {{ $venue->venue_name }} +

    + @if (!$venue->is_active) + + Inactive + + @endif +
    + @if ($venue->venue_location) +

    + + {{ $venue->venue_location }} +

    + @endif +
    +
    +
    + {{-- Toggle Active Status --}} + + + {{-- Edit --}} + + + {{-- Delete --}} + + +
    +
    + @endforeach +
    + @else +
    + +

    No venues defined

    +

    Add venues where events can be held.

    +
    + @endif +
    + + {{-- Delete Venue Modal --}} + @if ($deletingVenueId) + +
    +
    + + + Are you sure you want to delete {{ $deletingVenueName }}? + +
    + + @if ($hasAssociatedTickets) +
    + +
    +

    This venue cannot be deleted

    +

    + This venue is being used by {{ $associatedTicketsCount }} + {{ $associatedTicketsCount === 1 ? 'ticket' : 'tickets' }}. +

    +

    + Tip: Consider deactivating the venue instead. Deactivating will prevent + new event submissions using this venue while preserving existing ticket data. +

    +
    +
    + @endif +
    + + + + + +
    + @endif +
    diff --git a/resources/views/livewire/superadmin/tickets/index.blade.php b/resources/views/livewire/superadmin/tickets/index.blade.php index 1b90292..7fbe082 100644 --- a/resources/views/livewire/superadmin/tickets/index.blade.php +++ b/resources/views/livewire/superadmin/tickets/index.blade.php @@ -66,7 +66,10 @@ @endscope @scope('cell_organization', $ticket) - {{ $ticket->user->studentOrganization->org_name ?? 'N/A' }} + @php $orgDeleted = $ticket->user?->studentOrganization?->trashed(); @endphp + + {{ $orgDeleted ? 'Deleted Organization' : $ticket->user?->studentOrganization?->org_name ?? 'N/A' }} + @endscope @scope('cell_status', $ticket) diff --git a/resources/views/livewire/superadmin/users/index.blade.php b/resources/views/livewire/superadmin/users/index.blade.php index d52653d..f79e35c 100644 --- a/resources/views/livewire/superadmin/users/index.blade.php +++ b/resources/views/livewire/superadmin/users/index.blade.php @@ -51,74 +51,77 @@
    - - - @scope('cell_role_id', $user) - @php - $roleString = $user->role?->role_name ?? 'unknown'; - @endphp - - @endscope - - @scope('cell_email_verified_at', $user) - @if ($user->email_verified_at) - - @else - - @endif - @endscope - - @scope('cell_organization', $user) - @if ($user->studentOrganization) - {{ $user->studentOrganization->org_name }} - @elseif($user->office) - {{ $user->office->office_name }} - @else - N/A - @endif - @endscope + +
    + + @scope('cell_role_id', $user) + @php + $roleString = $user->role?->role_name ?? 'unknown'; + @endphp + + @endscope + + @scope('cell_email_verified_at', $user) + @if ($user->email_verified_at) + + @else + + @endif + @endscope - @scope('cell_actions', $user) -
    - - Edit - - @if (!$user->isSuperAdmin()) - - + @scope('cell_organization', $user) + @if ($user->studentOrganization) + {{ $user->studentOrganization->org_name }} + @elseif($user->office) + {{ $user->office->office_name }} + @else + N/A @endif -
    - @endscope + @endscope - -
    - -

    No users found

    -

    + @scope('cell_actions', $user) +

    + + Edit + + @if (!$user->isSuperAdmin()) + + + @endif +
    + @endscope + + +
    + +

    No users found

    +

    + @if ($this->hasActiveFilters()) + We couldn't find any users matching "{{ $search }}" or your + selected + role. + @else + There are no users registered in the system yet. + @endif +

    @if ($this->hasActiveFilters()) - We couldn't find any users matching "{{ $search }}" or your - selected - role. - @else - There are no users registered in the system yet. + @endif -

    - @if ($this->hasActiveFilters()) - - @endif -
    -
    - +
    +
    +
    +
    {{-- Custom Pagination --}} @if ($users->hasPages()) diff --git a/resources/views/reports/osa/summary-pdf.blade.php b/resources/views/reports/osa/summary-pdf.blade.php index b2fae9a..e44eb1d 100644 --- a/resources/views/reports/osa/summary-pdf.blade.php +++ b/resources/views/reports/osa/summary-pdf.blade.php @@ -80,10 +80,12 @@ @forelse ($data as $ticket) + @php $orgDeleted = $ticket->user?->studentOrganization?->trashed(); @endphp {{ $ticket->ticket_number }} {{ $ticket->title }} - {{ optional($ticket->user->studentOrganization)->org_name ?? 'N/A' }} + {{ $orgDeleted ? 'Deleted Organization' : optional($ticket->user?->studentOrganization)->org_name ?? 'N/A' }} + {{ optional($ticket->eventType)->type_name ?? 'N/A' }} {{ ucfirst($ticket->status) }} {{ optional($ticket->created_at)?->format('Y-m-d H:i') }} diff --git a/routes/web.php b/routes/web.php index 114f7ec..5072beb 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,7 +1,8 @@ route($user->getDashboardRoute()); } - // Otherwise, show the welcome page - return view('osa.welcome'); + // Redirect to login page + return redirect()->route('login'); }); // About Us - Public route Route::get('/about-us', AboutUs::class)->name('about-us'); +// FAQ - Public route +Route::get('/faq', Faq::class)->name('faq'); + // Profile route (accessible by all authenticated users) Route::middleware(['auth', 'verified'])->group(function () { // Generic profile route - redirects based on user role @@ -125,6 +129,15 @@ Route::get('/reports', \App\Livewire\Superadmin\Reports\Index::class)->name('superadmin.reports'); Route::get('/notifications', \App\Livewire\Superadmin\Notifications::class)->name('superadmin.notifications'); Route::get('/system-settings', SystemSettingsIndex::class)->name('superadmin.system-settings'); + Route::get('/system-settings/content/create', \App\Livewire\Superadmin\SystemSettings\ContentSectionEditor::class)->name('superadmin.content-section.create'); + Route::get('/system-settings/content/{id}/edit', \App\Livewire\Superadmin\SystemSettings\ContentSectionEditor::class)->name('superadmin.content-section.edit'); + Route::get('/system-settings/event-types/create', \App\Livewire\Superadmin\SystemSettings\EventTypeEditor::class)->name('superadmin.event-type.create'); + Route::get('/system-settings/event-types/{id}/edit', \App\Livewire\Superadmin\SystemSettings\EventTypeEditor::class)->name('superadmin.event-type.edit'); + Route::get('/system-settings/venues/create', \App\Livewire\Superadmin\SystemSettings\VenueEditor::class)->name('superadmin.venue.create'); + Route::get('/system-settings/venues/{id}/edit', \App\Livewire\Superadmin\SystemSettings\VenueEditor::class)->name('superadmin.venue.edit'); + Route::get('/faqs', \App\Livewire\Superadmin\FaqManager::class)->name('superadmin.faqs'); + Route::get('/faqs/create', \App\Livewire\Superadmin\FaqEditor::class)->name('superadmin.faq.create'); + Route::get('/faqs/{id}/edit', \App\Livewire\Superadmin\FaqEditor::class)->name('superadmin.faq.edit'); Route::get('/admin-tools', \App\Livewire\Superadmin\AdminTools\Index::class)->name('superadmin.admin-tools'); Route::get('/logs', Logs::class)->name('superadmin.logs'); Route::get('/profile', SuperadminProfile::class)->name('superadmin.profile');